1.过滤敏感词
前缀树
名称:Trie、字典树、查找树
特点:查找效率高,消耗内存大
应用:字符串检索、词频统计、字符串排序等
敏感词过滤器
定义前缀树
根据敏感词,初始化前缀树
编写过滤敏感词的方法
比对算法:
三个指针,指针1指向前缀树,指针2、3指向需要过滤的字符串
指针2 一直向前移动
指针3 当作浮标
public class SensitiveFilter {
private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);
//替换符
private static String REPLACEMENT = "***";
//根节点
private TrieNode root = new TrieNode();
//前缀树
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 key, TrieNode value) {
subNodes.put(key, value);
}
//获取子节点方法
public TrieNode getSubNode(Character key) {
return subNodes.get(key);
}
}
//初始化
@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 (IOException e) {
logger.error("加载敏感词文件失败:" + e.getMessage());
}
}
//将敏感词添加到前缀树当中
private void addKeyword(String keyword) {
TrieNode tempNode = root;//先获得根节点
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);
}
}
}
//判断是否为符号
private boolean isSymbol(Character c) {
// 0x2E80-0x9FFF是东亚文字范围
return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
}
/**
* 过滤敏感词
*
* @param text 待过滤文本
* @return 过滤后的文本
*/
public String filter(String text) {
if (StringUtils.isBlank(text)) { //判空
return null;
}
// 指针1
TrieNode tempNode = root;
// 指针2
int begin = 0;
// 指针3
int position = 0;
// 结果
StringBuilder sb = new StringBuilder();
while (begin < text.length()) {
if (position < text.length()) {
Character c = text.charAt(position);
//跳过符号
if (isSymbol(c)) {
//若指针1指向 树根 且 当前为符号 , 指针2 向前一步
if (tempNode == root) { //指针1指向根 == 指针2指的 要么是合法词 要么是符号 可以移动
sb.append(c);
begin++;
}
position++; //越过字继续前进
continue;
}
tempNode = tempNode.getSubNode(c);
if (tempNode == null) { // 以begin开头的字符串不是敏感词
sb.append(c);
// 进入下一个位置
position = ++begin;
//重新指向前缀树根
tempNode = root;
} else if (tempNode.isKeywordEnd) {//发现敏感词
sb.append(REPLACEMENT);
// 进入下一个位置
position = ++begin;
} else { //还有子节点 要继续检查
position++;
}
} else {// position遍历越界仍未匹配到敏感词
sb.append(text.charAt(begin));
position = ++begin;
tempNode = root;
}
}
return sb.toString();
}
}
2.发布帖子
使用AJAX异步发送发布帖子请求
进行敏感词过滤
封装Json工具
通常 服务端需要返回一个Json格式的字符串 里面包含 状态吗 信息 等内容。
为了方便,我们先在CommunityUtil里封装几个json工具,使用阿里的fastjson,引入依赖
//封装json工具
public static String getJSONString(int code, String msg, Map<String,Object> map){
JSONObject jsonObject = new JSONObject();
//将code msg 装入
jsonObject.put("code",code);
jsonObject.put("msg",msg);
//将map 大散 再装入 用key 遍历map
if (map != null){
for (String key: map.keySet()) {
jsonObject.put(key,map.get(key));
}
}
return jsonObject.toJSONString();
}
//重载json工具
public static String getJSONString(int code, String msg){
return getJSONString(code,msg,null);
}
//重载json工具
public static String getJSONString(int code){
return getJSONString(code,null,null);
}
在DiscussPostMapper 里增加SQL插入语句
在DiscussPostService里写addDiscussPost方法:注意转义html标签 和过滤敏感词
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 的 title content进行敏感词过滤
discussPost.setTitle(sensitiveFilter.filter(discussPost.getTitle()));
discussPost.setContent(sensitiveFilter.filter(discussPost.getContent()));
//插入数据库
return discussPostMapper.insertDiscussPost(discussPost);
}
最后写controller:利用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,"您还未登陆");
}
//创建 post
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,"发布成功");
}
3.事物管理
什么是事务
事务是由N步数据库操作序列组成的逻辑执行单元,这系列操作要么全执行,要么全放弃执行。
事务的特性(ACID)
原子性(Atomicity):事务是应用中不可再分的最小执行体。
一致性(Consistency):事务执行的结果,须使数据从一个一致性状态,变为另一个一致性状态。
隔离性(Isolation):各个事务的执行互不干扰,任何事务的内部操作对其他的事务都是隔离的。
持久性(Durability):事务一旦提交,对数据所做的任何改变都要记录到永久存储器中。
事务的隔离性
常见的并发异常
第一类丢失更新、第二类丢失更新。
脏读、不可重复读、幻读。
常见的隔离级别
Read Uncommitted:读取未提交的数据。
Read Committed:读取已提交的数据。
Repeatable Read:可重复读。
Serializable:串行化。
第一类丢失更新 某一个事务的回滚,导致另外一个事务已更新的数据丢失了
第二类丢失更新 某一个事务的提交,导致另外一个事务已更新的数据丢失了。
脏读 某一个事务,读取了另外一个事务未提交的数据。
不可重复读 某一个事务,对同一个数据前后读取的结果不一致。
幻读 某一个事务,对同一个表前后查询到的行数不一致。
实现机制
悲观锁(数据库)
共享锁(S锁)
事务A对某数据加了共享锁后,其他事务只能对该数据加共享锁,但不能加排他锁。
排他锁(X锁)
事务A对某数据加了排他锁后,其他事务对该数据既不能加共享锁,也不能加排他锁。
乐观锁(自定义)
版本号、时间戳等
在更新数据前,检查版本号是否发生变化。若变化则取消本次更新,否则就更新数据(版本+1)。
Spring事务管理
声明式事务
通过XML配置,声明某方法的事务特征。
通过注解,声明某方法的事务特征。
编程式事务
通过 TransactionTemplate 管理事务, 并通过它执行数据库的操作。
Spring事务示例
第一种,声明式事务,使用@Transactional注解,有两个参数,一个是隔离级别isolation,一个是传播方法Propagation -- 当此方法需要调用其他方法时,被调用的方法该遵循哪种隔离级别
第二种,编程式事务,适合比较复杂的情况下,不需要整个方法都事务管理,需要注入@TransactionTemplate,业务逻辑写在回调函数
这里两个方法都故意写了一个错误Integer.valueOf(“abc”);,查看数据库是否回滚
//REQUIRED(0) 支持当前事务(外部事务),如果不存在则创建新事务
//REQUIRES_NEW(3) 创建一个新的事务,并且暂停当前事务(外部事务)
//NESTED(6) 如果当前存在事务(外部事务),则嵌套在该事务中执行(独立的提交和回滚),否则就和REQUIRED一样
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
public Object save1(){
//新增用户
User user=new User();
userMapper.insertUser(user);
//新增帖子
DiscussPost post=new DiscussPost();
discussPostMapper.insertDiscussPost(post);
//报错
Integer.valueOf("abc");
return "ok";
}
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();
userMapper.insertUser(user);
//新增帖子
DiscussPost post=new DiscussPost();
discussPostMapper.insertDiscussPost(post);
//报错
Integer.valueOf("abc");
return "ok";
}
});
}
4.显示评论
数据层
根据实体查询一页评论数据。
根据实体查询评论的数量。
业务层
处理查询评论的业务。 处理查询评论数量的业务。
表现层
显示帖子详情数据时, 同时显示该帖子所有的评论数据。
首先是对照数据库写comment实体类
然后再dao包下写commentMapper,主要有两个方法,一个是查询评论,一个是查询评论数量,这里也需要用到分页查询,用了offset和limit。
然后就是再mapper目录下写comment-mapper.xml。
业务层 CommentService 也比较简单,没有什么需要处理的,直接返回查询结果
表现层
评论是显示在帖子详情页面,所以改造DiscussPostController的getDiscussPost方法。在CommunityConstant里加两个常量。
之前这个方法返回了帖子和作者的数据,因为评论需要分页,所以传入page对象。
对page对象进行配置,一页显示5条,page的路径和总的评论数
首先用上面写的方法查询评论,放到list里,然后还需要进行一些处理。查询到的评论里只有user_id,没有用户名,同时评论下还有回复。
VO代表显示对象,用来显示在页面上的对象。
整个逻辑就是查出当前帖子下的所有评论,遍历所有评论,处理用户名等信息,查询评论下的回复,遍历每个回复,处理用户名等信息。
/**
* 实体类型:帖子
*/
int ENTITY_TYPE_POST = 1;
/**
* 实体类型:评论
*/
int ENTITY_TYPE_COMMENT = 2;
//根据 帖子id 查询帖子及其相关信息
@RequestMapping(path = "/detail/{discussPostId}",method = RequestMethod.GET)
public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model, Page page){
//根据帖子id查询帖子
DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
model.addAttribute("post",post);
//根据userid查询user
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<>();
//每一条评论 找到评论的作者。找到该评论的回复,回复的作者,回复的用户
for (Comment comment:commentList) {
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);
//用于封装 每一条回复的 作者 回复咪表
List<Map<String, Object>> replyVoList = new ArrayList<>();
if(replyVoList != null){
for (Comment reply: replyList) {
Map<String,Object> replyVo = new HashMap<>();
//回复
replyVo.put("reply", reply);
// 放入 回复的作者
replyVo.put("user", userService.findUserById(reply.getUserId()));
//回复目标
User target = reply.getTargetId() == 0 ? null : userService.findUserById(reply.getTargetId());
replyVo.put("target", target);
//将 单条回复放入 此 评论 总的 回复表
replyVoList.add(replyVo);
}
}
//将回复总表 嵌入 单条评论
commentVo.put("replys", replyVoList);
//回复数量
int replyCount = commentService.findCommentCount(ENTITY_TYPE_COMMENT, comment.getId());
commentVo.put("replyCount", replyCount);
commentVoList.add(commentVo);
}
model.addAttribute("comments", commentVoList);
return "/site/discuss-detail";
}
5.私信列表 私信详情
常规业务 跳过
6.统一处理异常
PS:错误页面要放在resources.templates.error下,这样出错后自动跳到错误页面
@ControllerAdvice
在HomeController 里声明方法getErrorPage()错误访问页面
@RequestMapping(path = "/error", method = RequestMethod.GET)
public String getErrorPage() {
return "/error/500";
}
controller下新建包advice新建ExceptionAdvice类
annotations = Controller.class 表示只扫描Controller这个包下
@ExceptionHandler({Exception.class}),修饰的方法表示处理异常的方法,括号内的参数表示处理的错误类型
遍历所有异常信息,记入日志
通过 request.getHeader("x-requested-with");获得请求的方式
如果是异步请求,响应一个Json字符串显示服务器异常
否则响应错误路径
//处理异常
@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());
}
//从请求头里的x-requested-with 区分 是 普通请求 还是 异步请求
String xRequestedWith = request.getHeader("x-requested-with");
if ("XMLHttpRequest".equals(xRequestedWith)) {
response.setContentType("application/plain;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(CommunityUtil.getJSONString(1, "服务器异常!"));
} else {
response.sendRedirect(request.getContextPath() + "/error");
}
}
7.统一记录日志
AOP
为什么不封装一个用于记录日志的service,然后在对应的业务service里 调用?业务里面不应该耦合系统需求。
Target:目标对象,也就是我们实现的业务,有很多地方可以织入代码
Aspect:切面组件,pointcut声明织入到哪些目标的哪些织入点,advice具体的逻辑
JoinPoint:织入点
三种织入时机,编译装载运行时织入,各有优点,编译时织入可能有一些数据等还不知道,运行时有可能影响速度。
AOP示例
五种注解实现五种通知,前四个实现大同小异,around表示前后都执行,实现方法就是传入joinPoint,调用joinPoint的proceed方法,也就是业务方法,在调用前后写需要织入的逻辑。
package com.neu.langsam.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.neu.langsam.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 o=joinPoint.proceed();
System.out.println("around after");
return o;
}
}
记录用户访问日志
实现 aspect 下的 ServiceLogAspect 类
@Component
@Aspect
public class ServiceLogAspect {
private static final Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class);
@Pointcut("execution(* com.example.community.service.*.*(..))")
public void pointcut() {
}
@Before("pointcut()")
public void before(JoinPoint joinPoint){
//用户[1.1.1.1],在[xxx]访问了[com.neu.langsam.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();
logger.info(String.format("用户[%s],在[%s],访问了[%s].",ip,now,target));
}
}