基于Springboot的核心功能实现
包括自定义前缀树过滤敏感词;使用异步请求的方式发布帖子;查看帖子详情;添加评论时需要同时增加评论的数据和修改帖子的评论数量,进行两步数据库操作,出于安全考虑采用事务管理;私信列表部分包括显示私信列表与私信详情两个子功能;采用异步方式发送私信;配置Controller的全局配置类进行统一异常处理;采用AOP实现统一记录日志。
定义前缀树
根据敏感词,初始化前缀树
编写过滤敏感词的方法

该功能的实现需要异步请求,使用了AJAX
示例:怎么使用jQuery发送AJAX请求?
1.需要在Controller里添加处理异步请求的方法,一般请求方式为post,因为通常是浏览器通过异步的方式向服务器提交一些数据,然后服务端向浏览器响应一个提示,不需要返回网页而是返回一个字符串,所以需要添加@ResponseBody注解
// ajax示例
@RequestMapping(path = "/ajax", method = RequestMethod.POST)
@ResponseBody
public String testAjax(String name, int age) {// 请求处理逻辑System.out.println(name);System.out.println(age);// 返回JSON字符串,方法getJSONString()封装在工具类中return CommunityUtil.getJSONString(0, "操作成功!");
}
2.需要在网页中写一段jQuery代码,访问处理异步请求的方法。
分为两步:引入jQuery,使用jQuery发送异步请求
// ajax示例
采用AJAX请求,实现发布帖子的功能:

具体实现:
首先需要定义一个工具类方法处理Json相关的转换,因为服务端要向客户端返回一些提示信息和数据,使用了fastjson,需要提前导入fastjson的jar包
public class CommunityUtil {public static String getJSONString(int code, String msg, Map 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();}public static String getJSONString(int code, String msg){return getJSONString(code,msg,null);}public static String getJSONString(int code){return getJSONString(code,null,null);}
}
在DiscussPostService中添加增加帖子的相关业务层代码逻辑:
public int addDiscussPost(DiscussPost post){// 参数不能为空if(post == null){throw new IllegalArgumentException("参数不能为空!");}// 转义HTML标记post.setTitle(HtmlUtils.htmlEscape(post.getTitle()));post.setContent(HtmlUtils.htmlEscape(post.getContent()));// 过滤敏感词post.setTitle(sensitiveFilter.filter(post.getTitle()));post.setContent(sensitiveFilter.filter(post.getContent()));return discussPostMapper.insertDiscussPost(post);}
新建DiscussPostController类,在里面写发布帖子的方法:
// 发布帖子
@RequestMapping(path = "/add",method = RequestMethod.POST)
@ResponseBody
public String addDiscussPost(String title,String content){// 判断是否是登录状态User user = hostHolder.getUser();if(user == null){return CommunityUtil.getJSONString(403,"你还没有登录哦!");}// 添加帖子DiscussPost post = new DiscussPost();post.setUserId(user.getId());post.setTitle(title);post.setContent(content);post.setCreateTime(new Date());discussPostService.addDiscussPost(post);// 报错的情况,将来统一处理return CommunityUtil.getJSONString(0,"发布成功!");
}
浏览器发送异步请求:
// 获取标题和内容var title = $("#recipient-name").val();var content = $("#message-text").val();// 发送异步请求$.post(CONTEXT_PATH+"/discuss/add",{"title":title,"content":content},function (data){data = $.parseJSON(data);// 在提示框中显示返回的消息$("#hintBody").text(data.msg);// 显示提示框$("#hintModal").modal("show");// 2秒后,自动隐藏提示框setTimeout(function(){$("#hintModal").modal("hide");// 刷新页面if(data.code == 0){window.location.reload();}}, 2000);})
按照数据层,业务层,表现层逐级来写就行,注意 帖子详情中的内容很多,点赞功能暂未实现,点赞的相关内容后面补充(大概redis部分…)


表现层代码相对复杂(详细如下),主要是套娃套娃套娃🪆🪆🪆…
帖子,评论(帖子的评论),回复(帖子的评论的回评论)…
// 帖子详情@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 commentList = commentService.findCommentsByEntity(ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit());// 评论VO(详情)列表List
添加评论是在帖子详情页面,给帖子评论,或者是给帖子的评论进行评论(也叫回复)
因为既要增加评论的数据,又要修改帖子的评论数量,所以需要进行两步数据库操作,从安全性考虑,将这两步数据库操作放到一个事务里进行管理。(⚠️:事务管理)
使用声明式事务管理,用@Transactional进行注解,使用isolation属性声明事务的隔离级别,使用propagation属性声明事务的传播机制。
⚠️注意:只有对帖子进行评论的时候才需要更新帖子的评论数量,需要判断一下。业务层逻辑如下:
// 添加评论@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)public int addComment(Comment comment){if(comment == null){throw new IllegalArgumentException("参数不能为空!");}// 添加评论comment.setContent(HtmlUtils.htmlEscape(comment.getContent()));comment.setContent(sensitiveFilter.filter(comment.getContent()));int rows = commentMapper.insertComment(comment);// 更新帖子评论数量if(comment.getEntityType() == ENTITY_TYPE_POST){int count = commentMapper.selectCountByEntity(comment.getEntityType(),comment.getEntityId());discussPostService.updateCommentCount(comment.getEntityId(),count);}return rows;}

补充:为什么在添加评论的时候既需要触发评论事件,又需要触发发帖事件?
私信列表包括两个子功能,显示会话列表和私信详情信息。
▶️ 对于显示会话列表,需要查询当前用户的会话列表,每个会话只显示一条最新的私信,分页显示该列表,详细步骤主要包括:首先获得当前登录用户,并且设置分页信息,然后查询当前用户的会话列表,遍历会话列表,查询每一条会话所需要显示的其他信息(该会话私信的总数,该会话未读私信的数量,该会话对面用户的用户信息),除此之外,会话列表中还需要显示当前用户所有未读消息的数量。
▶️ 对于私信详情功能,需要查询某个会话所包含的私信列表,分页显示该列表,并且将显示的私信设置为已读状态,详细步骤主要包括:设置分页信息,根据会话Id查询私信列表,遍历私信列表,查询该条私信发送者有关的用户信息,最后将所有的未读私信设置为已读。

采用异步方式发送私信,发送成功后刷新私信列表
🔎详细步骤:首先根据私信接收者的用户名获得接收者的用户信息,如果接收者用户不存在,直接返回错误提示;如果接收者用户存在,那么构造message对象,将message存到数据库中。

服务端的三层架构:表现层 → 业务层 → 数据层。
浏览器发送的请求一律发给表现层,表现层调用业务层,业务层调用数据层。数据层出现异常后会抛出给它的调用者业务层,业务层会把异常抛出给表现层,所以无论是哪个层的异常,最终都会汇集到表现层,所以对表现层的异常进行捕获和处理就可以处理所有的异常。

🔎 Springboot提供的方案:只需要在特定路径src/main/resources/templates/error下,添加对应错误状态的页面,那么在发现相应错误的时候,就会自动的跳转到对应页面。错误状态页面的名字必须是错误状态,比如404。
跳出错误页面是表面上的处理,内在记录日志部分还没有处理。spring提供了@ControllerAdvice注解进行相关处理。
具体实现:
request.getHeader("x-requested-with");如果请求方式为 XMLHttpRequest,说明是异步请求,响应一个字符串。否则重定向到错误页面。
小结:不需要对Controller做任何处理,只需要使用@ControllerAdvice注解声明一个Controller的全局配置类,对添加了@Controller注解的所有类进行一个统一的异常处理。在全局配置类中使用@ExceptionHandler注解定义一个异常处理方法,对于所有类型的异常进行处理,处理过程是:首先输出异常信息(包括异常的详细信息),然后根据请求方式的不同,进行不同的响应,如果是异步请求,则响应给浏览器一个字符串;如果是普通请求,则重定向到错误页面。请求方式是通过request对象来获取的。
// 代码实现
@ControllerAdvice(annotations = Controller.class)
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());}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");}}
}
1.AOP的基本实现:
首先定义一个方面组件,该方面组件用@Component @Aspect处理,在方面组件里定义切点和通知。
切点:通过@Pointcut注解实现
// 示例
@Pointcut("execution(* com.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 afterRetuning() {System.out.println("afterRetuning");
}
// 示例
@AfterThrowing("pointcut()")
public void afterThrowing() {System.out.println("afterThrowing");
}
要有返回值(Object)和参数(ProceedingJoinPoint joinPoint),抛出异常throws Throwable。除了环绕通知以外的其他通知也可以添加连接点JointPoint的参数。
利用连接点,jointPoint.proceed();就是调用目标对象的方法逻辑,将目标组件的返回值return(return obj)。
// 示例
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {System.out.println("around before"); // 在目标组件前面织入逻辑Object obj = joinPoint.proceed();System.out.println("around after"); // 在目标组件后面织入逻辑return obj;
}
2.统一记录日志需求:对所有的业务组件记录日志,在业务组件调用的一开始记录日志,采用@Before的方式。日志记录格式:用户xxx,在xxx,访问了xxx。

具体实现:
@Pointcut("execution (* com.nowcoder.community.service.*.*(..))")
public void pointcut(){}
定义通知,日志格式:用户[1.2.3.4],在[xxx],访问了[service.xxx()].因为是在业务组件调用的一开始就记录日志,也就是在切点前织入代码逻辑,所以使用@Before注解。
问题1:用户IP怎么获取?可以通过request对象,先利用RequestContextHolder工具类中的getRequestAttributes()方法获取attributes;然后attributes.getRequest()获得request对象;最后使用request.getRemoteHost获得用户IP。
问题2:如何获得当前时间?new Date()获取。
问题3:如果获得调用的是哪个类哪个方法?jointPoint是程序织入的目标,也就是目标组件要调用的方法,通过jointPoint就可以得到调用的是哪个类的哪个方法。
补充:JoinPoint类,用来获取代理类和被代理类的信息。JoinPoint.getSignature()
@Before("pointcut()")
public void before(JoinPoint joinPoint){// 用户[1.2.3.4],在[xxx],访问了[com.nowcoder.community.service.xxx()].ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if(attributes == null){return;}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));
}
上一篇:他为升官起诉政府:我不是白人!
下一篇:Collections工具类