一、过滤敏感词
前缀树
名称:Trie、字典树、查找树
特点:查找效率高,消耗内存大
应用:字符串检索、词频统计、字符串排序等
敏感词过滤器
定义前缀树
根据敏感词,初始化前缀树
编写过滤敏感词的方法
定义敏感词
为简便处理定义一个敏感词文件,resources目录下新建一个sensitive-words.txt文件
定义前缀树
因为不会被外界访问,所以在util包下的SensitiveFilter类中定义了内部类
//前缀树
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 getTrieNode(Character c)
{
return subNodes.get(c);//通过key查value
}
}
根据敏感词初始化前缀树
@Component
public class SensitiveFilter
{
private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);
//把敏感词替换为
private static final String REPLACEMENT = "***";
//初始化trie的根节点
private TrieNode rootNode = new TrieNode();//空
@PostConstruct//初始化,当容器实例化Bean以后,此注解的方法就会被自动调用
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 (IOException e)
{
logger.error("加载敏感词文件失败:" + e.getMessage());
}
}
//将敏感词添加到前缀树
private void addKeyWord(String keyword)//keyword就是txt里面的内容
{
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);
}
}
}
过滤敏感词方法
依然是在这个类中定义方法
//实现检索过程,返回的是替换掉敏感词的字符串
//现在这个方法是啥呢,就是前缀树已经构建完成了,然后根据字符串遍历
public String filter(String text)
{
if (StringUtils.isBlank(text))
{
return null;
}
//指针1,指向树
TrieNode tempNode = rootNode;
//指针2
int begin = 0;
//指针3
int position = 0;
//结果字符串
StringBuilder sb = new StringBuilder();
while (begin < text.length())
{
char c = text.charAt(position);
//跳过符号
if (isSymbol(c))
{
//如果是特殊字符,记录结果,指针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;//begin++,让后面节点和前置节点在一个char上
//指针重新指向根节点
tempNode = rootNode;
}else if (tempNode.isKeyWordEnd())//如果没结束,返回值是false,直接position++继续检查
{
//发现敏感词,将begin~position字符串替换掉
sb.append(REPLACEMENT);
//进入下一个位置
begin = ++ position;
tempNode = rootNode;
}else
{
position++;//继续检查
}
}
//将最后一批字符计入结果
sb.append(text.substring(begin));
return sb.toString();
}
二、发布帖子
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请求,实现发布帖子的功能。
1.导入Fastjson
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.68</version>
</dependency>
2.在CommunityUtil类中写几个封装成Json的方法
//获取JSON字符串,msg是提示信息,map灵活使用
public static String getJSONString(int code, String msg, Map<String, Object> map)
{
JSONObject json = new JSONObject();
json.put("code", code);
json.put("msg", msg);
if (map != null)
{
//通过遍历可以来遍历map
for (String key : map.keySet())
{
json.put(key, map.get(key));//把map中的key和value存入JSON
}
}
return json.toJSONString();
}
public static String getJSONString(int code, String msg)
{
return getJSONString(code, msg, null);
}
public static String getJSONString(int code)
{
return getJSONString(code, null, null);
}
3.在AlphaController中写一个实例controller
// ajax示例
@RequestMapping(path = "/ajax", method = RequestMethod.POST)//异步提交一般用post
@ResponseBody
public String testAjax(String name, int age) {
System.out.println(name);
System.out.println(age);
return CommunityUtil.getJSONString(0, "操作成功!");
}
4.为了方便直接写一个静态的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>
为什么要用 HtmlUtils.htmlEscape? 因为有些同学在恶意注册的时候,会使用诸如 这样的名称,会导致网页打开就弹出一个对话框。 那么在转义之后,就没有这个问题了。
1.老规矩先写插入帖子的dao层
package com.nowcoder.community.dao;
import com.nowcoder.community.entity.DiscussPost;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface DiscussPostMapper
{
/*
* 开发个人主页的时候会用到userId,可以看到自己发布的帖子,首页是不需要的,需要实现动态sql,如果是0就不拼接,不是0就拼接
* offset是起始行号,limit是每一页最多显示多少条数据
* */
List<DiscussPost> selectDiscussPosts(int userId,int offset,int limit);
//如果需要动态拼一个条件,而且方法有且只有一个条件,必须写别名
int selectDiscussPostRows(@Param("userId") int userId);//查一共有多少帖子//param起别名
//增加帖子
int insertDiscussPost(DiscussPost discussPost);
}
2.写对应mapper.xml
<insert id="insertDiscussPost" parameterType="DiscussPost">
insert into discuss_post(<include refid="insertFields"></include>)
values (#{userId},#{title},#{content},#{type},#{status},#{createTime},#{commentCount},#{score})
</insert>
3.写Service层在DiscussPostService类中
public int addDiscussPost(DiscussPost post)
{
if (post == null)
{
throw new IllegalArgumentException("参数不能为空!");
}
//把前端代码中的<>也转化
//转义HTML标记
post.setTitle(HtmlUtils.htmlEscape(post.getTitle()));
post.setContent(HtmlUtils.htmlEscape(post.getTitle()));
//过滤,只有这两个需要转义和过滤
post.setTitle(sensitiveFilter.filter(post.getTitle()));
post.setContent(sensitiveFilter.filter(post.getContent()));
return discussPostMapper.insertDiscussPost(post);
}
4.controller层新建一个DiscussPostController类
package com.nowcoder.community.controller;
import com.nowcoder.community.entity.DiscussPost;
import com.nowcoder.community.entity.User;
import com.nowcoder.community.service.DiscussPostService;
import com.nowcoder.community.util.CommunityUtil;
import com.nowcoder.community.util.HostHolder;
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)
{
//403代表没有权限
return CommunityUtil.getJSONString(403,"你还没有登录");
}
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());//因为user的主键id是没法被get的,所以大可放心的用!
post.setTitle(title);
post.setContent(content);
post.setCreateTime(new Date());
discussPostService.addDiscussPost(post);
//报错会统一处理,不在这儿写啦89
return CommunityUtil.getJSONString(0,"发布成功!");
}
}
5.回头看index.html中发布相关
<button type="button" class="btn btn-primary btn-sm position-absolute rt-0" data-toggle="modal" data-target="#publishModal">我要发布</button>
</div>
<!-- 弹出框 -->
<div class="modal fade" id="publishModal" tabindex="-1" role="dialog" aria-labelledby="publishModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="publishModalLabel">新帖发布</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<form>
<div class="form-group">
<label for="recipient-name" class="col-form-label">标题:</label>
<input type="text" class="form-control" id="recipient-name">
</div>
<div class="form-group">
<label for="message-text" class="col-form-label">正文:</label>
<textarea class="form-control" id="message-text" rows="15"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="publishBtn">发布</button>
</div>
看这已经定义好了逻辑,我们改写这
6.改写上述js
$(function(){
$("#publishBtn").click(publish);
});
function publish() {
/*把弹框隐藏*/
$("#publishModal").modal("hide");
//获取标题和内容
var title = $("#recipient-name").val();
var content = $("#message-text").val();
//发送异步请求
$.post(
//global.js中定义的
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);
}
)
}
7.另外有个问题
我要发布按钮在登录前不应该显示,嗨呀其实也无所谓,因为发布时判断是否登录了嘛。。
三、帖子详情
DiscussPostMapper
DiscussPostService
DiscussPostController
index.html
在帖子标题上增加访问详情页面的链接
discuss-detail.html
处理静态资源的访问路径
复用index.html的header区域
显示标题、作者、发布时间、帖子正文等内容
1.DiscussPostMapper增加查询帖子详情
DiscussPost selectDiscussPostById(int id);
2.配置mapper.xml
<select id="selectDiscussPostById" resultType="com.nowcoder.community.entity.DiscussPost">
select
<include refid="selectFields"></include>
from discuss_post
where id = #{id}
</select>
3.写service层
//查询帖子详细信息
public DiscussPost findDiscussPostById(int id)
{
return discussPostMapper.selectDiscussPostById(id);
}
如果想把userId和id变成头像啥的,可以有两种方法
1.在mapper中写一个关联查询,这样的结果就是可能有些业务并不需要关联查询,就查多了,有冗余
2.因为已经在视图层了,可以注入UserService,通过UserService查两次,但是会慢,后期学Redis之后在内存中访问会快
3.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";
}
再重复一下,想要前端利用后端这些属性,需要用model封装一下,调用addAttribute()方法,例如model.addAttribute("user",user) 就是告诉前端,这是个叫做user的东西,你可以用里面的属性
然后就是,Controller里面返回的是项目中的位置,第一层不用写,所以如下所示
4.处理首页让每个帖子有个链接
5.处理discuss-detail页面
先改成Thymeleaf模板的格式
接着填充数据
四、事务管理
回顾
什么是事务
事务是由N步数据库操作序列组成的逻辑执行单元,这系列操作要么全执行,要么全放弃执行。
事务的特性(ACID)
原子性(Atomicity):事务是应用中不可再分的最小执行体。
一致性(Consistency):事务执行的结果,须使数据从一个一致性状态,变为另一个一致性状态。
隔离性(Isolation):各个事务的执行互不干扰,任何事务的内部操作对其他的事务都是隔离的。
持久性(Durability):事务一旦提交,对数据所做的任何改变都要记录到永久存储器中。
事务的隔离性
常见的并发异常
第一类丢失更新、第二类丢失更新。每一个浏览器的访问都算作一次线程
脏读、不可重复读、幻读。
常见的隔离级别
Read Uncommitted:读取未提交的数据。
Read Committed:读取已提交的数据。
Repeatable Read:可重复读。
Serializable:串行化(要加锁)
详见笔记
演示声明式事务
1.在AlphaService中写一个新方法加@Transaction注解
// REQUIRED: 支持当前事务(外部事务),如果不存在则创建新事务.
// REQUIRES_NEW: 创建一个新事务,并且暂停当前事务(外部事务).
// NESTED: 如果当前存在事务(外部事务),则嵌套在该事务中执行(独立的提交和回滚),否则就会REQUIRED一样.
@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";
}
2.写个测试方法调用这个方法,发现数据库中并没有插入任何数据
演示编程式事务
1.在加一个方法
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";
}
});
}
2.测试发现也没有插入数据
五、显示评论
数据层
根据实体查询一页评论数据。
根据实体查询评论的数量。
业务层
处理查询评论的业务。
处理查询评论数量的业务。
表现层
显示帖子详情数据时,
同时显示该帖子所有的评论数据。
根据评论的数据库我们创建一套实体类
id表明这个评论发出的早晚顺序
user_id 表明这个评论发出的用户
entity_type 表明这个评论的类型(是属于帖子的评论,还是评论的评论,还是问题的评论)
entity_id 表明这个评论的帖子是哪一个
target_id 表明这个帖子所指向的地址
content 表明的是的是帖子的内容
status 表明的是这个评论的状态
create_time 表明的是这个帖子创立的时间
数据层
先看comment表结构
entity_type:评论的类型,比如帖子的评论,评论用户评论的评论 - -
entity_id:评论的帖子是哪一个
target_id:记录评论指向的人
content:评论的内容
status:表明状态 0为正常的 1为删除的或者是错误的
create_time:创建的时间
1.写个实体类
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;
2.写个创建新的Mapper类
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> selectCommentsByEntity(int entityType,int entityId, int offset,int limit);//根据实体查询,查询帖子的评论/评论的评论/人的评论
//查询到的帖子数量
int selectCountByEntity(int entityType,int entityId);
}
3.写对应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="selectCommentsByEntity" resultType="com.nowcoder.community.entity.Comment">
select (<include refid="selectFields"></include>)
from comment
where status = 0
and entity_type = #{entityType}
and entity_id = #{entityId}
order by create_time desc
limit #{offset},#{limit}
</select>
<select id="selectCountByEntity" resultType="java.lang.Integer">
select count(id)
from comment
where status = 0
and entity_type = #{entityType}
and entity_id = #{entityId}
</select>
</mapper>
别忘了写完测试一波
业务层
很简单
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> findCommentByEntity(int entityType, int entityId, int offset, int limit)
{
return commentMapper.selectCommentsByEntity(entityType, entityId, offset, limit);
}
public int findCommentCount(int entityType, int entityId)
{
return commentMapper.selectCountByEntity(entityType, entityId);
}
}
表现层
controller:处理请求
页面:展现数据,通过Page类来展现分类
1.直接在DiscussPostController中加写内容
@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());
//评论VO列表--viewobject
//用map来封装给页面的数据
List<Map<String, Object>> commentVoList = new ArrayList<>();
if (commentList != null)
{
for (Comment comment : commentList)
{
Map<String, Object> commentVo = new HashMap<>();
//添加评论
commentVo.put("comment", comment);
//添加评论作者
commentVo.put("user", userService.findUserById(comment.getUserId()));//注意这里不能直接写user,这里的user是作者,而非是评论帖子的人
//回复列表,不做分页了
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()));
//处理targetid,看回复的目标
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";
}
2.处理模板
还有一堆慢慢写就没事
3.分页处理直接复用index页面的分页就可
为都一样并且controller中查询恢复帖子的列表也是根据当前页面offset和limit查的
六、添加评论
数据层
增加评论数据。
修改帖子的评论数量。
业务层
处理添加评论的业务:
先增加评论、再更新帖子的评论数量。
表现层
处理添加评论数据的请求。
设置添加评论的表单。
数据层
1.增加插入评论的mapper
2.增加帖子回复数量的mapper
业务层
1.DiscussPostService
2.CommentService
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public int addComment(Comment comment)
{
//内容过滤,html与敏感词
if (comment == null)
{
throw new IllegalArgumentException("参数不能为空!");
}
//先过滤标签
comment.setContent(HtmlUtils.htmlEscape(comment.getContent()));
//过滤敏感词
comment.setContent(sensitiveFilter.filter(comment.getContent()));
//添加评论
int rows = commentMapper.insertComment(comment);
//更新帖子的评论数量,要根据type判断是给谁评论的
if (comment.getEntityType() == ENTITY_TYPE_POST)
{
int count = commentMapper.selectCountByEntity(comment.getEntityType(), comment.getEntityId());//entityid表示的是评论哪一个帖子
discussPostService.updateCommentCount(comment.getEntityId(), count);
}
return rows;
}
表现层
1.单独创建一个Controller—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;
@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;
}
}
2.处理页面
给帖子回复
给评论回复
给某个人回复
七、私信之显示私信列表
私信列表
查询当前用户的会话列表,每个会话只显示一条最新的私信。
支持分页显示。
个人想法:
首先创建message实体类,创建message-mapper。我们发现message-mapper可能有这么几种方法会在展示层上用到,我们可以先写。首先是我们在会话列表中需要展示最新的私信,我们还需要得出总的会话行数,进行分页的操作。第二,我们点进私信详情时,展示与之对话的私信,并且也将分页功能实现
表设计
conversasion_id:表明通信的双方id拼接,规定小的id在前大的在后
数据层
1.写entity
2.写mapper
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> selectConversations(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);
}
<?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="com.nowcoder.community.entity.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
)/*查询所有对话的最大的id,让他来代表*/
order by id desc
limit #{offset}, #{limit}
</select>
<select id="selectConversationCount" resultType="java.lang.Integer">
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="com.nowcoder.community.entity.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="java.lang.Integer">
select count(id)
from message
where status != 2
and from_id != 1
and conversation_id = #{conversationId}
</select>
<select id="selectLetterUnreadCount" resultType="java.lang.Integer"></select>
</mapper>
业务层
新建个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
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);
}
}
表现层
1.新建一个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.PathVariable;
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);
//查询未读消息数量,因为是找所有的未读消息,所以不要传conversationid
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<>();
//为什么要对letterList进行判断呢,因为letterList才是得到的所有私信
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);
}
}
}
2.处理视图
先改成Thymeleaf的形式
往里边填充数据即可没啥好说的轻车熟路
八、私信之私信详情
私信详情
查询某个会话所包含的私信。
支持分页显示。
@RequestMapping(path = "/letter/detail/{conversationId}",method = RequestMethod.GET)
public String getLetterDetail(@PathVariable("conversationId")String conversationId,Page page,Model model){
//设置分页信息
page.setPath("/letter/detail/"+conversationId);
page.setLimit(5);
page.setRows(messageService.findLetterCount(conversationId));
//私信列表
List<Message> lettersList = messageService.findLetters(conversationId, page.getOffset(), page.getLimit());
List<Map<String,Object>> letters = new ArrayList<>();
if(lettersList!=null){
for(Message message:lettersList){
Map<String,Object> map = new HashMap<>();
map.put("letter",message);
map.put("fromUser",userService.findUserById(message.getFromId()));
letters.add(map);
}
}
model.addAttribute("letters",letters);
//判断和谁对话
User target = getLetterTarget(conversationId);
model.addAttribute("target",target);
return "/site/letter-detail";
}
private User getLetterTarget(String conversationId){
User user = hostHolder.getUser();
String[] ids = conversationId.split("_");
int id1 = Integer.parseInt(ids[0]);
int id2 = Integer.parseInt(ids[1]);
return user.getId()==id1?userService.findUserById(id2):userService.findUserById(id1);
}
2.处理页面
私信详情返回后处理已读消息个数解决方法
点开详情前先把看过的数减了
九、发送私信
发送私信
采用异步的方式发送私信。
发送成功后刷新私信列表。
数据层
业务层
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);
}
表现层
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)
{
//这里找的是私信的目标,用hostholder得到当前用户,那么要发送的就是id1了
return userService.findUserById(id1);
} else
{
return userService.findUserById(id0);
}
}
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());//获取的是私信的id
}
}
}
return ids;
}
@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);
}
十、统一处理异常
@ControllerAdvice
用于修饰类,表示该类是Controller的全局配置类。
在此类中,可以对Controller进行如下三种全局配置:
异常处理方案、绑定数据方案、绑定参数方案。
@ExceptionHandler—异常处理方案
用于修饰方法,该方法会在Controller出现异常后被调用,用于处理捕获到的异常。
@ModelAttribute—绑定数据方案(想象下Page类被自动封装进Model里)
用于修饰方法,该方法会在Controller方法执行前被调用,用于为Model对象绑定参数。
@DataBinder—绑定参数方案(想象下Page类的使用)
用于修饰方法,该方法会在Controller方法执行前被调用,用于绑定参数的转换器。
往上抛最终处理异常在表现层
SpringBoot自动处理方式
1.把报错的错误码作为页面名放到如下目录下,当报出来相关错误会自动显示报错的页面。
@ControllerAdvice和@ExceptionHandler处理异常
1.写一个跳转到处理页面的controller,这里在HomeController里写
@RequestMapping(path = "/error",method = RequestMethod.GET)
public String getErrorPage()
{
return "/error/500";
}
2.在controller包下新建advice包并创建处理异常类
package com.nowcoder.community.controller.advice;
import com.nowcoder.community.util.CommunityUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
@ControllerAdvice(annotations = Controller.class)//限制范围,只扫描带有Controller注解的Bean
public class ExceptionAdvice
{
private static final Logger logger = LoggerFactory.getLogger(ExceptionAdvice.class);
//加注解表示处理异常
@ExceptionHandler(Exception.class)
public void handleException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException//后面两个参数来处理响应
{
logger.error("服务器发生异常:" + e.getMessage());
for (StackTraceElement element : e.getStackTrace())
{
logger.error(element.toString());//详细输出所有错误信息
}
//通过Request来判断是普通请求还是异步请求,从请求的消息头来获取值
String xRequestedWith = request.getHeader("x-requested-with");
if ("XMLHttpRequest" == xRequestedWith)//以xml方式返回,因此是异步请求,普通方式返回html
{
response.setContentType("application/plain;charset=utf-8");//plain表示返回普通格式,浏览器将字符串转换回JSON
PrintWriter writer = response.getWriter();
writer.write(CommunityUtil.getJSONString(1,"服务器异常"));
}else
{
response.sendRedirect(request.getContextPath()+"/error");//普通请求就直接重定向
}
}
}
十一、统一记录日志
需求:对所有的service记录日志
记录日志不是也无需求,不要写在业务里面
用AOP
AOP的概念
Aspect Oriented Programing,即面向方面(切面)编程。
AOP是一种编程思想,是对OOP的补充,可以进一步提高编程的效率。
AOP的术语
针对aspectJ编程,和其他的Service无关,把aspect置入到目标,置入的位置叫连接点
Target:已处理完业务逻辑的代码为目标对象
Joinpoint:目标对象上有很多地方能被织入代码叫连接点
Pointcut:切点声明到底织入到哪些位置
Advice:通知声明到底要处理什么样的逻辑
AOP的实现
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会采用此种方式,在子类实例中织入代码。
来个小例子
1.导入一个包
2.新建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.*.*(..))")//第一个*是返回值,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 proceedingJoinPoint) throws Throwable//在连接点上下都执行
{
System.out.println("around before");
Object obj = proceedingJoinPoint.proceed();//调用目标组件被处理的方法
System.out.println("around after");
return obj;
}
}
正式处理业务
package com.nowcoder.community.aspect;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
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;
@Component
@Aspect
public class ServiceLogAspect
{
private static final Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class);
@Pointcut("execution(* com.nowcoder.community.service.*.*(..))")//第一个*是返回值,service包下面的所有类的所有的方法的所有参数都要处理
public void pointcut()
{
}
@Before("pointcut()")
public void before(JoinPoint joinPoint)//joinpoint是要加强/需要加日志的方法
{
//用户(xxx)在xx时间访问量com.nowcoder.community.service.xxx方法
//获取用户ip,通过request
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();
//这里%s是占位符
logger.info(String.format("用户[%s]","在[%s],访问了[%s].",ip,now,target));
}
}