仿牛客网第三章
一、过滤敏感词
目的:开发一个能够过滤敏感词的工具
对字符串进行过滤,调用API判断有没有敏感词。使用repalce替换
but
敏感词过多、字符串可能过长,使用replace替换性能太差了
采用前缀树数据结构、我们直接实现过滤敏感词的算法
- 前缀树
- 名称:Trie、字典树、查找树
- 特点:查找效率高,消耗内存大【空间换时间】
- 应用:字符串检索、词频统计、字符串排序等
- 敏感词过滤器
- 定义前缀树
- 根据敏感词,初始化前缀树
- 编写过滤敏感词的方法
签字数包含一个根节点、根节点不包含任意内容
其它节点都只包含一个字符
第二个指针不回头【一个一个的走】,第三个指针会往回走【小范围抖动】
开发复用的工具【评论、发帖子都需要使用】
定义敏感词
为简便处理定义一个敏感词文件,resources目录下新建一个sensitive-words.txt文件
定义前缀树
因为不会被外界访问,所以在util包下的SensitiveFilter类中定义了内部类
@Component //for复用,托管到容器
public class SensitiveFilter {
//定义前缀树的结构
private class TireNode{
//关键词结束的标识
private boolean isKeywordEnd = false;
//子节点(key是下级字符,value是下级节点)
private Map<Character,TireNode> subNodes = new HashMap<>();
//添加子节点
public void addSubNode(Character c,TireNode node){
subNodes.put(c,node);
}
//获取子节点
public TireNode getSubNode(Character c){
return subNodes.get(c);
}
public boolean isKeywordEnd() {
return isKeywordEnd;
}
public void setKeywordEnd(boolean keywordEnd) {
isKeywordEnd = keywordEnd;
}
}
}
根据敏感词初始化前缀树
@Component
public class SensitiveFilter {
private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);
//要替换的符号
private static final String REPLACEMENT="***";
//根节点
private TireNode rootNode = new TireNode();
@PostConstruct //服务启动初始化bean时构造器之后执行
public void init(){
try(//读文件
InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
BufferedReader br =new BufferedReader(new InputStreamReader(is));
) {
String keyword;
while((keyword = br.readLine())!=null){
//添加到前缀树
this.addKeyWord(keyword);
}
} catch (Exception e) {
logger.error("加载敏感词文件失败:"+e.getMessage());
}
}
//将一个敏感词添加到前缀树 内部
private void addKeyWord(String keyword) {
if(StringUtils.isBlank(keyword)) return;
char[] arr = keyword.toCharArray();
TireNode tmpNode = rootNode;
for(int i=0;i<arr.length;i++){
//看是已经具有子节点
TireNode subNode = tmpNode.getSubNode(arr[i]);
if(subNode==null){
//初始化化子节点
subNode = new TireNode();
tmpNode.addSubNode(arr[i],subNode);
}
//指向子节点,进入下一轮循环
tmpNode = subNode;
}
tmpNode.setKeywordEnd(true);//结束标识
}
//定义前缀树
private class TireNode{
//关键词结束的标识
private boolean isKeywordEnd = false;
//子节点(key是下级字符,value是下级节点)
private Map<Character,TireNode> subNodes = new HashMap<>();
//添加子节点
public void addSubNode(Character c,TireNode node){
subNodes.put(c,node);
}
//获取子节点
public TireNode getSubNode(Character c){
return subNodes.get(c);
}
public boolean isKeywordEnd() {
return isKeywordEnd;
}
public void setKeywordEnd(boolean keywordEnd) {
isKeywordEnd = keywordEnd;
}
}
}
过滤敏感词方法
依然是在这个类中定义方法
/**
* 过滤敏感词 被外界调用
* @param text 待过虑的词
* @return 过滤后的文本
*/
public String filter(String text){
if(StringUtils.isBlank(text)) return null;//看这个参数是否合法,判空
//指针1 指向树,不断扫描前缀树,默认指向根
TireNode tmpNode = rootNode;
//指针2 指向首,一直
int begin = 0;
//指针3
int position = 0;
//结果 记录最后结果,是变长的 stringBuilder
StringBuilder sb = new StringBuilder();
while(position<text.length()){
char c = text.charAt(position);//对字符进行判断
//跳过特殊符号【敏感词中间加一些特殊符号】
if(isSysbol(c)){
//若指针1处于根节点,将此符号计入结果,让指针2向下走一步
if(tmpNode==rootNode){
sb.append(c);
begin++;
}
//无论符号在开头或者中间指针3都向下走一步
position++;
continue;
}
//检查下级节点
tmpNode = tmpNode.getSubNode(c);
if(tmpNode==null){
//以begin为开头的字符不是敏感词
sb.append(text.charAt(begin));
//进入下一个词的判断
position = ++begin;
tmpNode = rootNode;//重新指向根节点
}else if(tmpNode.isKeywordEnd()){//发现了敏感词
//发现敏感词以begin开头,position结尾的词
sb.append(REPLACEMENT);//替换敏感词
begin = ++position;//进入下一个词的判断
tmpNode = rootNode;
}else{//没检测完,还没到结束标识
//继续检查下一个字符
position++;
}
}
//将最后一批字符记录
sb.append(text.substring(begin));
return sb.toString();
}
//判读是否为特殊符号 私有,这个类自己用
private boolean isSysbol(Character c){
//c<0x2E80||c>0x9FFF 东亚文字之外
//特殊符号返回false,所以前面!
return !CharUtils.isAsciiAlphanumeric(c)&&(c<0x2E80||c>0x9FFF);
}
二、发布帖子
异步请求,当前网页不刷新,but访问服务器,服务器返回一些结果
通常是局部刷新,添加数据、更改样式等…
-
AJAX
- Asynchronous JavaScript and XML
- 异步的JavaScript与XML,不是一门新技术,只是一个新的术语。
- 使用AJAX,网页能够将增量更新呈现在页面上,而不需要刷新整个页面。
- 虽然X代表XML,但目前JSON的使用比XML更加普遍。【XML太复杂、不好解析】
- https://developer.mozilla.org/zh-CN/docs/Web/Guide/AJAX
-
示例
- 使用jQuery发送AJAX请求。
-
实践
- 采用AJAX请求,实现发布帖子的功能。
AJAX使用示例
1.导入Fastjson 处理JSON相关的操作
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.68</version>
</dependency>
2.在CommunityUtil类中写几个封装成Json的方法
给浏览器返回一个编码:0代表? 1代表?
给浏览器返回提示信息:成功,or 失败
返回一些业务数据
//共有的、静态的
//分装成json对象,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);
if(map!=null){
for(String key:map.keySet()){
json.put(key,map.get(key));
}
}
return json.toJSONString();//===》json格式字符串
}
//传入的参数不同 吧
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
@RequestMapping(path = "/ajax",method = RequestMethod.POST)
@ResponseBody
public String testAjax(String name,String 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>
<input type="button" value="发送" onclick="send();">
</body>
<!--引入jQuery-->
<script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script>
<script>
function send() {
$.post(
"/community/demo/ajax",
{"name":"张三","age":12},
function(data) {
console.log(typeof(data))
console.log(data)
## $.parseJson 表明将传来的data数据转变为JSon对象
data = $.parseJSON(data)
console.log(typeof(data))
console.log(data.code)
console.log(data.msg)
}
);
}
</script>
</html>
完成发布帖子功能
1.老规矩先写插入帖子的dao层
@Repository
public interface DiscussPostMapper {
/**
* @param userId 考虑查看我的帖子的情况下设置动态sql
* @param offset
* @param limit
* @return
*/
List<DiscussPost> selectDiscussPosts(int userId,int offset,int limit);
//如果需要动态拼接条件(<if>里使用)并且这个方法有且只有一个参数需要用@Param起别名
//@Param用于给参数取别名
int selectDiscussPostRows(@Param("userId") int userId);
int insertDiscussPost(DiscussPost discussPost);
}
2.写对应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>
3.写Service层在DiscussPostService类中
@Autowired
SensitiveFilter sensitiveFilter;
public int addDiscussPost(DiscussPost discussPost){
if(discussPost==null){
throw new IllegalArgumentException("参数不能为空");
}
//转义HTML标记,将内容视为普通文本
discussPost.setTitle(HtmlUtils.htmlEscape(discussPost.getTitle()));
discussPost.setContent(HtmlUtils.htmlEscape(discussPost.getContent()));
//过滤敏感词
discussPost.setTitle(sensitiveFilter.filter(discussPost.getTitle()));
discussPost.setContent(sensitiveFilter.filter(discussPost.getContent()));
return discussPostMapper.insertDiscussPost(discussPost);
}
4.controller层新建一个DiscussPostController类
@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){
//返回Json数据
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,"发布成功");
}
}
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.htm
- 在帖子标题上增加访问详情页面的链接
-
discuss-detail.html
- 处理静态资源的访问路径
- 复用index.html的header区域
- 显示标题、作者、发布时间、帖子正文等内容
1.DiscussPostMapper增加查询帖子详情
DiscussPost selectDiscussPostById(int id);
2.配置mapper.xml 返回的数据类型resultType
<select id="selectDiscussPostById" resultType="DiscussPost">
select <include refid="selectFields"></include>
from discuss_post
where id = #{id}
</select>
3.写service层
public DiscussPost findDiscussPostById(int id){
return discussPostMapper.selectDiscussPostById(id);
}
4.controller层
@Autowired
private UserService userService;
//请求路径、请求方法
@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);
//根据userId查名字
User user = userService.findUserById(post.getUserId());
model.addAttribute("user",user);
return "site/discuss-detail";
}
4.处理首页让每个帖子有个链接
5.处理discuss-detail页面
先改成Thymeleaf模板的格式
接着填充数据
四、事务管理
回顾
什么是事务
- 事务是由N步数据库操作序列组成的逻辑执行单元,这系列操作要么全执行,要么全放弃执行。
事务的特性(ACID)
- 原子性(Atomicity):事务是应用中不可再分的最小执行体。
- 一致性(Consistency):事务执行的结果,须使数据从一个一致性状态,变为另一个一致性状态。
- 隔离性(Isolation):各个事务的执行互不干扰,任何事务的内部操作对其他的事务都是隔离的。
- 持久性(Durability):事务一旦提交,对数据所做的任何改变都要记录到永久存储器中。
事务的隔离性
-
常见的并发异常
- 第一类丢失更新、第二类丢失更新。
- 脏读、不可重复读、幻读。
- 第一类丢失更新、第二类丢失更新。
-
常见的隔离级别
- Read Uncommitted:读取未提交的数据。
- Read Committed:读取已提交的数据。
- Repeatable Read:可重复读。
- Serializable:串行化【级别高、降低数据库性能】
第一类的丢失更新
某一个事务的回滚,导致另外一个事务已更新的数据丢失了。
第二类丢失更新
某一个事务的提交,导致另外一个事务已更新的数据丢失了。
脏读
某一个事务,读取了另外一个事务未提交的数据。
不可重复读
某一个事务,对同一个数据前后读取的结果不一致。
幻读
某一个事务,对同一个表前后查询到的行数不一致。
事务隔离级别
实现机制
-
悲观锁(数据库)
- 共享锁(S锁)
事务A对某数据加了共享锁后,其他事务只能对该数据加共享锁,但不能加排他锁。 - 排他锁(X锁)
事务A对某数据加了排他锁后,其他事务对该数据既不能加共享锁,也不能加排他锁。
- 共享锁(S锁)
-
乐观锁(自定义)
- 版本号、时间戳等
在更新数据前,检查版本号是否发生变化。若变化则取消本次更新,否则就更新数据(版本号+1)。
- 版本号、时间戳等
Spring事务管理 Spring data access
Spring对任何数据库进行管理时都是透明的,一套API就可以管理数据库事务
-
声明式事务
- 通过XML配置,声明某方法的事务特征。
- 通过注解,声明某方法的事务特征。
-
编程式事务【业务复杂、控制局部事务】
- 通过 TransactionTemplate 管理事务,并通过它执行数据库的操作。
演示声明式事务
1.在AlphaService中写一个新方法加@Transaction注解
/**
* 传播机制--两个不同的业务都有可能有不同隔离级别且可能一个业务使用了另一个业务,
* 传播机制就是解决不同隔离隔离级别同时出现的情况。
* Propagation.REQUIRED:支持当前事务,就是调用者事务,如果不存在那就创建新事务
* Propagation.REQUIRES_NEW:创建一个事务,并且暂停当前事务(外部事务)
* Propagation.NESTED:如果存在外部事务,那么就会嵌套在外部事务之中,A调B,B有独立提交和回滚的能力
* 否则和REQUIRED一样。
*/ //参数:隔离级别、传播机制
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
public Object save1(){
//新增用户
User user = new User();
user.setUsername("hsw");
user.setSalt(CommunityUtil.generateUUID().substring(0,5));
user.setPassword(CommunityUtil.md5("123"+user.getSalt()));
user.setEmail("hsw@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);
int i = 1/0;
return "ok";
}
2.写个测试方法调用这个方法,发现数据库中并没有插入任何数据
演示编程式事务
1.在加一个方法
@Autowired
private TransactionTemplate transactionTemplate;//注入bean
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("hsw");
user.setSalt(CommunityUtil.generateUUID().substring(0,5));
user.setPassword(CommunityUtil.md5("123"+user.getSalt()));
user.setEmail("hsw@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);
int i = 1/0;
return "ok";
}
});
}
2.测试发现也没有插入数据
五、显示评论
-
数据层
- 根据实体查询一页评论数据。
- 根据实体查询评论的数量。
-
业务层
- 处理查询评论的业务。
- 处理查询评论数量的业务。
-
表现层
- 显示帖子详情数据时,
- 同时显示该帖子所有的评论数据。
根据评论的数据库我们创建一套实体类
id表明这个评论发出的早晚顺序
user_id 表明这个评论发出的用户
entity_type 表明这个评论的类型(是属于帖子的评论,还是评论的评论,还是问题的评论)
entity_id 表明这个评论的帖子是哪一个
**target_id **表明这个帖子所指向的地址【指向某个人】
content 表明的是的是帖子的内容
status 表明的是这个评论的状态,表明状态 0为正常的 1为删除的或者是错误的
create_time 表明的是这个帖子创立的时间
数据层
1.写个实体类
同时生成get、set方法
写一个toString方法
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类 【支持分页】
@Repository
public interface CommentMapper {
List<Comment> selectCommentByEntity(int entityType,int entityId,int offset,int limit);//实体条件、分页条件
int selectCountByEntity(int entityType,int entityId);//评论的总数
}
3.写对应Mapper.xml Mapper映射方法
<?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.hsw.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"> <!--id、返回的类型-->
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"> <!--id、返回的类型-->
select count(id)
from comment
where status = 0
and entity_type= #{entityType}
and entity_id=#{entityId}
</select>
</mapper>
别忘了写完测试一波
业务层
很简单
@Service
public class CommentService {
@Autowired
private CommentMapper commentMapper; //注入Mapper
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);
}
}
表现层
- controller:处理请求
- 页面:展现数据
1.直接在DiscussPostController中加写内容
@Autowired
private UserService userService;
@Autowired
private CommentService commentService;
@RequestMapping(path = "/detail/{discussPostId}",method = RequestMethod.GET)
//如果参数中有bean,最终springmvc都会存在model中,所以Page会存到model中
public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model, Page page){ //如果参数中有bean,最终springmvc都会存在model中
//查询这个帖子
DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
model.addAttribute("post",post);
//根据userId查名字
User user = userService.findUserById(post.getUserId());
model.addAttribute("user",user);
//查评论的分页信息
page.setLimit(5);//每页显示5条数据
page.setPath("/discuss/detail/"+discussPostId);//路径
page.setRows(post.getCommentCount()); //帖子相关字段中冗余存了一个commentcount 也可以从评论表查询【效率低】
//帖子的评论:称为--评论 ENTITY_TYPE_POST
//评论的评论:称为--回复 ENTITY_TYPE_REPLY
//评论列表
List<Comment> comments = commentService.findCommentsByEntity(
ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit());
//用map对我们要展示的数据 做统一的封装
List<Map<String,Object>> commentVoList = new ArrayList<>();
if(comments!=null){
for(Comment c:comments){
//评论Vo :Vo的意思是viewObject的意思 视图对象
Map<String,Object> commentVo = new HashMap<>();
//放评论
commentVo.put("comment",c);
//放作者
commentVo.put("user",userService.findUserById(c.getUserId()));
//回复列表
List<Comment> replys = commentService.findCommentsByEntity(ENTITY_TYPE_REPLY, c.getId(), 0, Integer.MAX_VALUE);//不分页了
//回复的Vo列表
List<Map<String,Object>> replyVoList = new ArrayList<>();
if(replys!=null){
for(Comment r:replys){
Map<String,Object> replyVo = new HashMap<>();
//放回复
replyVo.put("reply","r");
//放回复者user
replyVo.put("user",userService.findUserById(r.getUserId()));
//放被回复者,如果有的话
User target = r.getTargetId() == 0 ? null : userService.findUserById(r.getTargetId());
replyVo.put("target",target);
replyVoList.add(replyVo);
}
}
//回复加入进来
commentVo.put("replys",replyVoList);
//一条评论回复的数量
int replyCount = commentService.findCommentCount(ENTITY_COMMENT, c.getId());
commentVo.put("replyCount",replyCount);
//加入评论Vo
commentVoList.add(commentVo);
}
}
//传给模板
model.addAttribute("comments",commentVoList);
return "site/discuss-detail";
}
2.处理模板
还有一堆慢慢写就没事
遍历帖子
遍历回复
3.分页处理直接复用index页面的分页就可
因为都一样并且controller中查询恢复帖子的列表也是根据当前页面offset和limit查的
六、添加评论
增加评论,注意事务管理
-
数据层
- 增加评论数据。
- 修改帖子的评论数量。
-
业务层
- 处理添加评论的业务:
先增加评论、再更新帖子的评论数量。【两次DML操作】
- 处理添加评论的业务:
-
表现层
- 处理添加评论数据的请求。
- 设置添加评论的表单。
数据层
1.增加插入评论的mapper
2.增加帖子回复数量的mapper
业务层
1.DiscussPostService
2.CommentService
@Autowired
private DiscussPostService discussPostService;
@Autowired
private SensitiveFilter sensitiveFilter;
//声明式事务 加注解(隔离级别、传播机制)
@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()== CommunityContant.ENTITY_TYPE_POST){
int count = commentMapper.selectCountByEntity(CommunityContant.ENTITY_TYPE_POST, comment.getEntityId());
discussPostService.updateCommentCount(comment.getEntityId(),count);
}
return rows;
}
表现层
1.单独创建一个Controller—CommentController
@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可能有这么几种方法会在展示层上用到,我们可以先写。首先是我们在会话列表中需要展示最新的私信,我们还需要得出总的会话行数,进行分页的操作。第二,我们点进私信详情时,展示与之对话的私信,并且也将分页功能实现
表设计
数据层
1.写entity
2.写mapper
@Repository
public interface MessageMapper {
<!--查询当前用户的会话列表,针对每个会话只返回一条最新的私信-->
List<Message> selectConversation(int userId, int offset, int limit);
<!--查询当前用户的会话数量-->
int selectConversationCount(int userId);
<!--查询某个会话所包含的私信列表-->
List<Message> selectLetter(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.hsw.community.dao.MessageMapper">
<sql id="selectFields">
id, from_id, to_id, conversation_id, content, status, create_time
</sql>
<select id="selectConversation" 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 create_time 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="selectLetter" resultType="Message">
select <include refid="selectFields"></include>
from message
where status!=2
and from_id!=1
and conversation_id=#{conversationId}
order by create_time 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>
业务层
新建个MessageService即可
@Service
public class MessageService {
@Autowired
private MessageMapper messageMapper;
public List<Message> findConversations(int userId, int offset, int limit) {
return messageMapper.selectConversation(userId, offset, limit);
}
public int findConversationCount(int userId) {
return messageMapper.selectConversationCount(userId);
}
public List<Message> findLetters(String conversationId, int offset, int limit) {
return messageMapper.selectLetter(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
@Controller
public class MessageController {
@Autowired
private HostHolder hostHolder;
@Autowired
private MessageService messageService;
@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()));
//拿到对话对方的userId
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";
}
}
2.处理视图
复用首页的分页逻辑
八、私信之私信详情
- 私信详情
-
- 查询某个会话所包含的私信。
- 支持分页显示。
表现层
1.新增controller方法
@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.处理页面
声明ttymeleaf模板
修改静态变量,js、css
header用首页header替换、分页逻辑利用首页逻辑
私信详情返回后处理已读消息个数
解决方法
九、发送私信
- 发送私信
- 采用异步的方式发送私信。
- 发送成功后刷新私信列表。
数据层
业务层
读取消息,将状态改为已读。【可以传入多个id,定义为一个集合】
表现层
1.controller中
@RequestMapping(path = "/letter/send",method = RequestMethod.POST)
@ResponseBody
public String sendLetter(String toName,String content){
//System.out.println(toName+content);
User user = userService.selectByName(toName);//将name==》id
if(user==null){
return CommunityUtil.getJsonString(1,"目标用户不存在");
}
Message message = new Message();
message.setFromId(hostHolder.getUser().getId());
message.setToId(user.getId());
//拼接conversationId,谁小谁在前面
String conversationId = message.setFromId()<message.setToId()?
message.setFromId()+"_"+message.setToId():
message.setToId()+"_"+message.setFromId();
message.setConversationId(conversationId);
message.setContent(content);
message.setCreateTime(new Date());
message.setStatus(0);
messageService.addMessage(message);
return CommunityUtil.getJsonString(0);
}
2.letter.html页面中
letter.js
$(function(){
$("#sendBtn").click(send_letter);
$(".close").click(delete_msg);
});
function send_letter() {
//①关掉输入框 ②向服务端发送数据 ③提示框【发送成功or失败】
$("#sendModal").modal("hide");
var toName = $("#recipient-name").val();
var content = $("#message-text").val();
$.post(
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);
}
);
}
已读消息处理
Messagecontroller
在请求详情的方法中,更改状态,设置为已读
十、统一处理异常
-
@ControllerAdvice
- 用于修饰类,表示该类是Controller的全局配置类。
- 在此类中,可以对Controller进行如下三种全局配置:
异常处理方案、绑定数据方案、绑定参数方案。
-
@ExceptionHandler—异常处理方案
- 用于修饰方法,该方法会在Controller出现异常后被调用,用于处理捕获到的异常。
- 利用其统一处理,所有controller可能发生的异常
-
@ModelAttribute—绑定数据方案(想象下Page类被自动封装进Model里)
- 用于修饰方法,该方法会在Controller方法执行前被调用,用于为Model对象绑定参数。
- 给Model绑定一个统一的参数、供所有controller
-
@DataBinder—绑定参数方案(想象下Page类的使用)
往上抛最终处理异常在表现层,统一处理异常针对表现层,Spring还有SpringBoot给我们提供了现成的方案
SpringBoot自动处理方式
1.把报错的错误码作为页面名放到如下目录下,当报出来相关错误会自动显示报错的页面。
@ControllerAdvice和@ExceptionHandler处理异常
1.写一个跳转到处理页面的controller,这里在HomeController里写
@RequestMapping(path = "/error",method = RequestMethod.GET)
public String getErrorPage(){
return "/error/500";
}
2.在controller包下新建advice包并创建处理异常类
@ControllerAdvice(annotations = Controller.class)
//只扫描带有controller注解的bean
public class HandleException {
private static final Logger logger = LoggerFactory.getLogger(HandleException.class);
@ExceptionHandler({Exception.class})//处理所有异常的注解
//public voic 修饰 ,参数可以传进去很多可以查资料
public void handleException(Exception e,
HttpServletRequest request,
HttpServletResponse response) throws IOException {
logger.error("服务器发生异常 "+e.getMessage());
for(StackTraceElement element:e.getStackTrace()){
logger.error(element.toString());
}
//判断是html请求还是json请求,学习这个找法
String xRequestWith = request.getHeader("x-requested-with");
if("XMLHttpRequest".equals(xRequestWith)){
//异步请求返回XML、普通的请求返回的是html
//response.setContentType("application/json"); //浏览器会自动转成json数据
//浏览器会自动返回普通字符串
response.setContentType("application/plain;charSet=utf-8");
PrintWriter writer = response.getWriter();
writer.write(CommunityUtil.getJsonString(1,"服务器异常"));
}else{
response.sendRedirect(request.getContextPath()+"/error");
}
}
}
很方便,不需要每个页面加代码
十一、统一记录日志
控制通知:访问发生异常时,进行统一的异常处理
拦截器:也是针对控制器进行处理
记日志可能针对业务组件、数据访问层、控制器等进行日志记录
【记录日志属于系统需求,尽量不要在业务需求里耦合系统需求,一旦需求发生变化,改变非常大】
需求:对所有的service记录日志
AOP
AOP的概念
- Aspect Oriented Programing,即面向方面(切面)编程。
- AOP是一种编程思想,是对OOP的补充,可以进一步提高编程的效率。【AOP和OOP互补】
AOP的术语
- Target:已处理完业务逻辑的代码为目标对象【一个个业务组件称之为处理需求的目标对象】
- Aspect:封装业务需求的组件称之为aspect,即切面
- 【整个编程过程完全针对aspect进行编程,所以称之为面向切面编程】
- Weaving:将封装的代码织入到目标对象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类
@Component
@Aspect
public class AlphaAspect {
//* 代表方法的返回值 com.hsw.community.service包名 .*.所有类 *(..)所有方法以及所有的参数
@Pointcut("execution(* com.hsw.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("aroundBefore");
Object obj = joinPoint.proceed();//调用目标组件的方法
System.out.println("aroundAfter");
return obj;
}
}
正式处理业务
访问任意业务组件方法时,日志通知方法都被触发了,统一处理记录日志记录
AOP容易掌握、but AOP理念、机制、术语、概念多理解一下。。。
@Component
@Aspect
public class ServiceLoggerAspect {
private static final Logger logger = LoggerFactory.getLogger(ServiceLoggerAspect.class);//实例化Logger
@Pointcut("execution(* com.hsw.community.service.*.*(..))")//声明切点
public void pointcut(){
}
@Before("pointcut()")//前置通知
public void before(JoinPoint joinPoint){
//用户{1.2.3.4},在{xxxx},访问了{com.hsw.community.service.xxx}
//RequestContextHolder工具类 工具类==》子类型,得到的方法更多一些
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();//得到request对象
String ip = request.getRemoteHost();//用户ip地址
String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());//时间
String TypeName = joinPoint.getSignature().getDeclaringTypeName(); //类名
String methodName = joinPoint.getSignature().getName();//方法名
String target = TypeName+"."+methodName;
logger.info(String.format("用户[%s],在[%s],访问了[%s]",ip,now,target));
}
}
输出:
...
用户[127.0.0.1],在[2020-05-18 22:27:06],访问了
[com.hsw.community.service.UserService.findUserById]
...
仿牛客网第四章
一、Redis入门
Redis,一站式高性能存储方案,提升性能
支持数据类型多、性能完善、使用简单
-
Redis是一款基于键值对的NoSQL数据库,它的值支持多种数据结构:
字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等。 -
Redis将所有的数据都存放在内存中,所以它的读写性能十分惊人。
同时,Redis还可以将内存中的数据以快照或日志的形式保存到硬盘上,以保证数据的安全性。【快照即RDB形式,将内容原原本本的直接存在硬盘上。优点:体积小、恢复快 缺点:耗时、做存储时可能会产生阻塞 不适合实时场景、适合几个小时做一次的场景】
【日志形式AOF:每执行一次,将以日志形式存Redis命令 优点:速度快,可以实时存 缺点:追加的形式,体积大,占用磁盘空间;恢复慢】
-
Redis典型的应用场景包括:缓存、排行榜、计数器、社交网络【点赞】、消息队列等。【消息队列可以用更专业的服务器,kafak】
各个数据类型应用场景:
类型 | 简介 | 特性 | 场景 |
---|---|---|---|
String(字符串) | 二进制安全 | 可以包含任何数据,比如jpg图片或者序列化的对象,一个键最大能存储512M | — |
Hash(字典) | 键值对集合,即编程语言中的Map类型 | 适合存储对象,并且可以像数据库中update一个属性一样只修改某一项属性值(Memcached中需要取出整个字符串反序列化成对象修改完再序列化存回去) | 存储、读取、修改用户属性 |
List(列表) | 链表(双向链表) | 增删快,提供了操作某一段元素的API | 1,最新消息排行等功能(比如朋友圈的时间线) 2,消息队列 |
Set(集合) | 哈希表实现,元素不重复 | 1、添加、删除,查找的复杂度都是O(1) 2、为集合提供了求交集、并集、差集等操作 | 1、共同好友 2、利用唯一性,统计访问网站的所有独立ip 3、好友推荐时,根据tag求交集,大于某个阈值就可以推荐 |
Sorted Set(有序集合) | 将Set中的元素增加一个权重参数score,元素按score有序排列 | 数据插入集合时,已经进行天然排序 | 1、排行榜 2、带权重的消息队列 |
Redis使用演示
1.reids默认有16个库,从0-15,可以使用下边的语句切换
select [index]
2.刷新数据库FLUSHDB命令,如下
3.String类型的演示
set key value [EX seconds] [PX milliseconds] [NX|XX]
多个单词,用:连接
get key
incr key //值加一
decr key //值减一
4.hash类型数据
hset key field value
hget key field
5.list类型数据【存、取 左右都可以】
l表示左边
r表示右边
lpush key value [value ...]
llen key //看列表的长度
lindex key index //查询某个索引对应的值
lrange key start stop //查询索引范围内对应的值
lpop key
rpush key value [value ...]
rpop key
6.set类型数据 无序、不重复
sadd key member [member ...]
scard key //统计集合中有多少元素,输出个数
spop key [count] //随机从集合中弹出数据【可以实现抽奖】
smembers key //相比于scard来说,输出set中所有的member
7.sortset类型数据 有序集合
zadd key [NX|XX] [CH] [INCR] score member [score member ...]
zcard key
zscore key member //查询某个值的分数
zrank key member //返回排名
zrange key start stop [WITHSCORES] //取某个返回内的排名
8.常用命令
keys * //查询当前库里所有的key
type key //查询某个key值的类型
exists key [key ...] //查询某个key是否存在
del key //删除某个key的数据
expire key seconds //设置某个key的过期时间
二、 Spring整合Redis
-
引入依赖
- spring-boot-starter-data-redis
-
配置Redis
-
配置数据库参数
-
编写配置类,构造RedisTemplate【Redis核心组件,Spring提供】
SpringBoot已经自动配置了该类
but SpringBoot中的key定义为Object类型,我们常用的key是String类型,因此,我们需要重新配置
-
-
访问Redis
- redisTemplate.opsForValue() String
- redisTemplate.opsForHash() Hash
- redisTemplate.opsForList() List
- redisTemplate.opsForSet() Set
- redisTemplate.opsForZSet() Zset
导包
日常操作你懂得
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
配置Redis
application.properties中
- database=11表示使用第12个库
因为RedisTemplate是<String,Object>类型的不利于我们操作所以要写一个配置类
RedisConfig 配置类
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
//访问数据库需要建立连接、连接是由连接工厂创建的,因此注入连接工厂
RedisTemplate<String,Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory); //将工厂设置2给Template
//Template有了工厂,具备访问数据库的能力
//设置key的序列化方式
template.setKeySerializer(RedisSerializer.string());
//设置value的序列化方式
template.setValueSerializer(RedisSerializer.json());
//设置hash的key序列化方式
template.setHashKeySerializer(RedisSerializer.string());
//设置hash的value的序列化方式
template.setHashValueSerializer(RedisSerializer.json());
template.afterPropertiesSet();
return template;
}
}
RedisTemplate访问redis
在Redis事务执行过程中不要进行查询,否则会返回空数据!
1.写一个测试类
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class RedisTest {
@Autowired
private RedisTemplate redisTemplate;
@Test
public void testStrings() {
String redisKey = "test:count";
redisTemplate.opsForValue().set(redisKey, 1);
System.out.println(redisTemplate.opsForValue().get(redisKey));
System.out.println(redisTemplate.opsForValue().increment(redisKey));
System.out.println(redisTemplate.opsForValue().decrement(redisKey));
}
@Test
public void testHashes() {
String redisKey = "test:user";
redisTemplate.opsForHash().put(redisKey, "id", 1);
redisTemplate.opsForHash().put(redisKey, "username", "zhangsan");
System.out.println(redisTemplate.opsForHash().get(redisKey, "id"));
System.out.println(redisTemplate.opsForHash().get(redisKey, "username"));
}
@Test
public void testLists() {
String redisKey = "test:ids";
redisTemplate.opsForList().leftPush(redisKey, 101);
redisTemplate.opsForList().leftPush(redisKey, 102);
redisTemplate.opsForList().leftPush(redisKey, 103);
System.out.println(redisTemplate.opsForList().size(redisKey));
System.out.println(redisTemplate.opsForList().index(redisKey, 0));
System.out.println(redisTemplate.opsForList().range(redisKey, 0, 2));
System.out.println(redisTemplate.opsForList().leftPop(redisKey));
System.out.println(redisTemplate.opsForList().leftPop(redisKey));
System.out.println(redisTemplate.opsForList().leftPop(redisKey));
}
@Test
public void testSets() {
String redisKey = "test:teachers";
redisTemplate.opsForSet().add(redisKey, "刘备", "关羽", "张飞", "赵云", "诸葛亮");
System.out.println(redisTemplate.opsForSet().size(redisKey));
System.out.println(redisTemplate.opsForSet().pop(redisKey));
System.out.println(redisTemplate.opsForSet().members(redisKey));
}
@Test
public void testSortedSets() {
String redisKey = "test:students";
redisTemplate.opsForZSet().add(redisKey, "唐僧", 80);
redisTemplate.opsForZSet().add(redisKey, "悟空", 90);
redisTemplate.opsForZSet().add(redisKey, "八戒", 50);
redisTemplate.opsForZSet().add(redisKey, "沙僧", 70);
redisTemplate.opsForZSet().add(redisKey, "白龙马", 60);
System.out.println(redisTemplate.opsForZSet().zCard(redisKey));
System.out.println(redisTemplate.opsForZSet().score(redisKey, "八戒"));
System.out.println(redisTemplate.opsForZSet().reverseRank(redisKey, "八戒"));
System.out.println(redisTemplate.opsForZSet().reverseRange(redisKey, 0, 2));
}
@Test //常用命令
public void testKeys() {
redisTemplate.delete("test:user");
System.out.println(redisTemplate.hasKey("test:user"));
redisTemplate.expire("test:students", 10, TimeUnit.SECONDS);
}
// 多次访问同一个key 简化方案,绑定key
@Test
public void testBoundOperations() {
String redisKey = "test:count";
BoundValueOperations operations = redisTemplate.boundValueOps(redisKey);
operations.increment();
operations.increment();
operations.increment();
operations.increment();
operations.increment();
System.out.println(operations.get());
}
//声明式事务作用于整个方法,方法中需要查询时就不合适了,所以演示编程式事务
// 编程式事务 NoSQL数据库,不必须满足ACID,实现简单
//启用事务之后,不会立刻执行命令,将命令放到队列,直至提交事务时,将队列==》Redis中,一起执行
@Test
public void testTransactional() {
Object obj = redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
String redisKey = "test:tx";
operations.multi();
operations.opsForSet().add(redisKey, "zhangsan");
operations.opsForSet().add(redisKey, "lisi");
operations.opsForSet().add(redisKey, "wangwu");
//显示为[],所以一定不要在redis事务中做查询。
System.out.println(operations.opsForSet().members(redisKey));
return operations.exec();
}
});
//这里会显示所有的操作结果
System.out.println(obj);
}
}
三、点赞
重点考虑性能问题,可能很多人同时给同一个人点赞
因此,将点赞数据存在Redis中
- 点赞
- 支持对帖子、评论点赞。
- 第1次点赞,第2次取消点赞。
- 首页点赞数量
- 统计帖子的点赞数量。
- 详情页点赞数量
- 统计点赞数量。
- 显示点赞状态。
个人想法
因为我们是通过redis进行点赞的功能上线,所以我们在这一次可以不使用dao层来进行功能的书写。
存在Redis中,操作非常简单,因此不单独写数据访问层
Redis生成key的工具类
简单工具、方便复用,不需要容器进行管理
public class RedisUtil {
private static final String SPLIT=":";
private static final String PREFIX_ENTITY_LIKE = "like:entity";
//某个实体的赞
//like:entity:entityType:entityId 由方法参数传来的参数组成的key值
//value是个set:存的是用户id,为了统计谁给我点了赞
public static String getEntityLikeKey(int entityType,int entityId){
return PREFIX_ENTITY_LIKE+SPLIT+entityType+SPLIT+entityId;
}
}
业务层
@Service
public class LikeService {
@Autowired //注入RedisTemplate
private RedisTemplate redisTemplate;
//点赞
public void like(int userId,int entityType,int entityId){
String entityLikeKey = RedisUtil.getEntityLikeKey(entityType,entityId);//key
//第一次点是点赞,第二次点是取消赞
//先判断是否点过赞 value是set集合存的是userId
Boolean isMember = redisTemplate.opsForSet().isMember(entityLikeKey, userId);
if(isMember){
//说明点过赞,这次是取消赞
redisTemplate.opsForSet().remove(entityLikeKey,userId);
}else{
//说明是第一次点赞
redisTemplate.opsForSet().add(entityLikeKey,userId);
}
}
//查询实体点赞的数量
public long findEntityLikeCount(int entityType,int entityId){
String entityLikeKey = RedisUtil.getEntityLikeKey(entityType,entityId);
return redisTemplate.opsForSet().size(entityLikeKey);
}
//查询某人对某实体的点赞状态
//返回int 是为了以后业务扩展 比如点了踩啥的记录状态
public int findEntityLikeStatus(int userId,int entityType,int entityId){
String entityLikeKey = RedisUtil.getEntityLikeKey(entityType,entityId);
return redisTemplate.opsForSet().isMember(entityLikeKey,userId)?1:0;
}
}
表现层:点赞功能的实现
异步请求,不刷新页面,for 高效率
后面用拦截器拦截请求,使用SpringSecrety进行重构,检查权限【是否登录】
@Controller
public class LikeController {
@Autowired
private LikeService likeService;
@Autowired
private HostHolder hostHolder;
@RequestMapping(path = "/like",method = RequestMethod.POST)
@ResponseBody
public String like(int entityType,int entityId){
User user = hostHolder.getUser();
//实现点赞
likeService.like(user.getId(),entityType,entityId);
//统计数量
long likeCount = likeService.findEntityLikeCount(entityType,entityId);
//状态
int likeStatus = likeService.findEntityLikeStatus(user.getId(),entityType,entityId);
//返回的结果
Map<String,Object> map = new HashMap<>();
map.put("likeCount",likeCount);
map.put("likeStatus",likeStatus);
return CommunityUtil.getJsonString(0,null,map); //正确、无提示、点赞
}
}
处理页面
1.帖子点赞
- href的东西表名跳转的地方是找js的方法
- onclick里的like方法有三个参数
- this用来标志是哪里点的赞(帖子可以点赞,用户评论也可以点赞)
- 1表明是给帖子点赞
- post.id是帖子的id号
- 为了方便显示数据把 赞 这个字用< b >标签包围,11用< i >包围
2.评论点赞
- 2表明给帖子评论点赞,其他同理
3.评论的回复点赞
4.处理js
- 新建一个discuss.js
function like(btn,entityType,entityId) {
$.post(
CONTEXT_PATH+"/like",
{"entityType":entityType,"entityId":entityId},
function (data) {
//转化成json
data = $.parseJSON(data);
if(data.code==0){
$(btn).children("i").text(data.likeCount);
$(btn).children("b").text(data.likeStatus==1?"已赞":"赞");
}else{
alert(data.msg);//失败,通过控制器通知统一处理
}
}
);
}
表现层:点赞数量的显示
1.首页上点赞数量的显示
HomeController中getIndexPage方法
页面处理
四、我收到的赞
- 重构点赞功能
- 以用户为key,记录点赞数量
- increment(key),decrement(key)
- 开发个人主页
- 以用户为key,查询点赞数量
重构点赞功能
1.在RedisUtil中加一个key
2.LikeService增加一个操作记录用户获得点赞的数量
并用编程式事务完成。想想为啥不用声明式事务—代码中有答案
//点赞
public void like(int userId,int entityType,int entityId,int entityUserId){ //entityUserId 就是被点赞的user的Id
/* String entityLikeKey = RedisUtil.getEntityLikeKey(entityType,entityId);
//第一次点是点赞,第二次点是取消赞
//先判断是否点过赞 value是set集合存的是userId
Boolean isMember = redisTemplate.opsForSet().isMember(entityLikeKey, userId);
if(isMember){
//说明点过赞,这次是取消赞
redisTemplate.opsForSet().remove(entityLikeKey,userId);
}else{
//说明是第一次点赞
redisTemplate.opsForSet().add(entityLikeKey,userId);
}*/
//编程式事务
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
String entityLikeKey = RedisUtil.getEntityLikeKey(entityType,entityId);//以实体为key
String userLikeKey = RedisUtil.getUserLikeKey(entityUserId);//以用户为key
//判断当前用户有没有点赞,这一步应该在事务开启前执行,因为在事务中的查询不会立即得到结果
Boolean isMember = redisOperations.opsForSet().isMember(entityLikeKey, userId);
//事务开启
redisOperations.multi();
if(isMember){
//说明点过赞,这次是取消赞
redisTemplate.opsForSet().remove(entityLikeKey,userId);
//被点赞的用户点赞数量减一
redisOperations.opsForValue().decrement(userLikeKey);
}else{
//说明是第一次点赞
redisTemplate.opsForSet().add(entityLikeKey,userId);
//被点赞的用户点赞数量加一
redisOperations.opsForValue().increment(userLikeKey);
}
return redisOperations.exec();
}
});
}
3.LikeService增加查询某个用户获得赞的数量
4.重构表现层
detail页面
js
开发个人主页
1.UserController增加找用户页面的方法
@Autowired
private LikeService likeService;
//个人主页
@RequestMapping(path = "/profile/{userId}",method = RequestMethod.GET)
public String getProfilePage(@PathVariable("userId")int userId,Model model){
User user = userService.findUserById(userId);
if(user==null){
throw new RuntimeException("该用户不存在");
}
//用户
model.addAttribute("user",user);
int likeCount = likeService.findUserLikeCount(user.getId());
model.addAttribute("likeCount",likeCount);
return "/site/profile";
}
2.处理index.html中的链接
所有用户头像也得加链接
这里只处理了首页上的,其他的暂时没有处理。
五、关注、取消关注
- 需求
-
- 开发关注、取消关注功能。
- 统计用户的关注数、粉丝数。
- 关键
-
- 若A关注了B,则A是B的Follower(粉丝),B是A的Followee(目标)。
- 关注的目标可以是用户、帖子、题目等,在实现时将这些目标抽象为实体。
RedisUtil中增加获得key的方法
业务层
@Service
public class FollowService {
@Autowired
private RedisTemplate redisTemplate;
//关注
public void follow(int userId,int entityType,int entityId){
//还得依靠事务解决,因为有多次操作
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
String followee = RedisUtil.getFolloweeKey(userId,entityType);
String follower = RedisUtil.getFollowerKey(entityType,entityId);
//启用事务
redisOperations.multi();
//userId关注entityId
redisOperations.opsForZSet().add(followee,entityId,System.currentTimeMillis());
//entityId的粉丝是userId
redisOperations.opsForZSet().add(follower,userId,System.currentTimeMillis());
return redisOperations.exec();
}
});
}
//取消关注
public void unFollow(int userId,int entityType,int entityId){
//还得依靠事务解决,因为有多次操作
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
String followee = RedisUtil.getFolloweeKey(userId,entityType);
String follower = RedisUtil.getFollowerKey(entityType,entityId);
//启用事务
redisOperations.multi();
//userId没有关注谁
redisOperations.opsForZSet().remove(followee,entityId);
//谁的粉丝没有userId
redisOperations.opsForZSet().remove(follower,userId);
return redisOperations.exec();
}
});
}
//查询关注的实体的数量
public long findFolloweeCount(int userId,int entityType){
String followee = RedisUtil.getFolloweeKey(userId,entityType);
return redisTemplate.opsForZSet().zCard(followee);
}
//查询实体的粉丝数量
public long findFollowerCount(int entityType,int entityId){
String follower = RedisUtil.getFollowerKey(entityType,entityId);
return redisTemplate.opsForZSet().zCard(follower);
}
//查询当前用户是否关注该实体
public boolean hasFollowed(int userId,int entityType,int entityId){
String followee = RedisUtil.getFolloweeKey(userId,entityType);
return redisTemplate.opsForZSet().score(followee,entityId)!=null?true:false;
}
}
表现层
FollowController
@Controller
public class FollowController {
@Autowired
private FollowService followService;
@Autowired
private HostHolder hostHolder;
@RequestMapping(path = "/follow",method = RequestMethod.POST)
@ResponseBody
public String follow(int entityType,int entityId){
User user = hostHolder.getUser();
if(user==null){
throw new RuntimeException("用户没有登录");
}
followService.follow(user.getId(),entityType,entityId);
return CommunityUtil.getJsonString(0,"已关注");
}
@RequestMapping(path = "/unfollow",method = RequestMethod.POST)
@ResponseBody
public String unfollow(int entityType,int entityId){
User user = hostHolder.getUser();
if(user==null){
throw new RuntimeException("用户没有登录");
}
followService.unFollow(user.getId(),entityType,entityId);
return CommunityUtil.getJsonString(0,"已取消关注");
}
}
UserController中如下方法增加功能
页面处理
因为各种地方都可以关注,此处只演示在用户主页关注人,其他大同小异
在profile.js中写处理逻辑
$(function(){
$(".follow-btn").click(follow);
});
function follow() {
var btn = this;
if($(btn).hasClass("btn-info")) {
// 关注TA
$.post(
CONTEXT_PATH+"/follow",
{"entityType":3,"entityId":$(btn).prev().val()},
function (data) {
data = $.parseJSON(data);
if(data.code==0){
window.location.reload();
}else{
alert(data.msg)
}
}
)
} else {
// 取消关注
$.post(
CONTEXT_PATH+"/unfollow",
{"entityType":3,"entityId":$(btn).prev().val()},
function (data) {
data = $.parseJSON(data);
if(data.code==0){
window.location.reload();
}else{
alert(data.msg)
}
}
)
//$(btn).text("关注TA").removeClass("btn-secondary").addClass("btn-info");
}
}
继续修改profile.html处理显示
六、关注列表、粉丝列表
- 业务层
-
- 查询某个用户关注的人,支持分页。
- 查询某个用户的粉丝,支持分页。
- 表现层
-
- 处理“查询关注的人”、“查询粉丝”请求。
- 编写“查询关注的人”、“查询粉丝”模板。
业务层
FollowService中增加方法
@Autowired
private UserService userService;
//查询某个用户关注的人
public List<Map<String,Object>> findFollowees(int userId,int offset,int limit){
String followee = RedisUtil.getFolloweeKey(userId,ENTITY_USER);
Set<Integer> targetIds = redisTemplate.opsForZSet().reverseRange(followee, offset, offset + limit - 1);
if(targetIds==null){
return null;
}
List<Map<String,Object>> list = new ArrayList<>();
for(Integer id:targetIds){
Map<String,Object> map = new HashMap<>();
User user = userService.findUserById(id);
//用户
map.put("user",user);
Double score = redisTemplate.opsForZSet().score(followee, id);
//关注时间
map.put("followTime",new Date(score.longValue()));
list.add(map);
}
return list;
}
//查询某个用户的粉丝
public List<Map<String,Object>> findFollowers(int userId,int offset,int limit){
String follower = RedisUtil.getFollowerKey(ENTITY_USER,userId);
//虽然返回的是set但是是redis内置实现了一个set可以有序排列
Set<Integer> targetIds = redisTemplate.opsForZSet().reverseRange(follower, offset, offset + limit - 1);
if(targetIds==null){
return null;
}
List<Map<String,Object>> list = new ArrayList<>();
for(Integer id:targetIds){
Map<String,Object> map = new HashMap<>();
User user = userService.findUserById(id);
//用户
map.put("user",user);
Double score = redisTemplate.opsForZSet().score(follower, id);
//关注时间
map.put("followTime",new Date(score.longValue()));
list.add(map);
}
return list;
}
表现层
1.某个用户关注了谁—FollowController增加如下方法
@Autowired
private UserService userService;
@RequestMapping(path="/followees/{userId}",method = RequestMethod.GET)
public String getFollowees(@PathVariable("userId")int userId, Page page, Model model){
//页面需要用username
User user = userService.findUserById(userId);
if(user==null){
throw new RuntimeException("该用户不存在");
}
model.addAttribute("user",user);
page.setLimit(5);
page.setPath("/followees/"+userId);
page.setRows((int)followService.findFolloweeCount(userId,ENTITY_USER));
List<Map<String, Object>> followees = followService.findFollowees(userId, page.getOffset(), page.getLimit());
if(followees!=null){
for(Map<String,Object> map:followees){
//判段当前用户对这个用户的关注状态
User followeeUser = (User)map.get("user");
boolean hasFollowed = hasFollowed(followeeUser.getId());
map.put("hasFollowed",hasFollowed);
}
}
model.addAttribute("users",followees);
return "/site/followee";
}
private boolean hasFollowed(int userId){
if(hostHolder.getUser()==null){
return false;
}
return followService.hasFollowed(hostHolder.getUser().getId(),CommunityContant.ENTITY_USER,userId);
}
2.某个用户的粉丝
@RequestMapping(path="/followers/{userId}",method = RequestMethod.GET)
public String getFollowers(@PathVariable("userId")int userId, Page page, Model model){
//页面需要用username
User user = userService.findUserById(userId);
if(user==null){
throw new RuntimeException("该用户不存在");
}
model.addAttribute("user",user);
page.setLimit(5);
page.setPath("/followers/"+userId);
page.setRows((int)followService.findFollowerCount(ENTITY_USER,userId));
List<Map<String, Object>> followers = followService.findFollowers(userId, page.getOffset(), page.getLimit());
if(followers!=null){
for(Map<String,Object> map:followers){
//判段当前用户对这个用户的关注状态
User followeeUser = (User)map.get("user");
boolean hasFollowed = hasFollowed(followeeUser.getId());
map.put("hasFollowed",hasFollowed);
}
}
model.addAttribute("users",followers);
return "/site/follower";
}
处理页面
1.profile.html
2.followee.html
同理,仔细点尤其是处理已关注未关注按钮那里
七、优化登录模块
- 使用Redis存储验证码
-
- 验证码需要频繁的访问与刷新,对性能要求较高。
- 验证码不需永久保存,通常在很短的时间后就会失效。
- 分布式部署时,存在Session共享的问题。
- 使用Redis存储登录凭证
-
- 处理每次请求时,都要查询用户的登录凭证,访问的频率非常高。
- 使用Redis缓存用户信息
-
- 处理每次请求时,都要根据凭证查询用户信息,访问的频率非常高。
我们可以实现从redis中获取验证码,首先当客户端获取验证码时,先随机生成一个值,这个值作为是客户的登录凭证,我们将这个凭证设置时间为60秒,到时间自动删除。
使用Redis存储验证码
最初,我们把验证码存在了session里,这样不好。使用Redis存验证码的好处:
- Redis性能较高
- Redis可以设置失效时间
- 存到Redis里分布式部署的时候避免了Session共享的问题
1.RedisUtil中增加存储验证码的key
2.LoginController里方法
修改getKaptcha方法
修改Login方法
使用Redis存储登录凭证
最初,我们把登陆凭证存到了MySql里,每次都需要频繁的查询,因为设置了拦截器查询登陆状态。替换用login_ticket表存数据
1.RedisUtil中定义key
2.LoginTicketMapper加@Deprecated注解表名不推荐使用
3.重构代码主要集中在UserService和LoginService中
1.LoginService中login方法
3.UserService中findLoginTicket方法
使用Redis缓存用户信息
就是查询用户的时候先从Redis中取,没有的话先从数据库中取然后存到redis中。用户状态变化时直接删除Redis中的数据。
1.RedisUtil中增加key
2.在UserService中封装三个方法:
3.其他涉及查询User的地方调这三个方法