过滤敏感词
-
前缀树
- 名称:Trie、字典树、查找树
- 特点:查找效率高,消耗内存大
- 应用:字符串检索、词频统计、字符串排序等
-
敏感词过滤器
- 定义前缀树
- 根据敏感词,初始化前缀树
- 编写过滤敏感词的方法
敏感词算法:
流程
一个 trie 树存放敏感词,一对快慢指针,快指针指向给给定的字符串的起始位置,如果检测到了当前字符符合某个过滤词的第一个字母,就让慢指针开始往后匹配这个过滤词的其他字符,如果没有完全匹配(这里指不是过滤词或者只是过滤词的部分,并没有完全精确匹配),则让快慢指针都跳到快指针所指向的后一个字母的位置,继续按照此逻辑执行。匹配到了的话快慢指针都往前移一位,直到完全匹配就修改敏感词词汇。
遇到特殊符号的情况,快指针不动,慢指针动。
算法实现
在 Util
包下创建 SensitiveFilter
类:
package com.nowcoder.community.util;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.CharUtils;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Component
public class SensitiveFilter {
//替换符
public static final String REPLACEMENT="***";
//根节点
private TrieNode rootNode=new TrieNode();
//在实例化bean后,服务器初始化之前调用
@PostConstruct
public void init(){
try(
InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
BufferedReader reader=new BufferedReader(new InputStreamReader(is));
){
String keyword;
while((keyword=reader.readLine())!=null){
//将敏感词添加到前缀树
this.addKeyword(keyword);
}
} catch (Exception e) {
log.error("加载敏感词文件失败:"+e.getMessage());
}
}
//将一个敏感词添加到前缀树中
private void addKeyword(String keyword){
TrieNode tempNode=rootNode;
for(int i=0;i<keyword.length();i++){
char c=keyword.charAt(i);
TrieNode subNode=tempNode.getSubNode(c);
if(subNode==null){
//初始化子节点
subNode=new TrieNode();
tempNode.addSubNode(c,subNode);
}
//存在的话,指向子节点,进入下一轮循环
tempNode=subNode;
//设置结束标记
if(i==keyword.length()-1){
tempNode.setKeywordEnd(true);
}
}
}
/**
* 过滤敏感词
* @param text 待过滤的文本
* @return
*/
public String filter(String text){
if(text.isBlank()){
return null;
}
//指针1
TrieNode tempNode=rootNode;
//指针2
int begin=0;
//指针3
int position=0;
//结果
StringBuilder sb=new StringBuilder();
while(begin<text.length()){
if(position<text.length()){
char c=text.charAt(position);
//跳过符号
if(isSymbol(c)){
//若指针1处于根节点,将此符号计入结果,让指针2走向下一步
if(tempNode == rootNode){
sb.append(c);
begin++;
}
//无论符号在开头或者中间,指针3都向下走一步
position++;
continue;
}
//检查下级节点
tempNode=tempNode.getSubNode(c);
if(tempNode==null){//没有下个节点,说明是最后节点,而且之前的词汇都不是敏感词
//以begin开头的字符串不是敏感词
sb.append(text.charAt(begin));
//进入下一个位置的过滤词的判断,这一波的过滤词已经结束了,开始新的一轮
position = ++begin;
//重新指向根节点
tempNode=rootNode;
}else if(tempNode.isKeywordEnd()){
//发现了并完全匹配敏感词,将begin-position字符串替换掉
sb.append(REPLACEMENT);
//进入下一个位置的过滤词的判断,这一波的过滤词已经结束了,开始新的一轮,从这个过滤词的结束位置+1开始算
begin = ++position;
//重新指向根节点
tempNode=rootNode;
}else{
//检查下一个字符
position++;
}
}else{
//position遍历越界仍未匹配到敏感词,就将当前字符加入结果,开始下一个字符的过滤词筛查
sb.append(text.charAt(begin));
position=++begin;
tempNode=rootNode;
}
}
return sb.toString();
}
//判断是否为符号
private boolean isSymbol(Character c){
//0x2E80~0x9FFF是东亚文字范围,isAsciiAlphanumeric判断是不是普通字符
return !CharUtils.isAsciiAlphanumeric(c) && (c<0x2E80||c>0x9FFF);
}
//前缀树
private class TrieNode{
//关键词标识
private boolean isKeywordEnd = false;
//子节点(key是下级字符,value是下级节点)
private Map<Character,TrieNode> subNodes=new HashMap<>();
private boolean isKeywordEnd(){
return isKeywordEnd;
}
public void setKeywordEnd(boolean keywordEnd){
isKeywordEnd=keywordEnd;
}
//添加子节点
public void addSubNode(Character c,TrieNode node){
subNodes.put(c,node);
}
//获取子节点
public TrieNode getSubNode(Character c){
return subNodes.get(c);
}
}
}
-
首先判断待过滤的文本是否为空或空格,如果是,则返回 null。
-
创建三个指针:tempNode 指向根节点,begin 指向待过滤文本的开头位置,position 指向当前遍历的位置。
-
创建一个 StringBuilder 对象用于存储过滤后的文本结果。
-
进入循环,遍历文本的每个字符。
-
如果字符是符号,则判断 tempNode 是否为根节点,如果是,则将符号加入结果,并将 begin 指针向后移动一位(如 ♥ 赌 ♥♥♥ 博,这种情况我们只让第一个 ♥ 加入结果,赌字之后的 ♥ 就直接 pass,不加入结果);无论是否为根节点,都将 position 指针向后移动一位。
-
如果字符不是符号,则检查 tempNode 的下一个节点是否存在。
-
如果不存在,说明之前的词汇都不是敏感词,将以 begin 开头的字符串加入结果,并将 begin 和 position 指针都移动到下一个位置,重新指向根节点。
-
如果存在,继续判断是否为敏感词的结束节点。
- 如果是结束节点,说明发现了一个完全匹配的敏感词,将敏感词替换成指定的替换字符,并将 begin 指针移动到 position 的下一个位置,重新指向根节点。
- 如果不是结束节点,继续检查下一个字符。
-
-
循环结束后,将最后一批字符加入结果。
-
返回过滤后的文本结果。
ps:记得从 3.1
下面复制 sensitive-words.txt
到项目下。
写个测试类测试结果
package com.nowcoder.community;
import com.nowcoder.community.util.SensitiveFilter;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes=CommunityApplication.class)
public class SensitiveTests {
@Autowired
private SensitiveFilter sensitiveFilter;
@Test
public void testSensitiveFilter(){
String text="这里可以赌博,♥赌♥♥♥博可以嫖娼,可以吸毒,可以开票,赌博哈哈哈赌博吸毒嫖娼";
text=sensitiveFilter.filter(text);
System.out.println(text);
}
}
发布帖子
-
AJAX
- Asynchronous JavaScript and XML
- 异步的 JavaScript 与 XML,不是一门新技术,只是一个新的术语。
- 使用 AJAX,网页能够将增量更新呈现在页面上,而不需要刷新整个页面。
- 虽然 X 代表 XML,但目前 JSON 的使用比 XML 更加普遍。
- https://developer.mozilla.org/zh-CN/docs/Web/Guide/AJAX
-
示例
- 使用 jQuery 发送 AJAX 请求。
-
实践
- 采用 AJAX 请求,实现发布帖子的功能。
示例——使用 jQuery 发送 AJAX 请求
AlphaController
:
//ajax实例
@RequestMapping(path = "/ajax",method = RequestMethod.POST)
@ResponseBody
public String testAjax(String name,int age){
System.out.println(name);
System.out.println(age);
return CommunityUtil.getJSONString(0,"操作成功");
}
在 static/html
下创建 ajax-demo.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Ajax</title>
</head>
<body>
<p>
<input type="button" value="发送" onclick="send();">
</p>
<script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script>
<script>
function send(){
$.post(
"/community/alpha/ajax",
{"name":"张三","age":23},
function (data){
console.log(typeof(data));
console.log(data);
data=$.parseJSON(data);
console.log(typeof(data));
console.log(data.code);
console.log(data.msg);
}
);
}
</script>
</body>
</html>
在网页测试:http://localhost:8080/community/html/ajax-demo.html
发布帖子
在 DiscussPostMapper
下新增一个方法 int insertDiscussPost(DiscussPost discussPost);
在 discusspost_mapper.xml
下:
<sql id="insertFields">
user_id, title, content, type, status, create_time, comment_count, score
</sql>
<insert id="insertDiscussPost" parameterType="DiscussPost">
insert into discuss_Post(<include refid="insertFields"></include>)
values(#(userId),#(title),#(content),#(type),#(status),#(createTime),#(commentCount),#(score))
</insert>
在DiscussPostService
:
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
下新建 DiscussPostController
:
package com.nowcoder.community.controller;
import com.nowcoder.community.entity.DiscussPost;
import com.nowcoder.community.service.DiscussPostService;
import com.nowcoder.community.util.CommunityUtil;
import com.nowcoder.community.util.HostHolder;
import com.nowcoder.community.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Date;
@Controller
@RequestMapping("/discuss")
public class DiscussPostController {
@Autowired
private DiscussPostService discussPostService;
@Autowired
private HostHolder hostHolder;
@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.setTitle(title);
post.setContent(content);
post.setCreateTime(new Date());
discussPostService.addDiscussPost(post);
//报错的情况,将来统一处理
return CommunityUtil.getJSONString(0,"发布成功!");
}
}
修改 index.js
:
$(function(){
$("#publishBtn").click(publish);
});
function publish() {
$("#publishModal").modal("hide");
//获取标题和内容
var title=$("#recipient-name").val();
var content=$("#message-text").val();
//发送异步请求
$.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)
}
);
}
修改 index.html
的内容,只在登录情况下,显示 我要发布
按钮:
<button type="button" class="btn btn-primary btn-sm position-absolute rt-0" data-toggle="modal" data-target="#publishModal" th:if="${loginUser!=null}">我要发布</button>
帖子详情
在 DiscussPostMapper
下新增一个方法 DiscussPost selectDiscussPostById(int id);
。
在discusspost_mapper.xml
:
<select id="selectDiscussPostById" resultType="DiscussPost">
select <include refid="selectFields"></include>
from discuss_post
where id = #{id}
</select>
DiscussPostController
:
@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";
}
修改 index.html
,帖子详情链接跳转:<a th:href="@{|/discuss/detail/${map.post.id}|}" th:utext="${map.post.title}" >备战春招,面试刷题跟他复习,一个月全搞定!
处理 discuss-detail.html
:
- 使用模板引擎,修改相对路径,复用头部。
- 修改帖子标题:
<span th:utext="${post.title}">备战春招,面试刷题跟他复习,一个月全搞定!
- 修改头像:
<img th:src="${user.headerUrl}" class="align-self-start mr-4 rounded-circle user-header" alt="用户头像" >
- 修改用户名:
<div class="mt-0 text-warning" th:utext="${user.username}">寒江雪
- 修改发布时间:
发布于 <b th:text="${#dates.format(post.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-15 15:32:18
- 修改帖子正文:
<div class="mt-4 mb-3 content" th:utext="${post.content}">
事务管理
回顾
-
什么是事务
事务是由 N 步数据库操作序列组成的逻辑执行单元,这系列操作要么全执行,要么全放弃执行。
-
事务的特性(ACID)
- 原子性(Atomicity):事务是应用中不可再分的最小执行体。
- 一致性(Consistency):事务执行的结果,须使数据从一个一致性状态,变为另一个一致性状态。
- 隔离性(Isolation):各个事务的执行互不干扰,任何事务的内部操作对其他的事务都是隔离的。
- 持久性(Durability):事务一旦提交,对数据所做的任何改变都要记录到永久存储器中。
-
事务的隔离性
-
常见的并发异常
- 第一类丢失更新、第二类丢失更新。
- 脏读、不可重复读、幻读。
-
常见的隔离级别
- Read Uncommitted:读取未提交的数据。
- Read Committed:读取已提交的数据。
- Repeatable Read:可重复读。
- Serializable:串行化。
-
-
丢失更新(重要):
-
-
-
-
-
-
事务隔离级别:
- (基本选第 2、3 种级别)
实现机制
-
悲观锁(数据库)
- 共享锁(S 锁) 事务 A 对某数据加了共享锁后,其他事务只能对该数据加共享锁,但不能加排他锁。
- 排他锁(X 锁) 事务 A 对某数据加了排他锁后,其他事务对该数据既不能加共享锁,也不能加排他锁。
-
乐观锁(自定义)
- 版本号、时间戳等 在更新数据前,检查版本号是否发生变化。若变化则取消本次更新,否则就更新数据(版本号 +1)。
Spring 事务管理
-
声明式事务
- 通过 XML 配置,声明某方法的事务特征。
- 通过注解,声明某方法的事务特征。
-
编程式事务
- 通过 TransactionTemplate 管理事务, 并通过它执行数据库的操作。
示例
声明式事务
在 AlphaService
:
//REQUIRED:支持当前事务(外部事务,如果不存在则创建新事务)
//REQUIRES_NEW:创建一个新事务,并且暂停当前事务(外部事务)
//NESTED:如果当前存在事务(外部事务),则嵌套在该事务中执行)
@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());
discussPostMapper.insertDiscussPost(post);
Integer.valueOf("abc");
return "ok";
}
创建 TransactionTests
:
package com.nowcoder.community;
import com.nowcoder.community.service.AlphaService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes=CommunityApplication.class)
public class TransacationTests {
@Autowired
private AlphaService alphaService;
@Test
public void testSave1(){
Object obj=alphaService.save1();
System.out.println(obj);
}
}
编程式事务
AlphaService
,注入 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 status) {
//新增用户
User user=new User();
user.setUsername("beta");
user.setSalt(CommunityUtil.generateUUID().substring(0,5));
user.setPassword(CommunityUtil.md5("123"+user.getSalt()));
user.setEmail("beta@qq.com");
user.setHeaderUrl("http://image.nowcoder.com/head/999t.png");
user.setCreateTime(new Date());
userMapper.insertUser(user);
//新增帖子
DiscussPost post=new DiscussPost();
post.setUserId(user.getId());
post.setTitle("你好");
post.setContent("我是新人!");
post.setCreateTime(new Date());
discussPostMapper.insertDiscussPost(post);
Integer.valueOf("abc");
return "ok";
}
});
}
测试:
public void testSave2(){
Object obj=alphaService.save2();
System.out.println(obj);
}
显示评论
创建相关结构
创建 Comment
类:
package com.nowcoder.community.entity;
import java.util.Date;
public class Comment {
private int id;
private int userId;
private int entityType;
private int entityId;
private int targetId;
private String content;
private int status;
private Date createTime;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public int getEntityType() {
return entityType;
}
public void setEntityType(int entityType) {
this.entityType = entityType;
}
public int getEntityId() {
return entityId;
}
public void setEntityId(int entityId) {
this.entityId = entityId;
}
public int getTargetId() {
return targetId;
}
public void setTargetId(int targetId) {
this.targetId = targetId;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
@Override
public String toString() {
return "Comment{" +
"id=" + id +
", userId=" + userId +
", entityType=" + entityType +
", entityId=" + entityId +
", targetId=" + targetId +
", content='" + content + '\'' +
", status=" + status +
", createTime=" + createTime +
'}';
}
}
创建 CommentMapper
:
package com.nowcoder.community.dao;
import com.nowcoder.community.entity.Comment;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface CommentMapper {
List<Comment> selectCommentByEntity(int entityType,int entityId,int offset,int limit);
int selectCountByEntity(int entityType,int entityId);
}
创建 comment-mapper.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.nowcoder.community.dao.CommentMapper">
<sql id="selectFields">
id,user_id,entity_type,entity_id,target_id,content,status,create_time
</sql>
<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>
</mapper>
业务实现
创建 CommentService
:
package com.nowcoder.community.service;
import com.nowcoder.community.dao.CommentMapper;
import com.nowcoder.community.entity.Comment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CommentService {
@Autowired
private CommentMapper commentMapper;
//根据帖子类型查询帖子
public List<Comment> findCommentsByEntity(int entityType, int entityId, int offset, int limit){
return commentMapper.selectCommentByEntity(entityType,entityId,offset,limit);
}
//查询帖子总数
public int findCommentCount(int entityType,int entityId){
return commentMapper.selectCountByEntity(entityType,entityId);
}
}
定义一个常量:
//实体类型:帖子
int ENTITY_TYPE_POST=1;
//实体类型:评论
int ENTITY_TYPE_COMMENT=2;
DiscussPostController
:
/**
* 帖子详情+评论分页
* @param discussPostId
* @param model
* @return
*/
@RequestMapping(path = "/detail/{discussPostId}",method = RequestMethod.GET)
public String getDiscussPost(@PathVariable("discussPostId")int discussPostId, Model model, Page page){
//帖子
DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
model.addAttribute("post",post);
//作者
User user = userService.findUserById(post.getUserId());
model.addAttribute("user",user);
//评论分页信息
page.setLimit(5);
page.setPath("/discuss/detail/"+discussPostId);
page.setRows(post.getCommentCount());
//评论:给帖子的评论
//回复:给评论的评论
//评论列表
List<Comment> commentList = commentService.findCommentsByEntity(ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit());
List<Map<String,Object>> commentVoList=new ArrayList<>();
//评论Vo列表
if(commentList!=null){
for(Comment comment:commentList){
//评论VO
Map<String,Object> commentVo=new HashMap<>();
//评论
commentVo.put("comment",comment);
//作者
commentVo.put("user",userService.findUserById(comment.getUserId()));
//回复列表,不分页显示,有多少显示多少
List<Comment> replyList = commentService.findCommentsByEntity(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);
}
}
//将回复放入评论VO
commentVo.put("replys",replyVoList);
//回复数量
int replyCount = commentService.findCommentCount(ENTITY_TYPE_COMMENT, comment.getId());
commentVo.put("replyCount",replyCount);
//将每一条评论VO装到评论区里
commentVoList.add(commentVo);
}
}
model.addAttribute("comments",commentVoList);
return "/site/discuss-detail";
}
修改网页
index.html
:
- 修改回帖数量:
<li class="d-inline ml-2">回帖 <span data-type="text">7</span>
- 给分页取名:
<nav class="mt-5" th:if="${page.rows>0}" th:fragment="pagination">
discuss-detail.html
:
-
删除第 2 条回帖。
-
修改回帖数据:
<h6><b class="square"></b> <i th:text="${post.commentCount}">30</i>条回帖
-
修改回复数据的遍历:
<li class="media pb-3 pt-3 mb-3 border-bottom" th:each="cvo:${comments}">
-
修改评论用户头像:
<img th:src="${cvo.user.headerUrl}" class="align-self-start mr-4 rounded-circle user-header" alt="用户头像" >
-
修改评论用户名:
<span class="font-size-12 text-success" th:utext="${cvo.user.username}">掉脑袋切切
-
显示层数:
cvoStat
是 th 的一个隐含对象,可以获取到循环次数。<span class="badge badge-secondary float-right floor"> <i th:text="${page.offset+cvoStat.count}">1</i>#</span>
-
修改评论内容:
<div class="mt-2" th:utext="${cvo.comment.content}">这开课时间是不是有点晚啊。。。
-
修改时间:
<span>发布于 <b th:text="${#dates.format(cvo.comment.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-15 15:32:18</b>
-
修改回复数量:
<li class="d-inline ml-2"><a href="#" class="text-primary">回复(<i th:text="${cvo.replyCount}"></i>)</a>
-
删除第二条回复。
-
修改回复的遍历:
<li class="pb-3 pt-3 mb-3 border-bottom" th:each="rvo:${cvo.replys}">
-
修改回复列表显示:如果是直接评论楼主,则不显示回复谁,如果评论的其他人则显示回复谁:
<span th:if="${rvo.target==null}"> <b class="text-info" th:text="${rvo.user.username}">寒江雪</b>: </span> <span th:if="${rvo.target!=null}"> <i class="text-info" th:text="${rvo.user.username}">Sissi</i>回复 <b class="text-info" th:text="${rvo.target.username}">寒江雪</b>: </span>
-
修改回复内容:
<span th:utext="${rvo.reply.content}">这个是直播时间哈,觉得晚的话可以直接看之前的完整录播的~
-
修改回复时间:
<span th:text="${#dates.format(rvo.reply.createTime,'yyyy-MM-dd HH:mm:ss')}" >2019-04-15 15:32:18
-
修改回复框评论的绑定:
-
<li class="d-inline ml-2"><a th:href="|#huifu-${rvoStat.count}|" data-toggle="collapse" class="text-primary">回复</a>
-
<div th:id="|huifu-${rvoStat.count}|" class="mt-4 collapse">
-
-
复用首页的分页逻辑:
<nav class="mt-5" th:replace="index::pagination">
-
修改作者下的回帖数量:
<li class="d-inline ml-2"><a href="#replyform" class="text-primary">回帖 <i th:text="${map.post.commentCount}">7</i></a>
添加评论
创建相关结构
CommentMapper
新增一个方法:int insertComment(Comment comment);
在 mapper 下写:
<insert id="insertComment" parameterType="comment">
insert into comment (<include refid="insertFields"></include>)
values(#{userId},#{entityType},#{entityId},#{targetId},#{content},#{status},#{createTime})
</insert>
在 DiscussPostMapper
下新增一个方法:int updateCommentCount(int id,int commentCount);
配置 mapper:
<update id="updateCommentCount">
update discuss_post
set comment_count = #{commentCount}
where id=#{id};
</update>
业务实现
DiscussPostService
:
public int updateCommentCount(int id,int commentCount){
return discussPostMapper.updateCommentCount(id, commentCount);
}
CommentService
:
/**
* 添加评论
* @param comment
* @return
*/
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
public int addComment(Comment comment){
if(comment==null){
throw new IllegalArgumentException("参数不能为空");
}
//添加评论
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;
}
新建 CommentController
:
package com.nowcoder.community.controller;
import com.nowcoder.community.entity.Comment;
import com.nowcoder.community.service.CommentService;
import com.nowcoder.community.util.HostHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.util.Date;
@Controller
@RequestMapping("/comment")
public class CommentController {
@Autowired
private CommentService commentService;
@Autowired
private HostHolder hostHolder;
/**
* 添加评论
* @param discussPostId
* @param comment
* @return
*/
@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;
}
}
修改网页
discuss-detail.html
:
-
修改提交路径:
<form class="replyform" method="post" th:action="@{|/comment/add/${post.id}|">
-
加别名:
<textarea placeholder="在这里畅所欲言你的看法吧!" name=""content>
-
帖子的评论:
<!-- 回帖输入 --> <div class="container mt-3"> <form class="replyform" method="post" th:action="@{|/comment/add/${post.id}|}"> <p class="mt-3"> <a name="replyform"></a> <textarea placeholder="在这里畅所欲言你的看法吧!" name="content"></textarea> <input type="hidden" name="entityType" value="1"> <input type="hidden" name="entityId" th:value="${post.id}"> </p> <p class="text-right"> <button type="submit" class="btn btn-primary btn-sm"> 回 帖 </button> </p> </form> </div>
-
评论的直接评论:
<!-- 回复输入框 --> <li class="pb-3 pt-3"> <form method="post" th:action="@{|/comment/add/${post.id}|}"> <div> <input type="text" class="input-size" name="content" placeholder="请输入你的观点"/> <input type="hidden" name="entityType" value="2"> <input type="hidden" name="entityId" th:value="${cvo.comment.id}"> </div> <div class="text-right mt-2"> <button type="submit" class="btn btn-primary btn-sm" onclick="#"> 回 复 </button> </div> </form>
-
评论中的评论:
<form method="post" th:action="@{|/comment/add/${post.id}|}"> <div> <input type="text" class="input-size" name="content" th:placeholder="|回复${rvo.user.username}|"/> <input type="hidden" name="entityType" value="2"/> <input type="hidden" name="entityId" th:value="${cvo.comment.id}"/> <input type="hidden" name="targetId" th:value="${rvo.user.id}"/> </div> <div class="text-right mt-2"> <button type="submit" class="btn btn-primary btn-sm" onclick="#"> 回 复 </button> </div> </form>
私信列表
如图所示,两条数据我们只视作一条,无论谁发给谁,都是一个对话。小的放前面,大的放后面,查询效率会高一点。
创建相关结构
在 entity
新建 Message
类:
package com.nowcoder.community.entity;
import java.util.Date;
public class Message {
private int id;
private int fromId;
private int toId;
@Override
public String toString() {
return "Message{" +
"id=" + id +
", fromId=" + fromId +
", toId=" + toId +
", conversationId='" + conversationId + '\'' +
", content='" + content + '\'' +
", status=" + status +
", createTime=" + createTime +
'}';
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getFromId() {
return fromId;
}
public void setFromId(int fromId) {
this.fromId = fromId;
}
public int getToId() {
return toId;
}
public void setToId(int toId) {
this.toId = toId;
}
public String getConversationId() {
return conversationId;
}
public void setConversationId(String conversationId) {
this.conversationId = conversationId;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
private String conversationId;
private String content;
private int status;
private Date createTime;
}
创建 MessageMapper
:
package com.nowcoder.community.dao;
import com.nowcoder.community.entity.Message;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface MessageMapper {
//查询当前用户的会话列表,针对每个会话只返回一条最新的私信
List<Message> selectConversation(int userId, int offset, int limit);
//查询当前用户的会话数量
int selectConversationCount(int userId);
//查询某个会话所包含的私信列表
List<Message> selectLetters(String conversationId,int offset,int limit);
//查询某个会话所包含的私信数量
int selectLetterCount(String conversationId);
//查询未读私信的数量
int selectLetterUnreadCount(int userId,String conversationId);
}
配置 mapper:
from_id=1 是系统用户。
<?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.nowcoder.community.dao.MessageMapper">
<sql id="selectFields">
id,from_id,to_id,conversation_id,content,status,create_time
</sql>
<select id="selectConversations" resultType="Message">
select <include refid="selectFields"></include>
from message
where id in(
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>
<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>
<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>
<select id="selectLetterCount" resultType="int">
select count(id)
from message
where status!=2 and from_id!=1 and conversation_id=#{conversationId}
</select>
<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>
</mapper>
写个测试:
@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 = messageMapper.selectLetters("111_112", 0, 10);
for(Message message:list){
System.out.println(message);
}
count=messageMapper.selectLetterCount("111_112");
System.out.println(count);
count=messageMapper.selectLetterUnreadCount(131,"111_131");
System.out.println(count);
}
业务实现
创建 MessageService
:
package com.nowcoder.community.service;
import com.nowcoder.community.dao.MessageMapper;
import com.nowcoder.community.entity.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class MessageService {
@Autowired
public 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);
}
}
创建 MessageController
:
package com.nowcoder.community.controller;
import com.nowcoder.community.entity.Message;
import com.nowcoder.community.entity.Page;
import com.nowcoder.community.entity.User;
import com.nowcoder.community.service.MessageService;
import com.nowcoder.community.service.UserService;
import com.nowcoder.community.util.HostHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Controller
public class MessageController {
@Autowired
private MessageService messageService;
@Autowired
private HostHolder hostHolder;
@Autowired
private UserService userService;
//私信列表
@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()));
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";
}
}
修改网页
index.html
:
- 修改信息的链接:
<a class="nav-link position-relative" th:href="@{/letter/list}">消息<span data-type="text">12</span>
letter.html
:
-
使用模板引擎,修改相对路径,,复用头部。
-
删除多余的私信列表,只保留一个 li。
-
修改朋友私信链接,显示私信数量:
<a class="nav-link position-relative active" th:href="@{/letter/list}">朋友私信<span class="badge badge-danger" th:text="${letterUnreadCount}" th:if="${letterUnreadCount!=0}">3</span></a>
-
更改私信会话的遍历:
<li class="media pb-3 pt-3 mb-3 border-bottom position-relative" th:each="map:${conversations}">
-
更改某个会话的未读数量:
<span class="badge badge-danger" th:text="${map.unreadCount}" th:if="${map.unreadCount!=0}">3
-
更改会话用户头像:
<img th:src="${map.target.headerUrl}" class="mr-4 rounded-circle user-header" alt="用户头像" >
-
更改某个会话相关信息:
<div class="media-body"> <h6 class="mt-0 mb-3"> <span class="text-success" th:utext="${map.target.username}">落基山脉下的闲人</span> <span class="float-right text-muted font-size-12" th:text="${#dates.format(map.conversation.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-28 14:13:25</span> </h6> <div> <a href="letter-detail.html" th:utext="${map.conversation.content}">米粉车, 你来吧!</a> <ul class="d-inline font-size-12 float-right"> <li class="d-inline ml-2"><a href="#" class="text-primary">共<i th:text="${map.letterCount}">5</i>条会话</a></li> </ul> </div> </div>
-
复用 index 的分页。
私信会话查看
//私信详情
@RequestMapping(path = "/letter/detail/{conversationId}",method = RequestMethod.GET)
public String getLetterDetail(@PathVariable("conversationId") String conversationId,Model model, Page page){
//分页信息
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";
}
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(id0);
}
}
修改网页
letter.html
:
- 修改私信详情链接:
<a th:href="@{|/letter/detail/${map.conversation.conversationId}|}" th:utext="${map.conversation.content}">米粉车, 你来吧!
letter-detail.html
:
-
使用模板引擎,修改相对路径,,复用头部。
-
更改来自谁的用户私信:
<h6><b class="square"></b> 来自 <i class="text-success" th:utext="${target.username}">落基山脉下的闲人</i> 的私信
-
私信列表只保留一条 li。
-
更改私信列表:
<!-- 私信列表 --> <ul class="list-unstyled mt-4"> <li class="media pb-3 pt-3 mb-2" th:each="map:${letters}"> <a href="profile.html"> <img th:src="${map.fromUser.headerUrl}" class="mr-4 rounded-circle user-header" alt="用户头像" > </a> <div class="toast show d-lg-block" role="alert" aria-live="assertive" aria-atomic="true"> <div class="toast-header"> <strong class="mr-auto" th:utext="${map.fromUser.username}">落基山脉下的闲人</strong> <small th:text="${#dates.format(map.letter.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-25 15:49:32</small> <button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="toast-body" th:utext="${map.letter.content}"> 君不见, 黄河之水天上来, 奔流到海不复回! </div> </div> </li> </ul>
-
复用分页。
-
更改返回链接:
<button type="button" class="btn btn-secondary btn-sm" onclick="back();">返回</button>
-
加个 js:
<script> function back(){ location.href=CONTEXT_PATH+"/letter/list"; } </script>
发送私信
创建相关结构
MessageMapper
:
//新增信息
int insertMessage(Message message);
//修改信息的状态
int updateStatus(List<Integer> ids,int status);
配置 mapper:
<sql id="insertFields">
from_id,to_id,conversation_id,content,status,create_time
</sql>
<insert id="insertMessage" parameterType="Message" keyProperty="id">
insert into message(<include refid="insertFields"></include>)
values (#{fromId},#{toId},#{conversationId},#{content},#{status},#{createTime})
</insert>
<update id="updateStatus">
update message set status = #{status}
where id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
在UserService
:
/**
* 根据用户名查找用户
* @return
*/
public User findUserByName(String username){
return userMapper.selectByName(username);
}
业务实现
MessageService
:
public int addMessage(Message message){
message.setContent(HtmlUtils.htmlEscape(message.getContent()));
message.setContent(sensitiveFilter.filter(message.getContent()));
return messageMapper.insertMessage(message);
}
public int readMessage(List<Integer> ids){
return messageMapper.updateStatus(ids,1);
}
MessageController
:
/**
* 发送私信
* @param toName
* @param content
* @return
*/
@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());
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);
}
修改网页
letter.js
:
function send_letter() {
$("#sendModal").modal("hide");
var toName=$("#recipient-name").val();
var content=$("#message-text").val();
$.pos(
CONTEXT_PATH+"/letter/send",
{"toName":toName,"content":content},
function (data){
data=$.parseJSON(data);
if (data.code==0){
$("#hintBody").text("发送成功!");
}else {
$("#hintBody").text(data.msg);
}
//刷新
$("#hintModal").modal("show");
setTimeout(function (){
$("#hintModal").modal("hide");
location.reload();
},2000);
}
);
}
letter-detail.html
:
- 更改发私信弹窗的私信人用户名:
<input type="text" class="form-control" id="recipient-name" th:value="${target.username}">
设置已读私信
/**
* 获得未读消息id
* @param letterList
* @return
*/
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()&&message.getStatus()==0){
ids.add(message.getId());
}
}
}
return ids;
}
/**
* 私信详情
* @param conversationId
* @param model
* @param page
* @return
*/
@RequestMapping(path = "/letter/detail/{conversationId}",method = RequestMethod.GET)
public String getLetterDetail(@PathVariable("conversationId") String conversationId,Model model, Page page){
//分页信息
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));
//设置已读
List<Integer> ids=getLetterIds(letterList);
if(!ids.isEmpty()){
messageService.readMessage(ids);
}
return "/site/letter-detail";
}
统一处理异常
-
@ControllerAdvice
- 用于修饰类,表示该类是 Controller 的全局配置类。
- 在此类中,可以对 Controller 进行如下三种全局配置:
异常处理方案、绑定数据方案、绑定参数方案。
-
@ExceptionHandler
- 用于修饰方法,该方法会在 Controller 出现异常后被调用,用于处理捕获到的异常。
-
@ModelAttribute
- 用于修饰方法,该方法会在 Controller 方法执行前被调用,用于为 Model 对象绑定参数。
-
@DataBinder
- 用于修饰方法,该方法会在 Controller 方法执行前被调用,用于绑定参数的转换器。
预准备
error 文件夹必须放到 templates 下,文件下的 html 名称都是错误码。
修改 404、500 网页,使用 th 引擎,修改相对路径。
代码实现
在HomeController
:
@RequestMapping(path = "/error",method = RequestMethod.GET)
public String getErrorPage(){
return "/error/500";
}
在 Controller
下创建一个 advice
包,再创建 ExceptionAdvice
,
package com.nowcoder.community.controller.advice;
import com.nowcoder.community.util.CommunityUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@Slf4j
@ControllerAdvice(annotations = Controller.class)
public class ExceptionAdvice {
@ExceptionHandler(Exception.class)
public void handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException {
log.error("服务器发送异常:"+e.getMessage());
for(StackTraceElement element:e.getStackTrace()){
log.error(element.toString());
}
//判断是主动请求还是异步请求
String xRequestedWith = request.getHeader("x-requested-with");
//异步请求
if("XMLHttpRequest".equals(xRequestedWith)){
response.setContentType("application/plain;charset=utf-8");
//确保响应的内容以纯文本形式传输,并且使用UTF-8编码。
PrintWriter writer=response.getWriter();
writer.write(CommunityUtil.getJSONString(1,"服务器异常!"));
//获取一个JSON格式的字符串,并将该字符串写入响应流中。这样可以将一个包含错误信息的JSON字符串作为响应返回给客户端。
}else{
//主动请求
response.sendRedirect(request.getContextPath()+"/error");
}
}
}
统一记录日志
概念
AOP 的概念
-
Aspect Oriented Programing, 即面向方面(切面)编程。
-
AOP 是一种编程思想,是对 OOP 的补充, 可以进一步提高编程的效率。
AOP 的术语
AOP(面向切面编程)是一种编程范式,它通过在程序中定义切面(Aspect)来实现横切关注点(Cross-cutting Concerns)的模块化。以下是一些 AOP 的术语:
- 切面(Aspect):切面是一个模块,它包含了与横切关注点相关的一组通用行为。它定义了在哪些地方(连接点)以及如何(通知)执行这些行为。例如,日志记录、异常处理等可以作为一个切面。
- 连接点(Join Point):连接点是在程序执行过程中可以插入切面的点。它可以表示方法调用、方法执行、异常抛出等。例如,在方法执行前或执行后的连接点都可以作为切面的插入点。
- 通知(Advice):通知定义了在连接点处执行的切面代码。它表示了切面在连接点的何时和如何执行。常见的通知类型有前置通知(Before)、后置通知(After)、环绕通知(Around)、异常通知(AfterThrowing)和返回通知(AfterReturning)。
- 切入点(Pointcut):切入点定义了在哪些连接点上应用切面。它通过表达式或者指定的规则来匹配连接点。例如,匹配所有以"get"开头的方法调用的切入点可以用表达式
execution(* get*(..))
来表示。- 引入(Introduction):引入允许在不修改现有代码的情况下,向现有类添加新的方法或属性。它可以使现有的类实现新的接口或继承新的父类。引入可以用于为现有类添加新的功能。
- 织入(Weaving):织入是将切面应用到目标对象上,使切面的行为与目标对象的代码进行合并的过程。它可以在编译时、加载时或运行时进行。织入可以通过编译器、类加载器或使用代理对象实现。
例如,假设我们有一个 Web 应用程序,我们想要记录每个请求的执行时间。我们可以创建一个切面来实现这个功能。以下是一个使用 AOP 术语描述的例子:
- 切面:日志记录切面。
- 连接点:所有 Controller 类的公共方法。
- 通知:前置通知,在连接点处记录请求的开始时间。
- 切入点:匹配所有 Controller 类的公共方法。
AOP 的实现
-
AspectJ
- AspectJ 是语言级的实现,它扩展了 Java 语言,定义了 AOP 语法。
- AspectJ 在编译期织入代码,它有一个专门的编译器,用来生成遵守 Java 字节码规范的 class 文件。
-
Spring AOP
- Spring AOP 使用纯 Java 实现,它不需要专门的编译过程,也不需要特殊的类装载器。
- Spring AOP 在运行时通过代理的方式织入代码,只支持方法类型的连接点。
- Spring 支持对 AspectJ 的集成。
Spring AOP
-
JDK 动态代理
- Java 提供的动态代理技术,可以在运行时创建接口的代理实例。
- Spring AOP 默认采用此种方式,在接口的代理实例中织入代码。
-
CGLib 动态代理(写的 service 没有接口)
- 采用底层的字节码技术,在运行时创建子类代理实例。
- 当目标对象不存在接口时,Spring AOP 会采用此种方式,在子类实例中织入代码
示例
项目下创建 aspect
包,创建 AlphaAspect
类:
package com.nowcoder.community.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@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");
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
System.out.println("around before");
Object obj=joinPoint.proceed();//调用原始对象的方法,代理它允许
System.out.println("around after");
return obj;
}
}
记录日志
创建 ServiceLogAspect
类:
package com.nowcoder.community.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.text.SimpleDateFormat;
import java.util.Date;
@Slf4j
@Component
@Aspect
public class ServiceLogAspect {
@Pointcut("execution(* com.nowcoder.community.service.*.*(..))")
public void pointcut(){
}
@Before("pointcut()")
public void before(JoinPoint joinPoint){
//用户[1.2.3.4] 在[xxx],访问了[com.nowcoder.community.service.xxx()].
ServletRequestAttributes attributes= (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request=attributes.getRequest();
String ip=request.getRemoteHost();
String now=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
//获取方法
String target=joinPoint.getSignature().getDeclaringTypeName()+"."+joinPoint.getSignature().getName();
log.info(String.format("用户[%s],在[%s],访问了[%s]",ip,now,target));
}
}
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于