拆解src/main/java/核心源码

advice包

CustomizeExceptionHandler-全局异常处理

1.类的定义与注解

@ControllerAdvice
@Slf4j
public class CustomizeExceptionHandler {
// ...
}

@ControllerAdvice:这是一个 Spring 框架的注解,它的作用是定义一个全局的异常处理器。该处理器能够捕获所有控制器类抛出的异常。
@Slf4j:这是 Lombok 库的注解,它会自动为这个类添加一个日志记录器 log,便于记录日志信息。
2.异常处理方法handle

@ExceptionHandler(Exception.class)
ModelAndView handle(Throwable e, Model model, HttpServletRequest request, HttpServletResponse response) {
// ...
}

**@ExceptionHandler(Exception.class)**:此注解表明该方法可以处理所有类型的异常。只要控制器抛出异常,就会调用这个方法。
handle 方法接收四个参数:

  • Throwable e:代表捕获到的异常对象。
  • Model model:用于向视图传递数据。
  • HttpServletRequest request:封装了客户端的请求信息。
  • HttpServletResponse response:用于向客户端发送响应。

3.根据请求内容处理异常

String contentType = request.getContentType();
if ("application/json".equals(contentType)) {
// ...
} else {
// ...
}

首先获取请求的内容类型 contentType。若内容类型为 application/json,则以 JSON 格式返回错误信息;反之,则跳转到错误页面。

JSON 格式的错误信息常用于前端发送请求给后端,后端返回 JSON 格式的错误信息,由前端根据这些信息进行处理。而错误页面是一个完整的 HTML页面,包含了文本、样式和可能的交互元素,更方便用户察觉错误。

4.以JSON格式返回错误信息

if ("application/json".equals(contentType)) {
ResultDTO resultDTO;
if (e instanceof CustomizeException) {
resultDTO = ResultDTO.errorOf((CustomizeException) e);
} else {
log.error("handle error", e);
resultDTO = ResultDTO.errorOf(CustomizeErrorCode.SYS_ERROR);
}
try {
response.setContentType("application/json");
response.setStatus(200);
response.setCharacterEncoding("utf-8");
PrintWriter writer = response.getWriter();
writer.write(JSON.toJSONString(resultDTO));
writer.close();
} catch (IOException ioe) {
}
return null;
}

若捕获的异常是 CustomizeException 类型,就调用 ResultDTO.errorOf 方法,依据自定义异常生成错误信息。
若不是 CustomizeException 类型,就记录错误日志,同时使用系统默认的错误码 SYS_ERROR 生成错误信息。配置响应的内容类型为 application/json,状态码为 200,字符编码为 UTF - 8。把 ResultDTO 对象转换为 JSON 字符串,再通过 PrintWriter 写入响应。
最后返回 null,表明不需要跳转视图。
5.跳转到错误页面

else {
if (e instanceof CustomizeException) {
model.addAttribute("message", e.getMessage());
} else {
log.error("handle error", e);
model.addAttribute("message", CustomizeErrorCode.SYS_ERROR.getMessage());
}
return new ModelAndView("error");
}

若捕获的异常是 CustomizeException 类型,就把异常信息添加到 Model 中。
若不是 CustomizeException 类型,就记录错误日志,并且把系统默认的错误信息添加到 Model 中。
最后返回一个 ModelAndView 对象,跳转到名为 error 的视图页面。

cache包

HotTagCache-缓存热门标签

1.类的定义与注解

@Component
@Data
public class HotTagCache {
private List<String> hots = new ArrayList<>();
// ...
}

@Component:这是 Spring 框架的注解,它会把 HotTagCache 类标记为一个组件,这样 Spring 容器就能自动扫描并将其注册为一个 Bean。
@Data:这是 Lombok 库的注解,它会自动为类生成 getter、setter、toString、equals 和 hashCode 等方法。
**private List hots = new ArrayList<>()**:定义了一个私有的字符串列表 hots,用于存储热门标签。
2.updateTags方法

public void updateTags(Map<String, Integer> tags) {
int max = 10;
PriorityQueue<HotTagDTO> priorityQueue = new PriorityQueue<>(max);
// ...
}

updateTags 方法接收一个 Map<String, Integer> 类型的参数 tags,其中键为标签名,值为标签的优先级。
int max = 10:设定要缓存的热门标签的最大数量为 10。
PriorityQueue priorityQueue = new PriorityQueue<>(max):创建一个优先队列 priorityQueue,其最大容量为 10,用于存储 HotTagDTO 对象。优先队列会根据 HotTagDTO 对象的优先级进行排序,优先级最低的元素会位于队列头部。
3.遍历tags并更新优先队列

tags.forEach((name, priority) -> {
HotTagDTO hotTagDTO = new HotTagDTO();
hotTagDTO.setName(name);
hotTagDTO.setPriority(priority);
if (priorityQueue.size() < max) {
priorityQueue.add(hotTagDTO);
} else {
HotTagDTO minHot = priorityQueue.peek();
if (hotTagDTO.compareTo(minHot) > 0) {
priorityQueue.poll();
priorityQueue.add(hotTagDTO);
}
}
});

运用 forEach 方法遍历 tags 中的每个标签及其优先级。
针对每个标签,创建一个 HotTagDTO 对象,并设置其名称和优先级。
若优先队列的大小小于最大容量 max,则将 HotTagDTO 对象添加到优先队列中。
若优先队列已满,获取队列头部(优先级最低)的元素 minHot。
若当前 HotTagDTO 对象的优先级高于 minHot,则移除队列头部元素,再将当前 HotTagDTO 对象添加到队列中。
4.对优先队列中的元素进行排序并更新hots列表

List<String> sortedTags = new ArrayList<>();
HotTagDTO poll = priorityQueue.poll();
while (poll != null) {
sortedTags.add(0, poll.getName());
poll = priorityQueue.poll();
}
hots = sortedTags;

创建一个新的字符串列表 sortedTags,用于存储排序后的热门标签。
从优先队列中依次取出元素,将其标签名插入到 sortedTags 列表的头部,这样就能保证标签按优先级从高到低排序。
最后将 sortedTags 赋值给 hots 列表,完成热门标签的更新。

QuestionCache-缓存置顶问题

1.类的定义与注解

@Service
@Slf4j
public class QuestionCache {
// ...
}

@Service:这是 Spring 框架的注解,表明该类是一个服务层组件,Spring 容器会自动扫描并将其注册为一个 Bean。
@Slf4j:这是 Lombok 库的注解,会自动为该类添加一个日志记录器 log,方便记录日志信息。
2.依赖注入

@Autowired
private QuestionExtMapper questionExtMapper;
@Autowired
private UserMapper userMapper;

@Autowired:这是 Spring 框架的依赖注入注解,会自动将 QuestionExtMapper 和 UserMapper 这两个 Bean 注入到 QuestionCache 类中。QuestionExtMapper 用于与数据库中 Question 表进行交互,UserMapper 用于与数据库中 User 表进行交互。
3.缓存的创建

private static Cache<String, List<QuestionDTO>> cacheQuestions = CacheBuilder.newBuilder()
.maximumSize(100)
.expireAfterWrite(10, TimeUnit.MINUTES)
.removalListener(entity -> log.info("QUESTIONS_CACHE_REMOVE:{}", entity.getKey()))
.build();

用 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() {
List<QuestionDTO> stickies;
try {
stickies = cacheQuestions.get("sticky", () -> {
List<Question> questions = questionExtMapper.selectSticky();
if (questions != null && questions.size() != 0) {
List<QuestionDTO> questionDTOS = new ArrayList<>();
for (Question question : questions) {
User user = userMapper.selectByPrimaryKey(question.getCreator());
QuestionDTO questionDTO = new QuestionDTO();
BeanUtils.copyProperties(question, questionDTO);
questionDTO.setUser(user);
questionDTO.setDescription("");
questionDTOS.add(questionDTO);
}
return questionDTOS;
} else {
return Lists.newArrayList();
}
});
} catch (Exception e) {
log.error("getStickies error", e);
return Lists.newArrayList();
}
return stickies;
}

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
@Slf4j
public class QuestionRateLimiter {
// ...
}

@Service:这是 Spring 框架的注解,表明该类是一个服务层组件,Spring 容器会自动扫描并将其注册为一个 Bean。
@Slf4j:这是 Lombok 库的注解,会自动为该类添加一个日志记录器 log,方便记录日志信息。
2.依赖注入

@Autowired
private ApplicationContext applicationContext;

@Autowired:这是 Spring 框架的依赖注入注解,将 ApplicationContext 这个 Spring 应用上下文对象注入到 QuestionRateLimiter 类中。ApplicationContext 可用于发布事件,在用户达到发布频率限制时触发相应的处理逻辑。
3.缓存创建

private static Cache<Long, Integer> userLimiter = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(1, TimeUnit.MINUTES)
.removalListener(entity -> log.info("QUESTIONS_RATE_LIMITER_REMOVE:{}", entity.getKey()))
.build();

使用 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方法
    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;
    }
    }
    userLimiter.get(userId, () -> 0):尝试从缓存中获取指定用户 ID 对应的发布次数。
  • 如果缓存中存在该用户 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> tagDTOS = new ArrayList<>();

// 开发语言标签分类
TagDTO program = new TagDTO();
program.setCategoryName("开发语言");
program.setTags(Arrays.asList("javascript", "php", "css", "html", "html5", "java", "node.js", "python", "c++", "c", "golang", "objective-c", "typescript", "shell", "swift", "c#", "sass", "ruby", "bash", "less", "asp.net", "lua", "scala", "coffeescript", "actionscript", "rust", "erlang", "perl"));
tagDTOS.add(program);

// 平台框架标签分类
TagDTO framework = new TagDTO();
framework.setCategoryName("平台框架");
framework.setTags(Arrays.asList("laravel", "spring", "express", "django", "flask", "yii", "ruby-on-rails", "tornado", "koa", "struts"));
tagDTOS.add(framework);

// 服务器标签分类
TagDTO server = new TagDTO();
server.setCategoryName("服务器");
server.setTags(Arrays.asList("linux", "nginx", "docker", "apache", "ubuntu", "centos", "缓存 tomcat", "负载均衡", "unix", "hadoop", "windows-server"));
tagDTOS.add(server);

// 数据库标签分类
TagDTO db = new TagDTO();
db.setCategoryName("数据库");
db.setTags(Arrays.asList("mysql", "redis", "mongodb", "sql", "oracle", "nosql memcached", "sqlserver", "postgresql", "sqlite"));
tagDTOS.add(db);

// 开发工具标签分类
TagDTO tool = new TagDTO();
tool.setCategoryName("开发工具");
tool.setTags(Arrays.asList("git", "github", "visual-studio-code", "vim", "sublime-text", "xcode intellij-idea", "eclipse", "maven", "ide", "svn", "visual-studio", "atom emacs", "textmate", "hg"));
tagDTOS.add(tool);

return tagDTOS;
}

该方法返回一个 List,包含了不同分类的标签信息。
代码中创建了多个 TagDTO 对象,分别代表不同的标签分类,如开发语言、平台框架、服务器、数据库和开发工具。
为每个 TagDTO 对象设置分类名称和具体的标签列表,然后将这些对象添加到 tagDTOS 列表中并返回。
2.filterInvalid方法

public static String filterInvalid(String tags) {
String[] split = StringUtils.split(tags, ",");
List<TagDTO> tagDTOS = get();

List<String> tagList = tagDTOS.stream().flatMap(tag -> tag.getTags().stream()).collect(Collectors.toList());
String invalid = Arrays.stream(split).filter(t -> StringUtils.isBlank(t) || !tagList.contains(t)).collect(Collectors.joining(","));
return invalid;
}

该方法用于过滤输入的标签字符串,找出其中无效的标签。
首先,使用 StringUtils.split 方法将输入的标签字符串按逗号分隔成字符串数组。
调用 get 方法获取所有有效的标签分类信息。
使用 Java 8 的 Stream API 将所有有效的标签合并到一个列表 tagList 中。
对输入的标签数组进行过滤,找出空字符串或不在 tagList 中的标签,然后使用 Collectors.joining 方法将这些无效标签用逗号连接成一个字符串并返回。

controller包

AuthorizeController-用户登录与注销

1.类的定义与注解

@Controller
@Slf4j
public class AuthorizeController {
// ...
}

@Controller:这是 Spring 框架的注解,表明该类是一个控制器,用于处理 HTTP 请求。
@Slf4j:这是 Lombok 库的注解,会自动为该类添加一个日志记录器 log,方便记录日志信息。
2.依赖注入与属性注入

@Autowired
private UserStrategyFactory userStrategyFactory;

@Autowired
private GithubProvider githubProvider;

@Value("${github.client.id}")
private String clientId;

@Value("${github.client.secret}")
private String clientSecret;

@Value("${github.redirect.uri}")
private String redirectUri;

@Autowired
private UserService userService;

@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}")
public String newCallback(@PathVariable(name = "type") String type,
@RequestParam(name = "code") String code,
@RequestParam(name = "state", required = false) String state,
HttpServletRequest request,
HttpServletResponse response) {
UserStrategy userStrategy = userStrategyFactory.getStrategy(type);
LoginUserInfo loginUserInfo = userStrategy.getUser(code, state);
if (loginUserInfo != null && loginUserInfo.getId() != null) {
User user = new User();
String token = UUID.randomUUID().toString();
user.setToken(token);
user.setName(loginUserInfo.getName());
user.setAccountId(String.valueOf(loginUserInfo.getId()));
user.setType(type);
user.setAvatarUrl(loginUserInfo.getAvatarUrl());
userService.createOrUpdate(user);
Cookie cookie = new Cookie("token", token);
cookie.setMaxAge(60 * 60 * 24 * 30 * 6);
cookie.setPath("/");
response.addCookie(cookie);
return "redirect:/";
} else {
log.error("callback get github error,{}", loginUserInfo);
// 登录失败,重新登录
return "redirect:/";
}
}

**@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")
public String logout(HttpServletRequest request,
HttpServletResponse response) {
request.getSession().invalidate();
Cookie cookie = new Cookie("token", null);
cookie.setMaxAge(0);
cookie.setPath("/");
response.addCookie(cookie);
return "redirect:/";
}

**@GetMapping(“/logout”)**:这是 Spring 框架的注解,表明该方法处理 HTTP GET 请求,请求路径为 /logout。
方法接收两个参数:

  • HttpServletRequest request:封装了客户端的请求信息。
  • HttpServletResponse response:用于向客户端发送响应。

方法的执行逻辑如下:

  • 调用 request.getSession().invalidate() 方法使当前会话失效。
  • 创建一个名为 token 的 Cookie,并将其值设置为 null,有效期设置为 0,然后将该 Cookie 添加到响应中,这样客户端会删除该 Cookie。
  • 最后重定向到根路径 /。

CommentController-处理评论相关请求

1.类定义与依赖注入

@Controller
public class CommentController {
@Autowired
private CommentService commentService; // 评论业务服务
@Autowired
private QuestionRateLimiter questionRateLimiter; // 评论频率限流器
}

@Controller:表明这是一个 Spring MVC 控制器,负责处理 HTTP 请求。
@Autowired:自动注入 CommentService(处理评论逻辑)和 QuestionRateLimiter(限制用户评论频率)的 Bean。
2.提交评论的POST请求处理

@ResponseBody
@RequestMapping(value = "/comment", method = RequestMethod.POST)
public Object post(@RequestBody CommentCreateDTO commentCreateDTO, HttpServletRequest request) {
// ... 处理逻辑 ...
}

3.用户登录校验

User user = (User) request.getSession().getAttribute("user");
if (user == null) {
return ResultDTO.errorOf(CustomizeErrorCode.NO_LOGIN);
}

从 Session 中获取用户信息,若未登录则返回错误码 NO_LOGIN。
4.用户禁用状态检查

if (user.getDisable() != null && user.getDisable() == 1) {
return ResultDTO.errorOf(CustomizeErrorCode.USER_DISABLE);
}

检查用户是否被禁用(disable 字段为 1),若禁用则返回错误码 USER_DISABLE。
5.评论内容非空以及限流检查

if (StringUtils.isBlank(commentCreateDTO.getContent())) {
return ResultDTO.errorOf(CustomizeErrorCode.CONTENT_IS_EMPTY);
}
if (questionRateLimiter.reachLimit(user.getId())) {
return ResultDTO.errorOf(CustomizeErrorCode.RATE_LIMIT);
}

确保评论内容不为空,否则返回错误码 CONTENT_IS_EMPTY。调用 QuestionRateLimiter 检查用户是否达到评论频率限制,若超出则返回错误码 RATE_LIMIT。
6.创建评论对象并保存

Comment comment = new Comment();
comment.setParentId(commentCreateDTO.getParentId()); // 父评论或问题的ID
comment.setContent(commentCreateDTO.getContent()); // 评论内容
comment.setType(commentCreateDTO.getType()); // 评论类型(问题或评论的回复)
comment.setGmtCreate(System.currentTimeMillis()); // 创建时间
comment.setGmtModified(System.currentTimeMillis()); // 修改时间
comment.setCommentator(user.getId()); // 评论者ID
comment.setLikeCount(0L); // 点赞数初始化为0
commentService.insert(comment, user); // 保存评论

将前端提交的 CommentCreateDTO 转换为 Comment 对象,并插入数据库。
7.获取评论列表的GET请求

@ResponseBody
@RequestMapping(value = "/comment/{id}", method = RequestMethod.GET)
public ResultDTO<List<CommentDTO>> comments(@PathVariable(name = "id") Long id) {
List<CommentDTO> commentDTOS = commentService.listByTargetId(id, CommentTypeEnum.COMMENT);
return ResultDTO.okOf(commentDTOS);
}

通过路径参数 id 获取目标评论或问题的 ID。
调用 commentService 查询指定 ID 下的所有子评论(CommentTypeEnum.COMMENT 表示这是对评论的回复)

CustomizeErrorController-错误处理逻辑

1.类定义与注解