SpringBoot问答论坛社区
拆解src/main/java/核心源码
advice包
CustomizeExceptionHandler-全局异常处理
1.类的定义与注解
|
@ControllerAdvice:这是一个 Spring 框架的注解,它的作用是定义一个全局的异常处理器。该处理器能够捕获所有控制器类抛出的异常。
@Slf4j:这是 Lombok 库的注解,它会自动为这个类添加一个日志记录器 log,便于记录日志信息。
2.异常处理方法handle
|
**@ExceptionHandler(Exception.class)**:此注解表明该方法可以处理所有类型的异常。只要控制器抛出异常,就会调用这个方法。
handle 方法接收四个参数:
- Throwable e:代表捕获到的异常对象。
- Model model:用于向视图传递数据。
- HttpServletRequest request:封装了客户端的请求信息。
- HttpServletResponse response:用于向客户端发送响应。
3.根据请求内容处理异常
String contentType = request.getContentType(); |
首先获取请求的内容类型 contentType。若内容类型为 application/json,则以 JSON 格式返回错误信息;反之,则跳转到错误页面。
JSON 格式的错误信息常用于前端发送请求给后端,后端返回 JSON 格式的错误信息,由前端根据这些信息进行处理。而错误页面是一个完整的 HTML页面,包含了文本、样式和可能的交互元素,更方便用户察觉错误。
4.以JSON格式返回错误信息
if ("application/json".equals(contentType)) { |
若捕获的异常是 CustomizeException 类型,就调用 ResultDTO.errorOf 方法,依据自定义异常生成错误信息。
若不是 CustomizeException 类型,就记录错误日志,同时使用系统默认的错误码 SYS_ERROR 生成错误信息。配置响应的内容类型为 application/json,状态码为 200,字符编码为 UTF - 8。把 ResultDTO 对象转换为 JSON 字符串,再通过 PrintWriter 写入响应。
最后返回 null,表明不需要跳转视图。
5.跳转到错误页面
else { |
若捕获的异常是 CustomizeException 类型,就把异常信息添加到 Model 中。
若不是 CustomizeException 类型,就记录错误日志,并且把系统默认的错误信息添加到 Model 中。
最后返回一个 ModelAndView 对象,跳转到名为 error 的视图页面。
cache包
HotTagCache-缓存热门标签
1.类的定义与注解
|
@Component:这是 Spring 框架的注解,它会把 HotTagCache 类标记为一个组件,这样 Spring 容器就能自动扫描并将其注册为一个 Bean。
@Data:这是 Lombok 库的注解,它会自动为类生成 getter、setter、toString、equals 和 hashCode 等方法。
**private List
2.updateTags方法
public void updateTags(Map<String, Integer> tags) { |
updateTags 方法接收一个 Map<String, Integer> 类型的参数 tags,其中键为标签名,值为标签的优先级。
int max = 10:设定要缓存的热门标签的最大数量为 10。
PriorityQueue
3.遍历tags并更新优先队列
tags.forEach((name, priority) -> { |
运用 forEach 方法遍历 tags 中的每个标签及其优先级。
针对每个标签,创建一个 HotTagDTO 对象,并设置其名称和优先级。
若优先队列的大小小于最大容量 max,则将 HotTagDTO 对象添加到优先队列中。
若优先队列已满,获取队列头部(优先级最低)的元素 minHot。
若当前 HotTagDTO 对象的优先级高于 minHot,则移除队列头部元素,再将当前 HotTagDTO 对象添加到队列中。
4.对优先队列中的元素进行排序并更新hots列表
List<String> sortedTags = new ArrayList<>(); |
创建一个新的字符串列表 sortedTags,用于存储排序后的热门标签。
从优先队列中依次取出元素,将其标签名插入到 sortedTags 列表的头部,这样就能保证标签按优先级从高到低排序。
最后将 sortedTags 赋值给 hots 列表,完成热门标签的更新。
QuestionCache-缓存置顶问题
1.类的定义与注解
|
@Service:这是 Spring 框架的注解,表明该类是一个服务层组件,Spring 容器会自动扫描并将其注册为一个 Bean。
@Slf4j:这是 Lombok 库的注解,会自动为该类添加一个日志记录器 log,方便记录日志信息。
2.依赖注入
|
@Autowired:这是 Spring 框架的依赖注入注解,会自动将 QuestionExtMapper 和 UserMapper 这两个 Bean 注入到 QuestionCache 类中。QuestionExtMapper 用于与数据库中 Question 表进行交互,UserMapper 用于与数据库中 User 表进行交互。
3.缓存的创建
private static Cache<String, List<QuestionDTO>> cacheQuestions = CacheBuilder.newBuilder() |
用 Google Guava 缓存库创建了一个静态的缓存对象 cacheQuestions:
- maximumSize(100):设置缓存的最大容量为 100 个条目,当缓存中的条目数量超过 100 时,Guava 会根据其内部的淘汰策略移除一些条目。
- expireAfterWrite(10, TimeUnit.MINUTES):设置缓存条目的过期时间为写入后的 10 分钟,即一个条目在写入缓存 10 分钟后会被自动移除。
- removalListener(entity -> log.info(“QUESTIONS_CACHE_REMOVE:{}”, entity.getKey())):设置一个移除监听器,当缓存中的条目被移除时,会记录一条日志信息,包含被移除条目的键。
4.getStickies方法
public List<QuestionDTO> getStickies() { |
cacheQuestions.get(“sticky”, …):尝试从缓存中获取键为 “sticky” 的置顶问题列表:
- 如果缓存中存在该键对应的条目,则直接返回该条目。
- 如果缓存中不存在该键对应的条目,则会执行传入的 Callable 接口的实现(即 Lambda 表达式)来生成该条目。
在 Callable 实现中:
- 调用 questionExtMapper.selectSticky() 方法从数据库中查询置顶问题列表。
- 遍历查询到的 Question 列表,对于每个 Question 对象:
- 通过 userMapper.selectByPrimaryKey(question.getCreator()) 方法根据 Question 的创建者 ID 查询对应的 User 对象。
- 创建一个 QuestionDTO 对象,并使用 BeanUtils.copyProperties(question, questionDTO) 方法将 Question 对象的属性复制到 QuestionDTO 对象中。
- 将查询到的 User 对象设置到 QuestionDTO 对象中,并清空 QuestionDTO 对象的描述信息。
- 将 QuestionDTO 对象添加到 questionDTOS 列表中。
- 如果查询结果为空,则返回一个空列表。
如果在获取缓存或执行 Callable 过程中发生异常,会记录错误日志并返回一个空列表。
QuestionRateLimiter-限流用户发布问题的频率
1.类的定义与注解
|
@Service:这是 Spring 框架的注解,表明该类是一个服务层组件,Spring 容器会自动扫描并将其注册为一个 Bean。
@Slf4j:这是 Lombok 库的注解,会自动为该类添加一个日志记录器 log,方便记录日志信息。
2.依赖注入
|
@Autowired:这是 Spring 框架的依赖注入注解,将 ApplicationContext 这个 Spring 应用上下文对象注入到 QuestionRateLimiter 类中。ApplicationContext 可用于发布事件,在用户达到发布频率限制时触发相应的处理逻辑。
3.缓存创建
private static Cache<Long, Integer> userLimiter = CacheBuilder.newBuilder() |
使用 Google Guava 缓存库创建了一个静态的缓存对象 userLimiter。
- maximumSize(1000):设置缓存的最大容量为 1000 个条目,当缓存中的条目数量超过 1000 时,Guava 会根据其内部的淘汰策略移除一些条目。
- expireAfterWrite(1, TimeUnit.MINUTES):设置缓存条目的过期时间为写入后的 1 分钟,即一个条目在写入缓存 1 分钟后会被自动移除。
- removalListener(entity -> log.info(“QUESTIONS_RATE_LIMITER_REMOVE:{}”, entity.getKey())):设置一个移除监听器,当缓存中的条目被移除时,会记录一条日志信息,包含被移除条目的键(即用户 ID)。
4.reachLimit方法userLimiter.get(userId, () -> 0):尝试从缓存中获取指定用户 ID 对应的发布次数。public boolean reachLimit(Long userId) {
try {
Integer limit = userLimiter.get(userId, () -> 0);
userLimiter.put(userId, limit + 1);
log.info("user : {} post count : {}", userId, limit);
boolean isReachLimited = limit >= 2;
if (isReachLimited) {
applicationContext.publishEvent(new QuestionRateLimiterEvent(this, userId));
}
return isReachLimited;
} catch (ExecutionException e) {
return false;
}
} - 如果缓存中存在该用户 ID 对应的条目,则返回该条目的值(即发布次数)。
- 如果缓存中不存在该用户 ID 对应的条目,则会执行传入的 Callable 接口的实现(即 Lambda 表达式 () -> 0),将该用户的发布次数初始化为 0。
userLimiter.put(userId, limit + 1):将该用户的发布次数加 1 后重新存入缓存。
log.info(“user : {} post count : {}”, userId, limit):记录用户的 ID 和当前发布次数。
boolean isReachLimited = limit >= 2:判断用户的发布次数是否达到或超过 2 次,如果达到或超过则认为用户达到了发布频率限制。
如果用户达到发布频率限制(isReachLimited 为 true),则使用 applicationContext.publishEvent(new QuestionRateLimiterEvent(this, userId)) 发布一个 QuestionRateLimiterEvent 事件,通知系统中监听该事件的组件进行相应处理。
如果在获取缓存或执行 Callable 过程中发生异常(ExecutionException),则返回 false,表示用户未达到发布频率限制。
TagCache-管理标签数据
1.get方法
public static List<TagDTO> get() { |
该方法返回一个 List
代码中创建了多个 TagDTO 对象,分别代表不同的标签分类,如开发语言、平台框架、服务器、数据库和开发工具。
为每个 TagDTO 对象设置分类名称和具体的标签列表,然后将这些对象添加到 tagDTOS 列表中并返回。
2.filterInvalid方法
public static String filterInvalid(String tags) { |
该方法用于过滤输入的标签字符串,找出其中无效的标签。
首先,使用 StringUtils.split 方法将输入的标签字符串按逗号分隔成字符串数组。
调用 get 方法获取所有有效的标签分类信息。
使用 Java 8 的 Stream API 将所有有效的标签合并到一个列表 tagList 中。
对输入的标签数组进行过滤,找出空字符串或不在 tagList 中的标签,然后使用 Collectors.joining 方法将这些无效标签用逗号连接成一个字符串并返回。
controller包
AuthorizeController-用户登录与注销
1.类的定义与注解
|
@Controller:这是 Spring 框架的注解,表明该类是一个控制器,用于处理 HTTP 请求。
@Slf4j:这是 Lombok 库的注解,会自动为该类添加一个日志记录器 log,方便记录日志信息。
2.依赖注入与属性注入
|
@Autowired:这是 Spring 框架的依赖注入注解,将 UserStrategyFactory、GithubProvider 和 UserService 这三个 Bean 注入到 AuthorizeController 类中。
- UserStrategyFactory:用于根据不同的登录类型获取相应的用户策略。
- GithubProvider:可能用于与 GitHub 进行交互,获取授权信息。
- UserService:用于处理用户的创建和更新操作。
@Value:这是 Spring 框架的属性注入注解,从配置文件中读取 github.client.id、github.client.secret 和 github.redirect.uri 的值,并分别赋值给 clientId、clientSecret 和 redirectUri 变量。
3.newCallback方法
|
**@GetMapping(“/callback/{type}”)**:这是 Spring 框架的注解,表明该方法处理 HTTP GET 请求,请求路径为 /callback/{type},其中 {type} 是一个路径变量。
方法接收四个参数:
- @PathVariable(name = “type”) String type:从路径中获取登录类型。
- @RequestParam(name = “code”) String code:从请求参数中获取授权码。
- @RequestParam(name = “state”, required = false) String state:从请求参数中获取状态码,该参数可选。
- HttpServletRequest request:封装了客户端的请求信息。
- HttpServletResponse response:用于向客户端发送响应。
方法的执行逻辑如下:
- 通过 userStrategyFactory.getStrategy(type) 方法根据登录类型获取相应的用户策略。
- 调用 userStrategy.getUser(code, state) 方法获取登录用户的信息。
- 如果获取到的用户信息不为空且用户 ID 不为空,则创建一个新的 User 对象,并设置相关属性,包括生成一个唯一的 token。
- 调用 userService.createOrUpdate(user) 方法创建或更新用户信息。
- 创建一个名为 token 的 Cookie,并将其有效期设置为 6 个月,然后将该 Cookie 添加到响应中。
- 最后重定向到根路径 /。
- 如果获取用户信息失败,则记录错误日志,并同样重定向到根路径 /。
4.logout方法
|
**@GetMapping(“/logout”)**:这是 Spring 框架的注解,表明该方法处理 HTTP GET 请求,请求路径为 /logout。
方法接收两个参数:
- HttpServletRequest request:封装了客户端的请求信息。
- HttpServletResponse response:用于向客户端发送响应。
方法的执行逻辑如下:
- 调用 request.getSession().invalidate() 方法使当前会话失效。
- 创建一个名为 token 的 Cookie,并将其值设置为 null,有效期设置为 0,然后将该 Cookie 添加到响应中,这样客户端会删除该 Cookie。
- 最后重定向到根路径 /。
CommentController-处理评论相关请求
1.类定义与依赖注入
|
@Controller:表明这是一个 Spring MVC 控制器,负责处理 HTTP 请求。
@Autowired:自动注入 CommentService(处理评论逻辑)和 QuestionRateLimiter(限制用户评论频率)的 Bean。
2.提交评论的POST请求处理
|
3.用户登录校验
User user = (User) request.getSession().getAttribute("user"); |
从 Session 中获取用户信息,若未登录则返回错误码 NO_LOGIN。
4.用户禁用状态检查
if (user.getDisable() != null && user.getDisable() == 1) { |
检查用户是否被禁用(disable 字段为 1),若禁用则返回错误码 USER_DISABLE。
5.评论内容非空以及限流检查
if (StringUtils.isBlank(commentCreateDTO.getContent())) { |
确保评论内容不为空,否则返回错误码 CONTENT_IS_EMPTY。调用 QuestionRateLimiter 检查用户是否达到评论频率限制,若超出则返回错误码 RATE_LIMIT。
6.创建评论对象并保存
Comment comment = new Comment(); |
将前端提交的 CommentCreateDTO 转换为 Comment 对象,并插入数据库。
7.获取评论列表的GET请求
|
通过路径参数 id 获取目标评论或问题的 ID。
调用 commentService 查询指定 ID 下的所有子评论(CommentTypeEnum.COMMENT 表示这是对评论的回复)
CustomizeErrorController-错误处理逻辑
1.类定义与注解