⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 blog.csdn.net/m0_60353039/article/details/131810657 「兴趣使然黄小黄」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

前言

Spring AOP是一个基于面向切面编程的框架,用于将横切性关注点(如日志记录、事务管理)与业务逻辑分离,通过代理对象将这些关注点织入到目标对象的方法执行前后、抛出异常或返回结果时等特定位置执行,从而提高程序的可复用性、可维护性和灵活性。

但使用原生Spring AOP实现统一的拦截是非常繁琐、困难的。而在本节,我们将使用一种简单的方式进行统一功能处理,这也是AOP的一次实战,具体如下:

  • 统一用户登录权限验证
  • 统一数据格式返回
  • 统一异常处理

0 为什么需要统一功能处理?

统一功能处理是为了提高代码的可维护性、可重用性和可扩展性而进行的一种设计思想。在应用程序中,可能存在一些通用的功能需求,例如身份验证、日志记录、异常处理等。

这些功能需要在多个地方进行调用和处理,如果每个地方都单独实现这些功能,会导致代码冗余、难以维护和重复劳动。通过统一功能处理的方式,可以将这些通用功能抽取出来,以统一的方式进行处理。这样做有以下几个好处:

  • 「代码复用」:将通用功能抽取成独立的模块或组件,可以在多个地方共享使用,减少重复编写代码的工作量。
  • 「可维护性」:将通用功能集中处理,可以方便地对其进行修改、优化或扩展,而不需要在多个地方进行修改。
  • 「代码整洁性」:通过统一功能处理,可以使代码更加清晰、简洁,减少了冗余的代码。
  • 「可扩展性」:当需要添加新的功能时,只需要在统一功能处理的地方进行修改或扩展,而不需要在多个地方进行修改,降低了代码的耦合度。

1 统一用户登录权限验证

1.1 使用原生 Spring AOP 实现统一拦截的难点

以使用原生 Spring AOP 来实现⽤户统⼀登录验证为例,主要是使用前置通知和环绕通知实现的,具体实现如下

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
* @author 兴趣使然黄小黄
* @version 1.0
* @date 2023/7/18 16:37
*/
@Aspect // 表明此类为一个切面
@Component // 随着框架的启动而启动
public class UserAspect {
// 定义切点, 这里使用 Aspect 表达式语法
@Pointcut("execution(* com.hxh.demo.controller.UserController.*(..))")
public void pointcut(){ }


// 前置通知
@Before("pointcut()")
public void beforeAdvice() {
System.out.println("执行了前置通知~");
}

// 环绕通知
@Around("pointcut()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) {
System.out.println("进入环绕通知~");
Object obj = null;
// 执行目标方法
try {
obj = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("退出环绕通知~");
return obj;
}

}

从上述的代码示例可以看出,使用原生的 Spring AOP 实现统一拦截的难点主要有以下几个方面:

  • 定义拦截规则非常困难。如注册⽅法和登录⽅法是不拦截的,这样的话排除⽅法的规则很难定义,甚⾄没办法定义。
  • 在切面类中拿到 HttpSession 比较难。

为了解决 Spring AOP 的这些问题,Spring 提供了拦截器~

1.2 使用 Spring 拦截器实现统一用户登录验证

Spring拦截器是Spring框架提供的一个功能强大的组件,用于在请求到达控制器之前或之后进行拦截和处理。拦截器可以用于实现各种功能,如身份验证、日志记录、性能监测等。

要使用Spring拦截器,需要创建一个实现了HandlerInterceptor接口的拦截器类。该接口定义了三个方法:preHandle、postHandle和afterCompletion。

  • preHandle方法在请求到达控制器之前执行,可以用于进行身份验证、参数校验等;
  • postHandle方法在控制器处理完请求后执行,可以对模型和视图进行操作;
  • afterCompletion方法在视图渲染完成后执行,用于清理资源或记录日志。

拦截器的实现可以分为以下两个步骤:

  • 创建自定义拦截器,实现 HandlerInterceptor 接口的 preHandle(执行具体方法之前的预处理)方法。
  • 将自定义拦截器加入 WebMvcConfigurer 的 addInterceptors 方法中,并且设置拦截规则。

具体实现如下:

step1. 创建自定义拦截器,自定义拦截器是一个普通类,代码如下:

import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/**
* @author 兴趣使然黄小黄
* @version 1.0
* @date 2023/7/19 16:31
* 统一用户登录权限验证 —— 登录拦截器
*/
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 用户登录业务判断
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
return true; // 验证成功, 继续controller的流程
}
// 可以跳转登录界面或者返回 401/403 没有权限码
response.sendRedirect("/login.html"); // 跳转到登录页面
return false; // 验证失败
}
}

step2. 配置拦截器并设置拦截规则,代码如下:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
* @author 兴趣使然黄小黄
* @version 1.0
* @date 2023/7/19 16:51
*/
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns("/user/login") // 不拦截的 url 地址
.excludePathPatterns("/user/reg")
.excludePathPatterns("/**/*.html"); // 不拦截所有页面
}
}

1.3 拦截器的实现原理及源码分析

当有了拦截器后,会在调用 Controller 之前进行相应的业务处理,执行的流程如下图所示:

「拦截器实现原理的源码分析」

从上述案例实现结果的控制台的日志信息可以看出,所有的 Controller 执⾏都会通过⼀个调度器 DispatcherServlet 来实现。

而所有的方法都会执行 DispatcherServlet 中的 doDispatch 调度方法,doDispatch 源码如下:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
ModelAndView mv = null;
Object dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
if (isGet || HttpMethod.HEAD.matches(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
return;
}
}

// 调用预处理
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 执行 Controller 中的业务
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
this.applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new NestedServletException("Handler dispatch failed", var21);
}
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
} catch (Exception var22) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
} catch (Throwable var23) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
}
} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
} else if (multipartRequestParsed) {
this.cleanupMultipart(processedRequest);
}
}
}

从上述源码可以看出,在执行 Controller 之前,先会调用 预处理方法 applyPreHandle,该方法源码如下:

boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
// 获取项目中使用的拦截器 HandlerInterceptor
HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
if (!interceptor.preHandle(request, response, this.handler)) {
this.triggerAfterCompletion(request, response, (Exception)null);
return false;
}
}
return true;
}

在上述源码中,可以看出,在 applyPreHandle 中会获取所有拦截器 HandlerInterceptor 并执行拦截器中的 preHandle 方法,这与之前我们实现拦截器的步骤对应,如下图所示:

此时,相应的preHandle中的业务逻辑就会执行。

1.4 统一访问前缀添加

统一访问前缀的添加与登录拦截器实现类似,即给所有请求地址添加 /hxh 前缀,示例代码如下:

@Configuration
public class AppConfig implements WebMvcConfigurer {
// 给所有接口添加 /hxh 前缀
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("/hxh", c -> true);
}
}

另一种方式是在application配置文件中配置:

server.servlet.context-path=/hxh

2 统一异常处理

统一异常处理是指 在应用程序中定义一个公共的异常处理机制,用来处理所有的异常情况。 这样可以避免在应用程序中分散地处理异常,降低代码的复杂度和重复度,提高代码的可维护性和可扩展性。

需要考虑以下几点:

  • 异常处理的层次结构:定义异常处理的层次结构,确定哪些异常需要统一处理,哪些异常需要交给上层处理。
  • 异常处理的方式:确定如何处理异常,比如打印日志、返回错误码等。
  • 异常处理的细节:处理异常时需要注意的一些细节,比如是否需要事务回滚、是否需要释放资源等

本文讲述的统一异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的:

  • @ControllerAdvice 表示控制器通知类。
  • @ExceptionHandler 异常处理器。

以上两个注解组合使用,表示当出现异常的时候执行某个通知,即执行某个方法事件,具体实现代码如下:

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.HashMap;

/**
* @author 兴趣使然黄小黄
* @version 1.0
* @date 2023/7/19 18:27
* 统一异常处理
*/
@ControllerAdvice // 声明是一个异常处理器
public class MyExHandler {

// 拦截所有的空指针异常, 进行统一的数据返回
@ExceptionHandler(NullPointerException.class) // 统一处理空指针异常
@ResponseBody // 返回数据
public HashMap<String, Object> nullException(NullPointerException e) {
HashMap<String, Object> result = new HashMap<>();
result.put("code", "-1"); // 与前端定义好的异常状态码
result.put("msg", "空指针异常: " + e.getMessage()); // 错误码的描述信息
result.put("data", null); // 返回的数据
return result;
}
}

上述代码中,实现了对所有空指针异常的拦截并进行统一的数据返回。

在实际中,常常设置一个保底,比如发生的非空指针异常,也会有保底措施进行处理,类似于 try-catch 块中使用 Exception 进行捕获,代码示例如下:

@ExceptionHandler(Exception.class) 
@ResponseBody
public HashMap<String, Object> exception(Exception e) {
HashMap<String, Object> result = new HashMap<>();
result.put("code", "-1"); // 与前端定义好的异常状态码
result.put("msg", "异常: " + e.getMessage()); // 错误码的描述信息
result.put("data", null); // 返回的数据
return result;
}

3 统一数据返回格式

为了保持 API 的一致性和易用性,通常需要使用统一的数据返回格式。 一般而言,一个标准的数据返回格式应该包括以下几个元素:

  • 状态码:用于标志请求成功失败的状态信息;
  • 消息:用来描述请求状态的具体信息;
  • 数据:包含请求的数据信息;
  • 时间戳:可以记录请求的时间信息,便于调试和监控。

实现统一的数据返回格式可以使用 @ControllerAdvice + ResponseBodyAdvice 的方式实现,具体步骤如下:

  • 创建一个类,并添加 @ControllerAdvice 注解;
  • 实现 ResponseBodyAdvice 接口,并重写 supports 和 beforeBodyWrite 方法。

示例代码如下:

import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.util.HashMap;

/**
* @author 兴趣使然黄小黄
* @version 1.0
* @date 2023/7/19 18:59
* 统一数据返回格式
*/
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {

/**
* 此方法返回 true 则执行下面的 beforeBodyWrite 方法, 反之则不执行
*/
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}

/**
* 方法返回之前调用此方法
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
HashMap<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg", "");
result.put("data", body);
return null;
}
}

但是,如果返回的 body 原始数据类型是 String ,则会出现类型转化异常,即 ClassCastException。

因此,如果原始返回数据类型为 String ,则需要使用 jackson 进行单独处理,实现代码如下:

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.util.HashMap;

/**
* @author 兴趣使然黄小黄
* @version 1.0
* @date 2023/7/19 18:59
* 统一数据返回格式
*/
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {

@Autowired
private ObjectMapper objectMapper;

/**
* 此方法返回 true 则执行下面的 beforeBodyWrite 方法, 反之则不执行
*/
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}

/**
* 方法返回之前调用此方法
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
HashMap<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg", "");
result.put("data", body);
if (body instanceof String) {
// 需要对 String 特殊处理
try {
return objectMapper.writeValueAsString(result);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
return result;
}
}

但是,在实际业务中,上述代码只是作为保底使用,因为状态码始终返回的是200,过于死板,还需要具体问题具体分析。

文章目录
  1. 1. 前言
  2. 2. 0 为什么需要统一功能处理?
  3. 3. 1 统一用户登录权限验证
    1. 3.0.1. 1.1 使用原生 Spring AOP 实现统一拦截的难点
    2. 3.0.2. 1.2 使用 Spring 拦截器实现统一用户登录验证
    3. 3.0.3. 1.3 拦截器的实现原理及源码分析
    4. 3.0.4. 1.4 统一访问前缀添加
  • 4. 2 统一异常处理
  • 5. 3 统一数据返回格式