MyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation 迁移到了google code,并且改名为MyBatis 。2013年11月迁移到Github。
iBATIS一词来源于“internet”和“abatis”的组合,是一个基于Java的持久层框架。iBATIS提供的持久层框架包括SQL Maps和Data Access Objects(DAO)
MyBatis 是支持普通 SQL查询,存储过程和高级映射的优秀持久层框架。MyBatis 消除了几乎所有的JDBC代码和参数的手工设置以及结果集的检索。MyBatis 使用简单的 XML或注解用于配置和原始映射,将接口和 Java 的POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。
————————————————————————————————————————

我们在上一章开发了在线用户列表功能,本章我们开发聊天功能。聊天功能主要包括聊天记录显示,聊天内容发送和聊天信息接收。在在线用户列表中非当前用户后面存在“聊天”,下面我们实现这个按钮的功能,点击时建立当前用户与该用户的聊天链接。在/js/master.js中添加实现,在master.vm中引入master.js方法。chat函数实现获取用户的聊天界面,包括聊天记录以及发送聊天信息界面,替换到id=chatDiv的div中,具体实现如下:

[code lang=”javascript”]
function chat(fromUserName, toUserName) {
$.ajax({
type: "POST",
url: "/imlearntest/chat/chat",
data:{fromUserName:fromUserName, toUserName:toUserName},
success: function(result){
$("#chatDiv").html(result);
}
});
}
[/code]

实现链接/chat/chat,展示用户已有聊天信息,创建类com.sunhaojie.imlearntest.controller.ChatController,添加chat方法,实现如下:

[code lang=”java”]
@Controller
@RequestMapping("chat")
public class ChatController {

@Autowired
private ChatRecordBo chatRecordBo;

@RequestMapping("chat")
public String chat(@ModelAttribute("fromUserName") String fromUserName,
@ModelAttribute("toUserName") String toUserName, HttpServletRequest request,
HttpServletResponse response) {

PageModel<ChatRecordVo> chatRecordPageModel = chatRecordBo.list(fromUserName, toUserName, 0, 10);
request.setAttribute("pageModel", chatRecordPageModel);

return "chat/chat";
}
}
/**
*com.sunhaojie.imlearntest.vo.ChatRecordVo聊天记录实体类
*/
public class ChatRecordVo {
private int id;
private Date createTime;
private Date updateTime;
private int version;
/**
* 发送者用户名
*/
private String fromUserName;
/**
* 接受者用户名
*/
private String toUserName;
/**
* 聊天内容
*/
private String content;
}
/**
*com.sunhaojie.imlearntest.bo.ChatRecordBo聊天记录业务类
*/
@Service
public class ChatRecordBo {

public PageModel<ChatRecordVo> list(String fromUserName, String toUserName, int offset, int limit) {

PageModel<ChatRecordVo> pageModel = new PageModel<ChatRecordVo>();
List<ChatRecordVo> chatRecordVoList = new ArrayList<ChatRecordVo>();
for (int i=offset; i<offset + limit; i++) {
ChatRecordVo chatRecordVo = new ChatRecordVo();
chatRecordVo.setContent("内容" + i);
chatRecordVo.setCreateTime(new Date());
chatRecordVo.setFromUserName(fromUserName);
chatRecordVo.setToUserName(toUserName);
chatRecordVo.setId(i);
chatRecordVo.setUpdateTime(new Date());
chatRecordVo.setVersion(0);
chatRecordVoList.add(chatRecordVo);
}
pageModel.setClazz(ChatRecordVo.class);
pageModel.setData(chatRecordVoList);
pageModel.setLimit(limit);
pageModel.setOffset(offset);
pageModel.setTotal(Integer.MAX_VALUE);
pageModel.addParam("fromUserName", fromUserName);
pageModel.addParam("toUserName", toUserName);
return pageModel;
}
}
/**
*com.sunhaojie.imlearntest.vo.PageModel添加addParam方法
*/
public void addParam(String paramKey, String paramValue) {
if (params == null) {
params = new HashMap<String, String>();
}
params.put(paramKey, paramValue);
}
[/code]

因为局部刷新页面,所以我们把需要更新的div数据抽取为单独的页面,根据我们上一章关于分页的经验,我们把聊天记录列表也抽取为独立的页面,具体内容如下:

[code lang=”java”]
<h3>聊天窗口</h3>
<div id="chatRecordList">
#parse("/WEB-INF/vm/chat/chatlog.vm")
</div>
<div style="height:1px; width:100%; border-left:1px #fff solid;float:left;margin:5px;"></div>
<div align="left">
<textarea rows="4" cols="60" id="messageTextarea"></textarea> <input type="button" value="发送" onclick="sendMessage(‘$!{fromUserName}’, ‘$!{toUserName}’)"/>
</div>
chat/chatlog.vm
<table border="1" width="100%">
<thead>
<tr>
<th>$!{toUserName}的信息</th>
<th>你的信息</th>
</tr>
</thead>
<tbody id="chatLogTbody">
#foreach($item in ${pageModel.getData()})
<tr>
#if(${item.fromUserName} != ${fromUserName})
<td colspan="2" align="left">${item.content}</td>
#else
<td colspan="2" align="right">${item.content}</td>
#end
</tr>
#end
</tbody>
</table>
<div id="chatRecordPagination">
#parse("/WEB-INF/vm/pagination.vm")
</div>
<script type="text/javascript">
/**
* 更新聊天记录
*/
function chatRecordPagination(result) {
$("#chatRecordList").html(result);
}

$("#chatRecordPagination a").click(function(){
var $this = $(this);
clickGoto(chatRecordPagination, $this);
});
$("#chatRecordPagination input").change(function(){
var $this = $(this);
changeGoto(chatRecordPagination, $this);
});
</script>
[/code]

通过两个浏览器打开连接,使用不同的用户名登录,点击”聊天”按钮,显示了聊天记录信息,但是点击下一页还不能用,下面实现聊天记录的分页功能,在BaseURLUtil的静态urlMap中添加”urlMap.put(ChatRecordVo.class, “/chat/chatRecordList?1=1″);”
在ChatController中添加chatRecordList方法,具体代码如下:

[code lang=”java”]
@RequestMapping("chatRecordList")
public String chatRecordList(@ModelAttribute("page") int page, @ModelAttribute("limit") int limit,
@ModelAttribute("fromUserName") String fromUserName,
@ModelAttribute("toUserName") String toUserName, HttpServletRequest request,
HttpServletResponse response) {
if (page <= 0) {
page = 1;
}
PageModel<ChatRecordVo> chatRecordPageModel = chatRecordBo.list(fromUserName, toUserName, (page – 1) * limit,
limit);
request.setAttribute("pageModel", chatRecordPageModel);
return "user/chatlog";
}
[/code]

修改ChatRecordBo中的list方法,通过dao从数据库中读取数据,创建表chat_record并添加ChatRecord的映射文件/mybatis/sqlmaps/ChatRecord.xml,内容如下:

[code lang=”sql”]
CREATE TABLE `chat_record` (
`ID` int(11) NOT NULL AUTO_INCREMENT,
`CREATE_TIME` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`UPDATE_TIME` timestamp NOT NULL DEFAULT ‘0000-00-00 00:00:00’,
`VERSION` int(11) NOT NULL DEFAULT ‘0’,
`FROM_USER_NAME` varchar(64) NOT NULL,
`TO_USER_NAME` varchar(64) NOT NULL,
`CONTENT` varchar(256) NOT NULL,
PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
[/code]

[code lang=”xml”]
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.virxue.baseweb.dao.ChatRecordDao">
<sql id="row_sql">
ID, CREATE_TIME, UPDATE_TIME, VERSION,FROM_USER_NAME,TO_USER_NAME,
CONTENT
</sql>
<insert id="insert" parameterType="ChatRecordPo">
insert into CHAT_RECORD(
CREATE_TIME, UPDATE_TIME, VERSION,FROM_USER_NAME,TO_USER_NAME,CONTENT
) values (
now(), now(), 0, #{fromUserName}, #{toUserName}, #{content}
)
</insert>
<select id="list" resultType="ChatRecordPo">
select * from CHAT_RECORD
where 1 = 1
<if test="fromUserName != null &amp;&amp; toUserName != null">
and (from_user_name = #{fromUserName} and to_user_name = #{fromUserName}) or (from_user_name = #{toUserName} or to_user_name = #{toUserName})
</if>
order by id desc
limit #{offset}, #{limit}
</select>
<select id="count" resultType="int">
select count(*) from CHAT_RECORD
where 1 = 1
<if test="fromUserName != null">
and from_user_name = #{fromUserName} or to_user_name = #{fromUserName}
</if>
<if test="toUserName != null">
and from_user_name = #{toUserName} or to_user_name = #{toUserName}
</if>
</select>
</mapper>
[/code]

添加com.sunhaojie.imlearntest.po.ChatRecordPo和com.sunhaojie.imlearntest.dao.ChatRecordDao,并在com.sunhaojie.imlearntest.vo.ChatRecordVo中添加ChatRecordPo为入参的构造方法,具体代码如下:

[code lang=”java”]
public class ChatRecordPo {
private int id;
private Date createTime;
private Date updateTime;
private int version;
private String fromUserName;
private String toUserName;
private String content;
}
public interface ChatRecordDao {
public int insert(ChatRecordPo chatRecordPo);

public List<ChatRecordPo> list(@Param("fromUserName") String fromUserName, @Param("toUserName") String toUserName,
@Param("offset") int offset, @Param("limit") int limit);

public Integer count(@Param("fromUserName") String fromUserName, @Param("toUserName") String toUserName);
}
/**
* 构造方法
*/
public ChatRecordVo(ChatRecordPo chatRecordPo) {
this.content = chatRecordPo.getContent();
this.createTime = chatRecordPo.getCreateTime();
this.fromUserName = chatRecordPo.getFromUserName();
this.id = chatRecordPo.getId();
this.toUserName = chatRecordPo.getToUserName();
this.updateTime = chatRecordPo.getUpdateTime();
this.version = chatRecordPo.getVersion();
}
[/code]

修改ChatRecordBo中的list方法,从数据库中查询总数,获取分页数据,具体代码如下:

[code lang=”java”]
public PageModel<ChatRecordVo> list(String fromUserName, String toUserName, int offset, int limit) {
List<ChatRecordPo> list = chatRecordDao.list(fromUserName, toUserName, offset, limit);
Integer total = chatRecordDao.count(fromUserName, toUserName);

List<ChatRecordVo> chatRecordVoList = new ArrayList<ChatRecordVo>();
if (CollectionUtils.isNotEmpty(list) && total != null && total != 0) {
for (ChatRecordPo chatRecordPo : list) {
ChatRecordVo chatRecordVo = new ChatRecordVo(chatRecordPo);
chatRecordVoList.add(chatRecordVo);
}
}
PageModel<ChatRecordVo> pageModel = new PageModel<ChatRecordVo>();
pageModel.setClazz(ChatRecordVo.class);
pageModel.setData(chatRecordVoList);
pageModel.setLimit(limit);
pageModel.setOffset(offset);
pageModel.setTotal(total);
pageModel.addParam("fromUserName", fromUserName);
pageModel.addParam("toUserName", toUserName);
return pageModel;
}
[/code]

在展示的页面上有“发送”按钮,在这个按钮上我们注册一个click的响应方法sendMessage,用于把文本域messageTextarea的内容发送到服务器端,并在聊天列表中添加本行,在master.js中添加sendMessage,具体代码如下:

[code lang=”java”]
function sendMessage(fromUserName, toUserName) {
if(!toUserName) {
alert("没有聊天对象");
return ;
}

var message = $("#messageTextarea").val();
if(!message) {
alert("信息不能为空");
return ;
}

$.ajax({
type: "POST",
url: "/imlearntest/chat/sendMessage",
data:{fromUserName:fromUserName, toUserName:toUserName, content:message},
success: function(result){
if(result.status == 200) {
$("#messageTextarea").val("");
var trObj = $("<tr></tr>");
trObj.html(‘<td colspan="2" align="right">’+ message +'</td>’);
trObj.prependTo("#chatLogTbody");
}
}
});
}
[/code]

通过ajax发送消息之前,检查是否有接受者,文本域中是否有内容等。
实现连接/chat/sendMessage,把接受到的消息保存到数据库,并通知接受者有新消息,在ChatController中添加sendMessage方法,返回json字符串通知客户端状态,所以基础结果通知类com.sunhaojie.imlearntest.controller.BaseResultVo,具体代码如下:

[code lang=”java”]
@RequestMapping("sendMessage")
@ResponseBody
public BaseResultVo sendMessage(@ModelAttribute("fromUserName") String fromUserName,
@ModelAttribute("toUserName") String toUserName,
@ModelAttribute("content") String content, HttpServletRequest request,
HttpServletResponse response) {
chatBo.sendMessage(fromUserName, toUserName, content);
return BaseResultVo.success;
}
public class BaseResultVo {
private int status;
private String message;

public static BaseResultVo success = new BaseResultVo(200, "成功");

public BaseResultVo(int status, String message) {
this.status = status;
this.message = message;
}
}
[/code]

在聊天业务类com.sunhaojie.imlearntest.bo.ChatBo添加保存聊天内容到数据库,所以需要在ChatRecordBo添加保存聊天信息的addChat方法,具体代码如下:

[code lang=”java”]
@Service
public class ChatBo {
@Autowired
private ChatRecordBo chatRecordBo;
private static Map<String, Integer> chatStatus = new HashMap<String, Integer>();

public void sendMessage(String fromUserName, String toUserName, String content) {
String chatStatusKey = chatStatusKey(fromUserName, toUserName);
synchronized (chatStatus) {
Integer status = chatStatus.get(chatStatusKey);
if (status == null) {
status = 0;
chatStatus.put(chatStatusKey, status);
}
chatRecordBo.addChat(fromUserName, toUserName, content);
chatStatus.put(chatStatusKey, 1);
}
}
private String chatStatusKey(String fromUserName, String toUserName) {
return fromUserName + "@" + toUserName;
}
}
/**
* com.sunhaojie.imlearntest.bo.ChatRecordBo 添加聊天记录
*/
public boolean addChat(String fromUsername, String toUsername, String content) {
ChatRecordPo chatRecordPo = new ChatRecordPo();
chatRecordPo.setFromUserName(fromUsername);
chatRecordPo.setToUserName(toUsername);
chatRecordPo.setContent(content);
int insertRowNum = chatRecordDao.insert(chatRecordPo);
if (insertRowNum != 1) {
return false;
}
return true;
}
[/code]

前面我们已经在dao层添加了insert方法,所以这里可以直接使用,修改接受状态时存在接收方读取的情况,所以为了防止并发,我们添加了synchronized 同步访问chatStatus 属性。
测试发送聊天记录,已经可以发出了,并且数据库中也存在数据,但是在接收方没有及时获取消息内容,所以在建立聊天链接时需要客户端监听对方消息发送功能,这里采用长连接的方式实现。在chat.vm中添加接受消息的函数调用acceptMessage,这个函数会向服务器端发送一个请求,这个请求在收到消息变更或者超时时会返回,js内容如下:

[code lang=”html”]
<!– chat.vm底部添加 –>
#if($!{fromUserName})
<script type="text/javascript">
acceptMessage(‘$!{fromUserName}’, ‘$!{toUserName}’);
</script>
#end
[/code]

[code lang=”javascript”]
//master.js添加监听新消息功能
function acceptMessage(fromUserName, toUserName) {
$.ajax({
type: "POST",
url: "/imlearntest/chat/acceptMessage",
data:{fromUserName:fromUserName, toUserName:toUserName},
success: function(result){
if(result.status == 1) {
chatRecordList(fromUserName, toUserName);
}
acceptMessage(fromUserName, toUserName);
}
});
}
//master.js添加当有新消息时刷新消息列表
function chatRecordList(fromUserName, toUserName) {
$.ajax({
type: "POST",
url: "/imlearntest/chat/chatRecordList",
data:{fromUserName:fromUserName, toUserName:toUserName, page:0, limit:10},
success: function(result){
$("#chatRecordList").html(result);
}
});
}
[/code]

实现链接/chat/acceptMessage的功能,在ChatController添加acceptMessage方法,成功返回status为1 BaseResultVo对象否则status=0,在ChatBo中添加acceptMessage方法循环查看chatStatus中是否有新消息,如果有就返回,否则线程休眠1秒,超过30次循环则返回没有新消息,具体代码如下:

[code lang=”java”]
//ChatController实现新消息监听
@RequestMapping("acceptMessage")
@ResponseBody
public BaseResultVo acceptMessage(@ModelAttribute("fromUserName") String fromUserName,
@ModelAttribute("toUserName") String toUserName, HttpServletRequest request,
HttpServletResponse response) throws IOException {
boolean acceptMessageFlag = chatBo.acceptMessage(fromUserName, toUserName);
BaseResultVo result = null;
if (acceptMessageFlag) {
result = new BaseResultVo(1, "有新消息");
} else {
result = new BaseResultVo(0, "没有新消息");
}

return result;
}
//ChatBo实现新消息监听
public boolean acceptMessage(String fromUserName, String toUserName) {
String chatStatusKey = chatStatusKey(toUserName, fromUserName);
int times = 0;
do {
synchronized (chatStatus) {
Integer status = chatStatus.get(chatStatusKey);
if (status == null) {
status = 0;
chatStatus.put(chatStatusKey, status);
}

if (status == 1) {
chatStatus.put(chatStatusKey, 0);
return true;
}
}

times++;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} while (times < 30);

return false;
}
[/code]

再次测试,成功了,发送消息,接受方能及时收到消息了,把master.vm中聊天记录的测数据清理一下,一个简单的及时通讯web工具就开发完成了。

小练习:在本地完成用户聊天功能