过滤敏感词
1. 原理
使用前缀树实现过滤敏感词算法
- 前缀树
- 名称:Trie、字典树、查找树
- 特点:查找效率高,消耗内存大
- 应用:字符串检索、词频统计、字符串排序等
- 敏感词过滤器
- 定义前缀树
- 根据敏感词,初始化前缀树
- 编写过滤敏感词的方法
2. 代码实现
创建resources目录下的敏感词列表sensitive-words.txt
创建工具类
定义前缀树
// 定义前缀树
private class TrieNode {
// 关键词结束标识 (如果是单词的结尾,就是敏感词)
private boolean isKeywordEnd = false;
// 子节点(key是下级字符,value是下级节点)
private Map<Character, TrieNode> subNodes = new HashMap<>();
public boolean isKeywordEnd() {
return isKeywordEnd;
}
public void setKeywordEnd(boolean keywordEnd) {
isKeywordEnd = keywordEnd;
}
// 添加子节点
public void addSubNode(Character c, TrieNode node) {
subNodes.put(c, node);
}
// 获取子节点
public TrieNode getSuNode(Character c) {
return subNodes.get(c);
}
}
``
### 根据敏感词,初始化前缀树
```java
// 将一个敏感词添加到前缀树中
private void addKeyword(String keyword) {
TrieNode tempNode = rootNode;
for (int i = 0; i < keyword.length(); i++) {
char c = keyword.charAt(i);
TrieNode suNode = tempNode.getSuNode(c);
if (suNode == null) {
// 初始化子节点
suNode = new TrieNode();
tempNode.addSubNode(c, suNode);
}
// 指向子节点,进入下一轮循环
tempNode = suNode;
// 设置结束标识
if(i == keyword.length() - 1) {
tempNode.setKeywordEnd(true);
}
}
}
编写测试类
public class SensitiveTests {
@Autowired
private SensitiveFilter sensitiveFilter;
@Test
public void testSensitiveFilter() {
String text = "这里可以赌博,可以嫖娼,可以吸毒,可以开票,哈哈哈!";
text = sensitiveFilter.filter(text);
System.out.println(text);
text = "这里可以☆赌☆博☆,可以☆嫖☆娼☆,可以☆吸☆毒☆,可以☆开☆票☆,哈哈哈!";
text = sensitiveFilter.filter(text);
System.out.println(text);
}
}
发布帖子
1. 原理
异步请求:当前网页不刷新,向服务器返回结果,这些结果中提炼的数据对网页进行刷新
- AJAX(实现异步请求技术)
- Asynchronous JavaScript and XML
- 异步的JavaScript与XML,不是一门新技术,只是一个新的术语。
- 使用AJAX,网页能够将增量更新呈现在页面上,而不需要刷新整个页面。
- 虽然X代表XML,但目前JSON的使用比XML更加普遍。
- https://developer.mozilla.org/zh-CN/docs/Web/Guide/AJAX
2. 示例
使用jQuery发送AJAX请求。
引入fastjson依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
实现工具类:返回json数据
/**
* 返回json数据
* 传入编码,字符信息,业务数据
*/
public static String getJSONString(int code, String msg, Map<String, Object> map) {
JSONObject json = new JSONObject();
json.put("code", code);
json.put("msg", msg);
// 把map对象中的每个键值对传入json对象中
if(map != null) {
for(String key : map.keySet()) {
json.put(key, map.get(key));
}
}
return json.toJSONString();
}
/**
* 对getJSONString方法进行重载(参数不同)
*/
public static String getJSONString(int code, String msg) {
return getJSONString(code, msg, null);
}
public static String getJSONString(int code) {
return getJSONString(code, null, null);
}
编写main方法进行测试
public static void main(String[] args) {
Map<String, Object> map = new HashMap<>();
map.put("name", "zhangsan");
map.put("age", 25);
System.out.println(getJSONString(0, "ok", map));
}
一个java项目中可以有多个mai方法
java是面向对象的语言,所以java的main函数是写在类里面的,而C/C++等语言是面向过程的语言,main函数是写在类外面的,导致C/C++只能有一个main函数。而java不同,java在运行时会自动寻找main函数,并把找到的第一个main函数作为入口进行执行,而其他的main函数会被当成普通函数来处理。
编写controller方法
@RequestMapping(path = "/ahax", method = RequestMethod.POST)
@ResponseBody
public String textAjax(String name, int age) {
System.out.println(name);
System.out.println(age);
return CommunityUtil.getJSONString(0, "操作成功");
}
在网页编写jquery代码
<script>
function send() {
$.post(
"/community/alpha/ajax",
{"name":"张三","age":23},
function(data) {
// 打印
console.log(typeof(data));
console.log(data);
// 把data转换为json对象
data = $.parseJSON(data);
console.log(typeof(data));
console.log(data.code);
console.log(data.msg);
}
);
}
</script>
3. 实践
采用AJAX请求,实现发布帖子的功能。
创建mapper接口
@Mapper
public interface DiscussPostMapper {
List<DiscussPost> selectDiscussPosts(int userId, int offset, int limit);
// @Param注解用于给参数取别名,
// 如果只有一个参数,并且在<if>里使用,则必须加别名.
int selectDiscussPostRows(@Param("userId") int userId);
int insertDiscussPost(DiscussPost discussPost);
}
实现mapper接口
<!--int insertDiscussPost(DiscussPost discussPost);-->
<insert id="insertDiscussPost" parameterType="DiscussPost">
insert into discuss_post(<include refid="insertFields"></include>)
values(#{userId},#{title},#{content},#{type},#{status},#{createTime},#{commentCount},#{score})
</insert>
开发service层
- addDiscussPost
- 对参数进行判断
- 转义HTML标记:替换标签,省得浏览器误认为元素
- 过滤敏感词
public int addDiscussPost(DiscussPost post) {
// 对参数进行判断
if(post == null) {
throw new IllegalArgumentException("参数不能为空");
}
// 转义HTML标记:替换标签,省得浏览器误认为元素
post.setTitle(HtmlUtils.htmlEscape(post.getTitle()));
post.setContent(HtmlUtils.htmlEscape(post.getContent()));
// 过滤敏感词
post.setTitle(sensitiveFilter.filter(post.getTitle()));
post.setContent(sensitiveFilter.filter(post.getContent()));
return discussPostMapper.insertDiscussPost(post);
}
开发controller层
@RequestMapping(path = "/add", method = RequestMethod.POST)
@ResponseBody
public String addDiscussPost(String title, String content) {
User user = hostHolder.getUser();
if(user == null) {
return CommunityUtil.getJSONString(403, "你还没有登录哦!");
}
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());
post.setUserId(user.getId());
post.setTitle(title);
post.setContent(content);
post.setCreateTime(new Date());
discussPostService.addDiscussPost(post);
// 报错的情况,将来统一处理.
return CommunityUtil.getJSONString(0, "发布成功!");
}
页面
index.js
function publish() {
$("#publishModal").modal("hide");
// 获取标题和内容
var title = $("#recipient-name").val();
var content = $("#message-text").val();
// 发送异步请求(POST)
$.post(
CONTEXT_PATH + "/discuss/add",
{"title":title,"content":content},
function(data) {
data = $.parseJSON(data);
// 在提示框中显示返回消息
$("#hintBody").text(data.msg);
// 显示提示框
$("#hintModal").modal("show");
// 2秒后,自动隐藏提示框
setTimeout(function(){
$("#hintModal").modal("hide");
// 刷新页面
if(data.code == 0) {
window.location.reload();
}
}, 2000);
}
);
}
帖子详情
1. 具体步骤
- DiscussPostMapper
- DiscussPostService
- DiscussPostController
- index.html
- 在帖子标题上增加访问详情页面的链接
- discuss-detail.html
- 处理静态资源的访问路径
- 复用index.html的header区域
- 显示标题、作者、发布时间、帖子正文等内容
2. 代码实现
创建mapper接口
返回的是DiscussPost
// 查询帖子详情
DiscussPost selectDiscussPostById(int id);
实现mapper接口
<!--DiscussPost selectDiscussPostById(int id);-->
<select id="selectDiscussPostById" resultType="DiscussPost">
select <include refid="selectFields"></include>
from discuss_post
where id = #{id}
</select>
开发service层
public DiscussPost findDiscussPostById(int id) {
return discussPostMapper.selectDiscussPostById(id);
}
开发controller层
@RequestMapping(path = "/detail/{discussPostId}", method = RequestMethod.GET)
public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model) {
// 帖子
DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
model.addAttribute("post", post);
// 作者
User user = userService.findUserById(post.getUserId());
model.addAttribute("user", user);
return "/site/discuss-detail";
}
处理html
index.html
<a th:href="@{|/discuss/detail/|${map.post.id}"
事务管理
1. 事务
定义
- 什么是事务
- 事务是由N步数据库操作序列组成的逻辑执行单元,这系列操作要么全执行,要么全放弃执行。
- 事务的特性(ACID)
- 原子性(Atomicity):事务是应用中不可再分的最小执行体。
- 一致性(Consistency):事务执行的结果,须使数据从一个一致性状态,变为另一个一致性状态。
一致性状态:事务需要满足数据库相关的约束 - 隔离性(Isolation):各个事务的执行互不干扰,任何事务的内部操作对其他的事务都是隔离的。
- 持久性(Durability):事务一旦提交,对数据所做的任何改变都要记录到永久存储器中。
隔离性
程序往往是多线程,因此需要隔离性,判断优先级顺序
- 常见的并发异常
- 第一类丢失更新、第二类丢失更新。
- 脏读、不可重复读、幻读。
- 常见的隔离级别(既能保证业务的需要,又保持数据库性能)
- Read Uncommitted:读取未提交的数据。
- Read Committed:读取已提交的数据。
- Repeatable Read:不可重复读。
- Serializable:串行化。(能解决所有的问题,但是需要对数据库进行加锁,下降数据库性能)
第一类丢失更新:某一个事务的回滚,导致另外一个事务已更新的数据丢失了。
第二类丢失更新:某一个事务的提交,导致另外一个事务已更新的数据丢失了。
脏读:某一个事务,读取了另外一个事务未提交的数据。
不可重复读:某一个事务,对同一个数据前后读取的结果不一致。
幻读:某一个事务,对同一个表前后查询到的行数不一致。(查询多行数据导致不一致)
隔离级别
实现机制
- 悲观锁(数据库)
悲观锁看待事务非常悲观,如果并发就一定有问题- 共享锁(S锁)
事务A对某数据加了共享锁后,其他事务只能对该数据加共享锁,但不能加排他锁。 - 排他锁(X锁)
事务A对某数据加了排他锁后,其他事务对该数据既不能加共享锁,也不能加排他锁。
- 共享锁(S锁)
- 乐观锁(自定义)
- 版本号、时间戳等
在更新数据前,检查版本号是否发生变化。若变化则取消本次更新,否则就更新数据(版本号+1)。
- 版本号、时间戳等
2. Spring事务管理
- 声明式事务
- 通过XML配置,声明某方法的事务特征。
- 通过注解,声明某方法的事务特征。
- 编程式事务
- 通过 TransactionTemplate 管理事务,并通过它执行数据库的操作。
一般选择第一种方法,如果业务复杂,又只想管理中间某一部分,就使用第二种方法
- 通过 TransactionTemplate 管理事务,并通过它执行数据库的操作。
3. 代码模拟
声明式事务
@Transactional:选择一个默认的事务管理的方式
- propagation: 事物的传播机制(业务方法a可能会调用业务方法b,这两个方法可能都会加上transcational注解,为了解决以谁为准的问题
- REQUIRED:支持当前事务(外部事务,a调用b,a就是b的外部事务),如果不存在,则创建新事务
- REQUIRES_NEW:创建一个新事务,并且暂停当前事务(外部事务)
- NESTED:如果当前存在事务(外部事务),则嵌套在该事务中执行(独立的提交和回滚),如果外部事务不存在,则和REQUIRED一样
save1:
- 新增用户
- 新增帖子
- 模拟报错回滚
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public Object save1() {
// 新增用户
User user = new User();
user.setUsername("alpha");
user.setSalt(CommunityUtil.generateUUID().substring(0, 5));
user.setPassword(CommunityUtil.md5("123" + user.getSalt()));
user.setEmail("alpha@qq.com");
user.setHeaderUrl("http://image.nowcoder.com/head/99t.png");
user.setCreateTime(new Date());
userMapper.insertUser(user);
// 新增帖子
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());
post.setTitle("Hello");
post.setContent("新人报道!");
post.setCreateTime(new Date());
disscussPostMapper.insertDiscussPost(post);
// 模拟报错:把"abc"字符串转为整数
Integer.valueOf("abc");
return "ok";
}
编程式事务
有些库函数(library function)却要求应用先传给它一个函数,好在合适的时候调用,以完成目标任务。这个被传入的、后又被调用的函数就称为回调函数(callback function)。
@Autowired
private TransactionTemplate transactionTemplate;
public Object save2() {
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
return transactionTemplate.execute(new TransactionCallback<Object>() {
@Override
// 回调方法
public Object doInTransaction(TransactionStatus transactionStatus) {
// 新增用户
User user = new User();
user.setUsername("beta");
user.setSalt(CommunityUtil.generateUUID().substring(0, 5));
user.setPassword(CommunityUtil.md5("123" + user.getSalt()));
user.setEmail("alpha@qq.com");
user.setHeaderUrl("http://image.nowcoder.com/head/99t.png");
user.setCreateTime(new Date());
userMapper.insertUser(user);
// 新增帖子
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());
post.setTitle("你好");
post.setContent("我是新人");
post.setCreateTime(new Date());
disscussPostMapper.insertDiscussPost(post);
// 模拟报错:把"abc"字符串转为整数
Integer.valueOf("abc");
return "ok";
}
});
显示评论
1. 步骤
-
数据层
entity_type: 评论可以评论原贴,也可以评论评论,entity_type只评论对象的类型
entity_id: 评论的对象
target_id : 评论指向的人
status : 状态,删没删除?
- 根据实体查询一页评论数据。
- 根据实体查询评论的数量。
-
业务层
- 处理查询评论的业务。
- 处理查询评论数量的业务。
-
表现层
- 显示帖子详情数据时,同时显示该帖子所有的评论数据。
2. 代码实现
数据层
新建实体类Comment
创建mapper接口
public interface CommentMapper {
/**
* 根据实体查询(是帖子的评论?评论的评论?)
* offset&limint: 为了分页
*/
List<Comment> selectCommentByEntity(int entityType, int entityId, int offset, int limit);
/**
* 查询数据的条目数
*/
int selectCountByEntity(int entityType, int entityId);
}
实现maper接口
- limit N,M : 相当于 limit M offset N , 从第 N 条记录开始, 返回 M 条记录
<sql id="selectFields">
id, user_id, entity_type, entity_id, target_id, content, status, create_time
</sql>
<!--List<Comment> selectCommentByEntity(int entityType, int entityId, int offset, int limit);-->
<select id="selectCommentByEntity" resultType="Comment">
select <include refid="selectFields"></include>
from comment
where status = 0
and entity_type = #{entityType}
and entity_id = #{entityId}
-- 按时间正序排列,先评论的在前面
order by create_time asc
limit #{offset}, #{limit}
</select>
<select id="selectCountByEntity" resultType="int">
select count(id)
from comment
where status = 0
and entity_type = #{entityType}
and entity_id = #{entityId}
</select>
业务层
public class CommentService {
@Autowired
private CommentMapper commentMapper;
/**
* 查询某页数据的集合
*/
public List<Comment> findCommentByEntity(int entityType, int entityId, int offset, int limit) {
return commentMapper.selectCommentByEntity(entityType, entityId, offset, limit);
}
/**
* 查询评论的数量
*/
public int findCommentCount(int entityTpye, int entityId) {
return commentMapper.selectCountByEntity(entityTpye, entityId);
}
}
表现层
controller层
在DiscussPostController上面写,进行补充即可
List<Comment> commentList = commentService.findCommentByEntity(
ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit());
// 评论VO列表:view object
List<Map<String, Object>> commentVoList = new ArrayList<>();
if(commentList != null) {
for (Comment comment : commentList) {
// 一个评论的vo
Map<String, Object> commentVo = new HashMap<>();
// 往vo里添加评论
commentVo.put("comment", comment);
// 往vo里加作者
commentVo.put("user", userService.findUserById(comment.getUserId()));
// 回复列表
List<Comment> replyList = commentService.findCommentByEntity(
ENTITY_TYPE_COMMENT, comment.getId(), 0, Integer.MAX_VALUE);
// 回复的vo列表
List<Map<String, Object>> replyVoList = new ArrayList<>();
if(replyList != null) {
for(Comment reply : replyList) {
Map<String, Object> replyVo = new HashMap<>();
// 回复
replyVo.put("reply", reply);
// 作者
replyVo.put("user", userService.findUserById(reply.getUserId()));
// 回复的目标
User target = reply.getTargetId() == 0 ? null : userService.findUserById(reply.getTargetId());
replyVo.put("target", target);
replyVoList.add(replyVo);
}
}
commentVo.put("replys", replyVoList);
// 回复数量
int replyCount = commentService.findCommentCount(ENTITY_TYPE_COMMENT, comment.getId());
commentVo.put("replyCount", replyCount);
commentVoList.add(commentVo);
}
}
model.addAttribute("comments", commentVoList);
return "/site/discuss-detail";
}
templates
index.html
<span th:text="map.post.commentCount">7</span>
添加评论
1. 步骤
- 数据层
- 增加评论数据。
- 修改帖子的评论数量。
- 业务层
- 处理添加评论的业务:先增加评论、再更新帖子的评论数量。
- 表现层
- 处理添加评论数据的请求。
- 设置添加评论的表单。
2. 代码实现
数据层
增加评论数据
/**
* 增加评论
*/
int insertComment(Comment comment);
<!--int insertComment(Comment comment);-->
<!--新增则需要声明参数类型-->
<select id="insertComment" parameterType="Comment">
insert into comment(<include refid="insertFields"></include>>)
values(#{userId},#{entityType},#{entityId},#{targetId},#{content},#{status},#{createTime})
</select>
修改帖子的评论数量
/**
* 增加更新条数
*/
int updateCommentCount(int id, int commentCount);
<update id="updateCommentCount">
update discuss_post set comment_count = #{commentCount} where id = #{id}
</update>
业务层
public int updateCommentCount(int id, int commentCount) {
return discussPostMapper.updateCommentCount(id, commentCount);
}
处理添加评论的业务:
- 先增加评论
- 再更新帖子的评论数量。
两次事务管理:增加评论、添加评论的数量,希望要么全成功,要么全失败
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public int addComment(Comment comment) {
if(comment == null) {
throw new IllegalArgumentException("参数不能为空!");
}
// 对内容进行过滤:html标签的过滤 + 敏感词过滤
comment.setContent(HtmlUtils.htmlEscape(comment.getContent()));
comment.setContent(sensitiveFilter.filter(comment.getContent()));
int rows = commentMapper.insertComment(comment);
// 更新帖子评论数量
if(comment.getEntityType() == ENTITY_TYPE_POST) {
int count = commentMapper.selectCountByEntity(comment.getEntityType(), comment.getEntityId());
discussPostService.updateCommentCount(comment.getEntityId(), count);
}
return rows;
}
表现层
controller层
- 处理添加评论数据的请求。
- 设置添加评论的表单。
// 回帖之后是重定向到原来的帖子那里,通过路径传过来
@RequestMapping(path = "/add/{discussPostId}", method = RequestMethod.POST)
public String addComment(@PathVariable("discussPostId") int discussPostId, Comment comment) {
comment.setUserId(hostHolder.getUser().getId());
comment.setStatus(0); // 表示有效
comment.setCreateTime(new Date());
commentService.addComment(comment);
// 重定向到帖子详情页
return "redirect:/discuss/detail/" + discussPostId;
}
html页面
- 给帖子进行回帖
- 给评论进行评论
- 给楼中楼进行回复
私信列表
1. 步骤
from_id :消息的发送方 1-系统通知(不是私信)
to_id :消息的接收方
status : 0-未读 1-已读 2-删除
conversation_id : 会话id 111_112(111和112的会话,小在前,大在后,不区分方向)
- 私信列表
- 查询当前用户的会话列表(我和某个人的多次对话,称为一次会话),每个会话只显示一条最新的私信。
- 支持分页显示。
- 私信详情
- 查询某个会话所包含的私信。
- 支持分页显示。
2. 代码实现
数据层
构造实体类Message
创建mapper接口``
@Mapper
public interface MessageMapper {
// 查询当前用户的会话列表,针对每个会话只返回一条最新的私信
List<Message> selectConversations(int userId, int offset, int limit);
// 查询当前用户的会话数量
int selectConversationCount(int userId);
// 查询某个会话所包含的私信列表
List<Message> selectLetters(String conversationId, int offset, int limit);
// 查询某个会话所包含的私信数量
int selectLetterCount(String conversationId);
// 查询未读的私信数量
// 把String conversationId作为动态的参数去用,传了就用,不传就不用,这样可以实现两种方式的业务
int selectLetterUnreadCount(int userId, String conversationId);
实现mapper接口(有点复杂)
<sql id="selectFields">
id, from_id, to_id, conversation_id, content, status, create_time
</sql>
<!--List<Message> selectConversations(int userId, int offset, int limit);-->
<select id="selectConversations" resultType="Message">
select <include refid="selectFields"></include>
from message
where id in (
-- 得到每条会话最新的id,所以用max
-- 查询当前用户的会话列表,针对每个会话只返回一条最新的私信
SELECT MAX(id) FROM message
WHERE status != 2
AND from_id != 1
AND (from_id = #{userId} or to_id = #{userId})
GROUP BY conversation_id
)
order by id desc
limit #{offset}, #{limit}
</select>
<!--int selectConversationCount(int userId);-->
<select id="selectConversationCount" resultType="int">
select count(m.maxid) from (
SELECT MAX(id) as maxid FROM message
WHERE status != 2
AND from_id != 1
AND (from_id = #{userId} or to_id = #{userId})
GROUP BY conversation_id
) as m
</select>
<!--List<Message> selectLetters(String conversationId, int offset, int limit);-->
<!--查询某个会话所包含的私信列表-->
<select id="selectLetters" resultType="Message">
select <include refid="selectFields"></include>
from message
where status != 2
and from_id != 1
and conversation_id = #{conversationId}
order by id desc
limit #{offset}, #{limit}
</select>
<!--int selectLetterCount(String conversationId);-->
<!--查询某个会话所包含的私信数量-->
<select id="selectLetterCount" resultType="int">
select count(id)
from message
where status != 2
and from_id != 1
and conversation_id = #{conversationId}
</select>
<!--int selectLetterUnreadCount(int userId, String conversationId);-->
<!--未读一定是当前用户发消息给我的,一定是to_id-->
<select id="selectLetterUnreadCount" resultType="int">
select count(id)
from message
where status = 0
and from_id != 1
and to_id = #{userId}
<if test="conversationId!=null">
and conversation_id = #{conversationId}
</if>
</select>
创建测试类
@Test
public void testSelectLetters() {
List<Message> list = messageMapper.selectConversations(111, 0, 20);
for(Message message : list) {
System.out.println(message);
}
int count = messageMapper.selectConversationCount(111);
System.out.println(count);
List<Message> list1 = messageMapper.selectLetters("111_112", 0, 10);
for(Message message : list1) {
System.out.println(message);
}
count = messageMapper.selectLetterCount("111_112");
System.out.println(count);
count = messageMapper.selectLetterUnreadCount(131, "111_131");
System.out.println(count);
}
业务层
@Service
public class MessageService {
@Autowired
private MessageMapper messageMapper;
public List<Message> findConversations(int userId, int offset, int limit) {
return messageMapper.selectConversations(userId, offset, limit);
}
public int findConversationCount(int userId) {
return messageMapper.selectConversationCount(userId);
}
public List<Message> findLetters(String conversationId, int offset, int limit) {
return messageMapper.selectLetters(conversationId, offset, limit);
}
public int findLetterCount(String conversationId) {
return messageMapper.selectLetterCount(conversationId);
}
public int findLetterUnreadCount(int userId, String conversationId) {
return messageMapper.selectLetterUnreadCount(userId, conversationId);
}
}
表现层
controller层
- 设置分页信息
- 添加会话列表
- 查询未读消息数量
@RequestMapping(path = "/letter/list", method = RequestMethod.GET)
public String getLetterList(Model model, Page page) {
User user = hostHolder.getUser();
// 设置分页信息
page.setLimit(5);
page.setPath("/letter/list");
page.setRows(messageService.findConversationCount(user.getId()));
// 会话列表
List<Message> conversationList = messageService.findConversations(
user.getId(), page.getOffset(), page.getLimit());
List<Map<String, Object>> conversations = new ArrayList<>();
if(conversationList != null) {
for(Message message : conversationList) {
Map<String, Object> map = new HashMap<>();
map.put("conversation", message);
map.put("letterCount", messageService.findLetterCount(message.getConversationId()))
map.put("unreadCount", messageService.findLetterUnreadCount(user.getId(), message.getConversationId()));
// 如果当前用户是对话的发起者,那目标id就是接受人
int targetId = user.getId() == message.getFromId() ? message.getToId() : message.getFromId();
map.put("target", userService.findUserById(targetId));
conversations.add(map);
}
}
model.addAttribute("conversations", conversations);
// 查询未读消息数量
int letterUnreadCount = messageService.findLetterUnreadCount(user.getId(), null);
model.addAttribute("letterUnreadCount", letterUnreadCount);
return "/site/letter";
}
- 私信详情
- 查询某个会话所包含的私信。
- 支持分页显示。
@RequestMapping(path = "/letter/detail/{conversationId}", method = RequestMethod.GET)
public String getLetterDetail(@PathVariable("conversationId") String conversationId, Page page, Model model) {
// 分页信息
page.setLimit(5);
page.setPath("/letter/detail/" + conversationId);
page.setRows(messageService.findLetterCount(conversationId));
// 私信列表
List<Message> letterList = messageService.findLetters(conversationId, page.getOffset(), page.getLimit());
List<Map<String, Object>> letters = new ArrayList<>();
if(letterList != null) {
for(Message message : letterList) {
Map<String, Object> map = new HashMap<>();
map.put("letter", message);
map.put("fromUser", userService.findUserById(message.getFromId()));
letters.add(map);
}
}
model.addAttribute("letters", letters);
// 查询私信目标,发送给模板
model.addAttribute("target", getLetterTarget((conversationId)));
return "/site/letter-detail";
}
/**
* conversationId由两段构成,哪段和用户id不一样哪一段就是目标id
*/
private User getLetterTarget(String conversationId) {
String[] ids = conversationId.split("_");
int id0 = Integer.parseInt(ids[0]);
int id1 = Integer.parseInt(ids[1]);
if(hostHolder.getUser().getId() == id0) {
return userService.findUserById(id1);
} else {
return userService.findUserById(id1);
}
}
发送私信
1. 步骤
- 发送私信
- 采用异步的方式发送私信,发送私信时要自动填写发私信人的名字
- 系统判断成功还是失败,发送通知
- 发送成功后刷新私信列表。
- 设置已读
- 访问私信详情时,将显示的私信设置为已读状态。
2. 代码实现
数据层
创建mapper接口
// 增加一个消息
int insertMessage(Message message);
// 把未读消息设为已读,把status从0改到1。设置删除
int updateStatus(List<Integer> ids, int status);
实现mapper接口
<!--// 增加一个消息-->
<!--int insertMessage(Message message);-->
<insert id="insertMessage" parameterType="Message" keyProperty="id">
insert into message(<include refid="insertFields"></include>)
values (#{fromId},#{toId},#{conversationId},#{content},#{status},#{createTime})
</insert>
<!--// 把未读消息设为已读,把status从0改到1。设置删除-->
<!--int updateStatus(List<Integer> ids, int status);-->
<update id="updateStatus">
update message set status = #{status}
where id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
业务层
- 增加message的方法
- 改变私信状态的方法
<!--// 增加一个消息-->
<!--int insertMessage(Message message);-->
<insert id="insertMessage" parameterType="Message" keyProperty="id">
insert into message(<include refid="insertFields"></include>)
values (#{fromId},#{toId},#{conversationId},#{content},#{status},#{createTime})
</insert>
<!--// 把未读消息设为已读,把status从0改到1。设置删除-->
<!--int updateStatus(List<Integer> ids, int status);-->
<update id="updateStatus">
update message set status = #{status}
where id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
表现层
@RequestMapping(path = "/letter/send", method = RequestMethod.POST)
@ResponseBody
public String sendLetter(String toName, String content) {
// 构造接受者的数据
User target = userService.findUserByName(toName);
if(target == null) {
return CommunityUtil.getJSONString(1, "目标用户不存在");
}
Message message = new Message();
message.setFromId(hostHolder.getUser().getId());
message.setToId(target.getId());
// 因为ConversationId统一是小的在前大的在后
if(message.getFromId() < message.getToId()) {
message.setConversationId(message.getFromId() + "_" + message.getToId());
} else {
message.setConversationId(message.getToId() + "_" + message.getFromId());
}
message.setContent(content);
message.setCreateTime(new Date());
messageService.addMessage(message);
return CommunityUtil.getJSONString(0);
}
- 设置已读
- 访问私信详情时,将显示的私信设置为已读状态。
/**
* 得到集合中未读消息的id
*/
private List<Integer> getLetterIds(List<Message> letterList) {
List<Integer> ids = new ArrayList<>();
if(letterList != null) {
for (Message message : letterList) {
// 如果我是接受者身份 而且消息确实是未读状态
if(hostHolder.getUser().getId() == message.getToId()) {
ids.add(message.getId());
}
}
}
return ids;
}
统一异常处理
1. 原理
数据层出现异常抛到业务层,业务层出现异常抛到表现层,因此统一异常处理是针对表现层而言的
- @ControllerAdvice
- 用于修饰类,表示该类是Controller的全局配置类。
- 在此类中,可以对Controller进行如下三种全局配置:异常处理方案、绑定数据方案、绑定参数方案。
- @ExceptionHandler
- 用于修饰方法,该方法会在Controller出现异常后被调用,用于处理捕获到的异常。
- @ModelAttribute
- 用于修饰方法,该方法会在Controller方法执行前被调用,用于为Model对象绑定参数。
- @DataBinder
- 用于修饰方法,该方法会在Controller方法执行前被调用,用于绑定参数的转换器。
springMVC自内置很多底层转换器
- 用于修饰方法,该方法会在Controller方法执行前被调用,用于绑定参数的转换器。
2. 实现
- 错误页面摆放位置
- 配置错误页面
- 新建advice.ExceptionAdvice
- 使用@ControllerAdvice注解
// ControllerAdvice 只去扫描带有controller的bean
@ControllerAdvice(annotations = Controller.class)
public class ExceptionAdvice {
private static final Logger logger = LoggerFactory.getLogger(ExceptionAdvice.class);
// Exception是所有异常的父类,则表示所有的方法都用ExceptionHandler处理
@ExceptionHandler({Exception.class})
public void handleException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException {
logger.error("服务器发生异常:" + e.getMessage());
// 每个element记录一条异常的信息
for(StackTraceElement element: e.getStackTrace()) {
logger.error(element.toString());
}
String xRequestWith = request.getHeader("x-requested-with");
// 如果等于这个值,就是异步请求,普通请求期待返回html
if("XMLHttpRequest".equals(xRequestWith)) {
// response.setContentType(MIME)的作用是使客户端浏览器,区分不同种类的数据,
// 并根据不同的MIME调用浏览器内不同的程序嵌入模块来处理相应的数据。
response.setContentType("application/plain;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(CommunityUtil.getJSONString(1, "服务器异常!"));
} else {
response.sendRedirect(request.getContextPath() + "/error");
}
}
}
统一记录日志
1. 需求
纪录日志需要记录异常情况和不异常情况,所以不能用统一异常处理
拦截器是针对控制层而言的,记录日志是可能针对业务层组件或者数据访问层而言的,所以也不能用拦截器
- 帖子模块
- 评论模块
- 消息模块
2. AOP
概念
- Aspect Oriented Programing,即面向方面(切面)编程。
- AOP是一种编程思想,是对OOP(面向对象)的补充,可以进一步提高编程的效率。
oop思想:封装bean去调用
术语
- target:已程序中已经开发好的bean
- JoinPoint(连接点):能够被织入代码的位置(属性、构造器、静态块、成员方法)
- aspect(切面): 另外一个额外的bean。封装业务需求的组件就是aspect
整个编程过程就是面向aspect编程- Pointcut:声明切点,声明到底织入哪些对象的哪些位置
- advice:实现具体的系统逻辑,具体要做什么?位置在哪里?
- 利用框架提供置入功能(weaving),把aspect织入到不同的组件中
- 编译时:织入方式越原始,速度越快,但是可能有些特殊情况下处理得不精细
- 运行时:效率低
实现
- AspectJ
- AspectJ是语言级的实现,它扩展了Java语言,定义了AOP语法。
- AspectJ在编译期织入代码,它有一个专门的编译器,用来生成遵守Java字节码规范的class文件。
支持所有的连接点
- Spring AOP
- Spring AOP使用纯Java实现,它不需要专门的编译过程,也不需要特殊的类装载器。
- Spring AOP在运行时通过代理的方式织入代码,只支持方法类型的连接点(只能将方面组件的代码织入到方法中)。
- Spring支持对AspectJ的集成。
Spring AOP
- JDK动态代理
代理:对某一个对象生成代理对象,调用的时候织入代理对象而不是原始对象- Java提供的动态代理技术,可以在运行时创建接口的代理实例。
- Spring AOP默认采用此种方式,在接口的代理实例中织入代码。
- CGLib动态代理
- 采用底层的字节码技术,在运行时创建子类代理实例。
- 当目标对象不存在接口时,Spring AOP会采用此种方式,在子类实例中织入代码。
3. 代码实现
demo
@Component
@Aspect
public class AlphaAspect {
/**
* 定义切点
* 业务组件下所有的类 下面的所有的方法 (。。)表示所有的参数和返回值
*/
@Pointcut("execution(* com.nowcoder.community.service.*.*(..))")
public void pointcut() {
}
/**
* 定义通知
* 在连接点开头切入
*/
@Before("pointcut()")
public void before() {
System.out.println("before");
}
/**
* 在连接点之后切入
*/
@After("pointcut()")
public void after() {
System.out.println("after");
}
/**
* 在返回值以后处理
*/
@AfterReturning("pointcut()")
public void afterReturning() {
System.out.println("afterReturning");
}
/**
* 在抛异常后织入
*/
@AfterThrowing("pointcut()")
public void afterThrowing() {
System.out.println("afterThrowing");
}
/**
* 在前后都织入逻辑
* 参数ProceedingJoinPoint就是连接点-目标织入的部位
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("around before");
// 利用oinPoint.proceed()调用目标组件的方法
Object obj = joinPoint.proceed();
System.out.println("around after");
return obj;
}
}
实现记录日志功能
在业务组件一开始记录日志
用户xx 在xx时刻 访问xx功能
@Component
@Aspect
public class ServiceLogAspect {
private static final Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class);
@Pointcut("execution(* com.nowcoder.community.service.*.*(..))")
public void pointcut() {
}
/**
* 在方法前记录日志
* @param joinPoint 连接点指的是方法织入的目标
*/
@Before("pointcut()")
public void before(JoinPoint joinPoint) {
// 用户[1.2.3.4],在[xxx],访问了[com.newcoder.community.service.xxx()].
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// ip
String ip = request.getRemoteHost();
// 日期
String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
// 方法名
// joinPoint.getSignature().getDeclaringTypeName() 获得的是类名,还要在后面拼上方法名
String target = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
logger.info(String.format("用户[%s],在[%s],访问了[%s].", ip, now, target));
}
}