之前写过一次在线聊天网站,不过那次是无框架版的,这次用框架构建网站,基本功能和上次差不多
涉及知识
java
spring(4.3.5):spring、spring MVC
hibernate
bootstrap
jsp
JavaScript,jquery
websocket
mysql
功能
1.用户的登录、注册、注销、密码修改
2.获知在线用户名字及数量
3.向在线用户发送消息
4.查看与该用户的历史信息
5.当有非当前聊天用户的信息到来时,会有提示
数据库
类一览
mysql数据库建立
一个账户表,一个聊天内容表
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';
hibernate实体层bean
P.S.关于hibernate注解解释可看http://blog.csdn.net/name_z/article/details/51318271
实体类Account和TalkContent都继承了通用实体类CommonBean
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;@Transientprotected 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 {...}
使用CommonBean原因
1.各实体类的相同部分(如这次的id)可放在CommonBean中,从而减少了代码量,使代码整洁
2.采用abstract方法约束子对象,方便后面dao层使通用持久层CommonDao类
hibernate持久层dao
使用模板与反射减少代码量。
所有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类后,对于增删改查的方法均可不作修改直接调用父类CommonBean的增删改查即可对子类进行操作
@Repository
public abstract class CommonDao<T extends CommonBean> implements ICommonDao<T> {@Autowiredprotected 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();}@Overridepublic 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;}}@Overridepublic boolean save(T entity) {return this.execute(SAVE_METHOD, entity);}@Overridepublic boolean delete(T entity) {return this.execute(DELETE_METHOD, entity);}@Overridepublic boolean updateByID(T entity) {return this.execute(UPDATE_METHOD, entity);}@Overridepublic T getByID(T entity) {return this.getByID(entity.getId());}@Overridepublic T getByID(int id) {return this.queryUnique(HQLGenerator.generateSingleEqualQueryHql(this.CLASS_SIMPLENAME,this.ID_COLUMN_NAME, String.valueOf(id)));}@SuppressWarnings("unchecked")@Overridepublic 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());}@Overridepublic Account getByName(String name) {return this.queryUnique(HQLGenerator.generateSingleEqualQueryHql(this.CLASS_SIMPLENAME, NAME_PROP_NAME, name));}@Overridepublic Account getByName(Account account) {return this.getByName(account.getName());}}
hql语句生成
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:负责聊天内容的保存,分为保存到内存和保存到数据库
TalkManager
可以指定保存在内存中的聊天内容的最大数量,超过清空
保存到内存中的聊天内容
每次点开目标对象,都会显示这些内容(服务器中途没有关闭过)
保存到数据库中的内容
只保存在数据库中,没有保存在内存中的内容,只有点击历史信息后,才能显示,点击历史信息后,这部分内容也会保存到内存中
spring mvc表单提交
就是点击按钮后,客户端才能发送数据给服务器,服务器spring mvc返回视图以及数据
登录页面jsp:
<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>
login的controller:
@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";}
}
使用websocket和jquery完成实时数据显示
因为客户端与客户端之间的交互需要实时性(发送的、接收到的信息能马上显示),因此不能用spring mvc表单提交,要用websocket。而使用websocket提交信息后,服务器只会发回信息,而不是网页,因此需要jquery实时更新当前显示的信息,包括聊天信息和提示信息
步骤:
1.客户端与服务器建立联系后,客户端马上向服务器发送注册信息(将账户名字与(websocket)session构成联系),每次的刷新都回导致注册信息的更新
2.用户点击发送按钮后,客户端向服务器发送信息,服务器保存信息并且发送给目标用户
3.用户关闭页面时,服务器获知并且在注册信息表中删除该用户
main.jsp
注意点:
聊天内容和提示信息除了有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>
...
main.js
//发送聊天内容,格式为: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);}}
}
服务器负责接受websocket的controller
@Controller
@ServerEndpoint("/talk")
public class TalkWebSocket extends TextWebSocketHandler {.../*** 接受客户端发送的信息,如果为注册信息则注册(将用户名字与该session建立联系),否则为聊天信息,如果为有效聊天信息则保存到数据库,并且发送给目标账户* @param message* @param session* @throws IOException*/@OnMessagepublic 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 ;}//根据名字获取sessionSession targetSession = OnlineAccountManager.getSession(targetAccountName);if(targetSession != null){//保存聊天内容到数据库TalkManager.saveTalkInDatabase(srcAccountName, targetAccountName, talk);//使用session向客户端发送信息targetSession.getBasicRemote().sendText(srcAccountName + ">" + talk);}}}
}
bootstrap
采用网格系统作为基础构建网页,使用部分bootstrap中的部件:导航栏、输入狂组、按钮、面板
效果展示
登录页面
http://localhost:8080/Talk/login.do
登录失败:
注册页面
http://localhost:8080/Talk/regist.do
登录失败:
主页:
刚登录进来的页面:
发送消息:
接受信息但当前聊天内容并不是该发送者:
接受信息: