之前写过一次在线聊天网站,不过那次是无框架版的,这次用框架构建网站,基本功能和上次差不多
github:https://github.com/ZhongWenhui1995/javaweb-spring-hibernate-
java
spring(4.3.5):spring、spring MVC
hibernate
bootstrap
jsp
JavaScript,jquery
websocket
mysql
1.用户的登录、注册、注销、密码修改
2.获知在线用户名字及数量
3.向在线用户发送消息
4.查看与该用户的历史信息
5.当有非当前聊天用户的信息到来时,会有提示
一个账户表,一个聊天内容表
create database db_talk;
create table tbl_account
(
id int not null primary key auto_increment,
name varchar(30) not null unique,
password char(20) not null
)CHARACTER SET ‘utf8‘
COLLATE ‘utf8_general_ci‘;
create table tbl_talk
(
id int not null primary key auto_increment,
content varchar(255),
srcAccountId int not null,
targetAccountId int not null,
time datetime not null default now(),
foreign key(srcAccountId) references tbl_account(id),
foreign key(targetAccountId) references tbl_account(id)
)CHARACTER SET ‘utf8‘
COLLATE ‘utf8_general_ci‘;
P.S.关于hibernate注解解释可看http://blog.csdn.net/name_z/article/details/51318271
实体类Account和TalkContent都继承了通用实体类CommonBean
@MappedSuperclass
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class CommonBean {
@Id
@Column(name = "id", insertable = false, updatable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
protected int id;
@Transient
protected static final String DEFAULT_ID_COLUMN_NAME = "id";
public int getId() {
return id;
}
// public void setId(int id) {
// this.id = id;
// }
//获取本类的简单类名
public abstract String getClassSimpleName();
public String getIdColumnName(){
return DEFAULT_ID_COLUMN_NAME;
}
}
@Entity
@Table(name = "tbl_talk")
public class TalkContent extends CommonBean {...}
@Entity
@Table(name = "tbl_account")
public class Account extends CommonBean {...}
1.各实体类的相同部分(如这次的id)可放在CommonBean中,从而减少了代码量,使代码整洁
2.采用abstract方法约束子对象,方便后面dao层使通用持久层CommonDao类
使用模板与反射减少代码量。
所有dao类接口都继承了ICommonDao,CommonDao实现了ICommonDao,所有dao类实现都继承了CommonDao
CommonDao使用模板设计,所有子类能不做修改直接调用CommonDao中方法进行实体的增删改查
public interface ICommonDao<T extends CommonBean> {
boolean save(T entity);
boolean delete(T entity);
boolean updateByID(T entity);
T getByID(T entity);
T getByID(int id);
List<T> getAll();
void setSessionFactory(SessionFactory sessionFactory);
}
public interface IAccountDao extends ICommonDao<Account> {
Account getByName(String name);
Account getByName(Account account);
}
public interface ITalkContentDao extends ICommonDao<TalkContent> {...}
通过继承实现CommonBean类后,对于增删改查的方法均可不作修改直接调用父类CommonBean的增删改查即可对子类进行操作
@Repository
public abstract class CommonDao<T extends CommonBean> implements ICommonDao<T> {
@Autowired
protected SessionFactory sessionFactory;
private static final String SAVE_METHOD = "save";
private static final String DELETE_METHOD = "delete";
private static final String UPDATE_METHOD = "update";
protected final String CLASS_SIMPLENAME;
protected final String ID_COLUMN_NAME;
/**
* 传入一个已经实体化的对象(非null对象),并用该对象初始化CLASS_SIMPLENAME、ID_COLUMN_NAME两个变量(CommonBean中有这两个方法)
*
* @param entity
*/
public CommonDao(T entity) {
this.CLASS_SIMPLENAME = entity.getClassSimpleName();
this.ID_COLUMN_NAME = entity.getIdColumnName();
}
/**
* 获取开启事务后的session
*
* @return
*/
protected Session getCurrentSession() {
Session session = this.sessionFactory.getCurrentSession();
try {
session.beginTransaction();
} catch (TransactionException e) {
}
return session;
}
/**
* 传入hql语句进行查询,返回list
*
* @param hql
* @return
*/
@SuppressWarnings("unchecked")
protected List<T> queryList(String hql) {
Session session = this.getCurrentSession();
Query query = session.createQuery(hql);
return query.list();
}
/**
* 传入hql语句进行查询,如果查询不到结果,返回null
*
* @param hql
* @return
*/
@SuppressWarnings("unchecked")
protected T queryUnique(String hql) {
Session session = this.getCurrentSession();
Query query = session.createQuery(hql);
return (T) query.uniqueResult();
}
@Override
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
private Method getMethod(String operation) throws Exception {
return Session.class.getMethod(operation, Object.class);
}
/**
* 通过反射执行执行增、删、改操作
*
* @param oper
* @param entity
* @return
*/
private boolean execute(String oper, T entity) {
try {
Session session = this.getCurrentSession();
Method method = this.getMethod(oper);
method.invoke(session, entity);
session.getTransaction().commit();
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
public boolean save(T entity) {
return this.execute(SAVE_METHOD, entity);
}
@Override
public boolean delete(T entity) {
return this.execute(DELETE_METHOD, entity);
}
@Override
public boolean updateByID(T entity) {
return this.execute(UPDATE_METHOD, entity);
}
@Override
public T getByID(T entity) {
return this.getByID(entity.getId());
}
@Override
public T getByID(int id) {
return this.queryUnique(HQLGenerator.generateSingleEqualQueryHql(this.CLASS_SIMPLENAME,
this.ID_COLUMN_NAME, String.valueOf(id)));
}
@SuppressWarnings("unchecked")
@Override
public List<T> getAll() {
Session session = this.getCurrentSession();
return session.createQuery(HQLGenerator.generateAllQuery(this.CLASS_SIMPLENAME)).list();
}
AccountDao类:
@Repository
public class AccountDao extends CommonDao<Account> implements IAccountDao {
private static final String NAME_PROP_NAME = "name";
public AccountDao() {
super(new Account());
}
@Override
public Account getByName(String name) {
return this.queryUnique(HQLGenerator.generateSingleEqualQueryHql(this.CLASS_SIMPLENAME, NAME_PROP_NAME, name));
}
@Override
public Account getByName(Account account) {
return this.getByName(account.getName());
}
}
hql语句的获取统一从一个类中获取,负责的hql语句直接写成static final变量
public class HQLGenerator {
//from classname obj where obj.column = ‘value‘
private static final String SINGLE_QUERY = ALL_QUERY + " obj where obj." + COLUMN_PLACER + BLANK + EQUAL + BLANK + SINGLE_QUOTE + VALUE_PLACER + SINGLE_QUOTE;
...
/**
* 生成对单个列的值进行查询的hql语句
*
* @param classname
* 类名
* @param column
* 列名
* @param value
* 值
* @return
*/
public static String generateSingleEqualQueryHql(String classname, String column, String value) {
return SINGLE_QUERY.replace(CLASSNAME_PLACER, classname).replace(COLUMN_PLACER, column).replace(VALUE_PLACER,
value);
}
...
}
AccountManager:负责账户的登录、修改
OnlineAccountManager:保存当前在线账户的名字与session联系
TalkManager:负责聊天内容的保存,分为保存到内存和保存到数据库
可以指定保存在内存中的聊天内容的最大数量,超过清空
每次点开目标对象,都会显示这些内容(服务器中途没有关闭过)
只保存在数据库中,没有保存在内存中的内容,只有点击历史信息后,才能显示,点击历史信息后,这部分内容也会保存到内存中
就是点击按钮后,客户端才能发送数据给服务器,服务器spring mvc返回视图以及数据
<form:form method="POST" modelAttribute="account" action="/Talk/login.do">
<table>
<tr>
<td><form:label path="name" >名字:</form:label></td>
<td><form:input path="name" id="name" class="form-control input-sm"/></td>
</tr>
<tr>
<td><form:label path="password">密码:</form:label></td>
<td><form:password path="password" id="password" class="form-control input-sm"/></td>
</tr>
<tr>
<td colspan="2"><input type="submit" value="登录" /></td>
</tr>
</table>
</form:form>
@Controller
public class LoginController {
@RequestMapping(method = RequestMethod.POST, path = "/login")
public String login(ModelMap model, Account account) {
account = AccountManager.login(account);
if (account != null) {
TalkContent talkContent = new TalkContent();
talkContent.setSrcAccount(account);
talkContent.setTargetAccountName("");
//给视图添加数据
model.addAttribute("talkContent", talkContent);
model.addAttribute("onlineNum", OnlineAccountManager.getOnlineNum());
model.addAttribute("accounts", OnlineAccountManager.getOnlineAccountsName());
return "main";
}
model.addAttribute("warnMessage", AccountManager.ERROR_LOGIN);
return "login";
}
@RequestMapping(method = RequestMethod.GET, path = "/login")
public String getLoginJsp(ModelMap model) {
//因为jsp页面提交中注明account,因此必须添加account属性
model.addAttribute("account", new Account());
model.addAttribute("warnMessage", "");
return "login";
}
}
因为客户端与客户端之间的交互需要实时性(发送的、接收到的信息能马上显示),因此不能用spring mvc表单提交,要用websocket。而使用websocket提交信息后,服务器只会发回信息,而不是网页,因此需要jquery实时更新当前显示的信息,包括聊天信息和提示信息
1.客户端与服务器建立联系后,客户端马上向服务器发送注册信息(将账户名字与(websocket)session构成联系),每次的刷新都回导致注册信息的更新
2.用户点击发送按钮后,客户端向服务器发送信息,服务器保存信息并且发送给目标用户
3.用户关闭页面时,服务器获知并且在注册信息表中删除该用户
注意点:
聊天内容和提示信息除了有id方便jquery的操作,还需要${}保证当点击刷新或者其他spring mvc提交表单的操作后,聊天内容和提示信息可以从服务器中获取而显示,否则,只有jquery设置的内容会丢失。
...
<!-- 聊天内容部分 -->
<div class="panel-body" data-spy="scroll" data-target="#navbar-example" data-offset="300" style="height: 300px; overflow: auto; position: relative;">
<pre id="talks">${talks}</pre>
</div>
...
<!-- 信息提示部分 -->
<div class="panel-body" data-spy="scroll" data-target="#navbar-example" data-offset="0" style="height: 250px; overflow: auto; position: relative;">
<pre id="message">${message}</pre>
</div>
...
<!-- 发送聊天内容部分 -->
<div class="input-group">
<input id="talk" type="text" class="form-control">
<span class="input-group-addon"> <button id="send" type="button" class="btn btn-default">发送</button></span>
</div>
...
//发送聊天内容,格式为:from(发送人)to(接收人)talk(聊天内容)
function setBtnSend() {
$(document).ready(
function() {
$("#send").click(
function() {
var srcName = $("#accountName").text();
var targetName = $("#targetAccountName").text();
var talk = $("#talk").val();
var message = "from(" + srcName + ")to(" + targetName + ")talk(" + talk + ")";
sendMessage(message);
var temp = $("#talks").text();
$("#talks").text(temp + "\n" + srcName + ">" + talk);
});
});
}
function setWebSocket() {
//当与服务器建立联系后,马上发送注册信息,因此每次页面的刷新也会重新发送注册信息
webSocket.onopen = function(event) {
var message = "regist(" + $("#accountName").text() + ")";
sendMessage(message);
};
...
//接受到服务器发送的信息
webSocket.onmessage = function(event) {
var targetName = $("#targetAccountName").text();
var temp = $("#talks").text();
var message = event.data;
//判断发送对象是否为当前聊天对象,是则显示到聊天内容,否则显示到提示信息中
if (message.indexOf(targetName + ">") > 0) {
$("#talks").text(temp + "\n" + message);
} else {
$("#message").text(message);
}
}
}
@Controller
@ServerEndpoint("/talk")
public class TalkWebSocket extends TextWebSocketHandler {
...
/**
* 接受客户端发送的信息,如果为注册信息则注册(将用户名字与该session建立联系),否则为聊天信息,如果为有效聊天信息则保存到数据库,并且发送给目标账户
* @param message
* @param session
* @throws IOException
*/
@OnMessage
public void onMessage(String message, Session session) throws IOException {
message = URLDecoder.decode(message, "UTF-8");
if(message.startsWith(REGIST)){
//注册该用户,将名字与session构成联系
OnlineAccountManager.regist(regexGroupOne(REGIST_PATTERN, message), session);
}else{
String srcAccountName = regexGroupOne(SRCACCOUNT_PATTERN, message);
String targetAccountName = regexGroupOne(TARGETACCOUNT_PATTERN, message);
String talk = regexGroupOne(TALK_PATTERN, message);
//如果接收人为空或者聊天内容为空,直接忽略该条信息
if(targetAccountName == null || "".equals(talk)){
return ;
}
//根据名字获取session
Session targetSession = OnlineAccountManager.getSession(targetAccountName);
if(targetSession != null){
//保存聊天内容到数据库
TalkManager.saveTalkInDatabase(srcAccountName, targetAccountName, talk);
//使用session向客户端发送信息
targetSession.getBasicRemote().sendText(srcAccountName + ">" + talk);
}
}
}
}
采用网格系统作为基础构建网页,使用部分bootstrap中的部件:导航栏、输入狂组、按钮、面板
http://localhost:8080/Talk/login.do
登录失败:
http://localhost:8080/Talk/regist.do
登录失败:
刚登录进来的页面:
发送消息:
接受信息但当前聊天内容并不是该发送者:
接受信息:
原文:http://blog.csdn.net/name_z/article/details/55189873