SpringBoot 统一功能处理

一、用户登录拦截器

1、拦截器实现步骤

步骤1:自定义拦截器

// 自定义拦截器
@Component
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(AppVar.SESSION_KEY) != null) {
            // 返回 true -> 拦截器验证成功,继续执行后续的方法
            return true;
        }
        // 返回 false -> 拦截器验证失败,不会执行后续的目标方法
        return false;
    }
}

代码解析:

  1. 通过 @Component 注解将 LoginInterceptor 类标记为一个 Spring 组件,使其成为 Spring 容器中的一个可被管理的 Bean。

  2. LoginInterceptor 类实现了 Spring 提供的拦截器接口 HandlerInterceptor,并覆盖了其中的 preHandle 方法。preHandle 方法在目标方法执行前被调用。

  3. 在 preHandle 方法中,首先通过 HttpServletRequest 获取当前请求的 HttpSession 对象。如果 HttpSession 不为 null,且其中存储的 AppVar.SESSION_KEY 属性不为 null,表示用户已登录。

  4. 如果验证成功,即用户已登录,返回 true,表示拦截器验证通过,可以继续执行后续的目标方法;如果验证失败,即用户未登录,返回 false,表示拦截器验证失败,不会执行后续的目标方法。

步骤2:将自定义拦截器配置到系统设置中,并设置拦截规则

@Configuration
public class AppConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;
    // 在系统配置中添加拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/login.html")
                .excludePathPatterns("/reg.html")
                .excludePathPatterns("/css/**")
                .excludePathPatterns("/editor.md/**")
                .excludePathPatterns("/img/**")
                .excludePathPatterns("/js/**");
    }
}

代码解析:

  1. 通过 @Autowired 注解将 LoginInterceptor 注入到 AppConfig 类中,该拦截器在上面已经定义好了。

  2. 在 addInterceptors 方法中,通过 registry.addInterceptor(loginInterceptor) 将 LoginInterceptor 拦截器添加到拦截器链中。

  3. 使用 addPathPatterns 方法设置需要拦截的 URL,这里使用 “/**” 表示拦截所有的请求。

  4. 使用 excludePathPatterns 方法设置不需要拦截的 URL,这些路由在拦截器中会被忽略。这里排除了一些静态资源和特定路径,比如登录页、注册页、CSS 文件、图片文件和 JavaScript
    文件。

2、拦截器实现原理

本质上 Spring 中的拦截器也是通过动态代理和环绕通知的 思想 来实现的。在拦截器中,可以通过实现 HandlerInterceptor 接口并重写 preHandlepostHandleafterCompletion 方法来实现环绕通知的功能。

通过阅读源码我们可以看到,在 Spring 中所有 Controller 的执行都会通过一个核心调度器 DispatcherServlet 来实现,所有的请求方法都会执行 DispatcherServlet 中的 doDispatch 调度方法,doDispatch 方法中有一系列的事件处理方法,而在开始执行 Controller 中的目标方法 之前,会先调用预处理方法 applyPreHandle,在 applyPreHandle 方法中会获取所有拦截器 HandlerInterceptor 并执行拦截器中的 preHandle 方法。如果拦截器中有一个返回了 false 那么后续的流程就不会执行了。

二、统一异常处理

通过使用 @RestControllerAdvice(@ControllerAdvice+@ResponseBody) 注解和 @ExceptionHandler 注解结合使用,可以实现全局的或是针对特定异常的统一异常处理,并将处理结果以统一的数据格式返回给客户端。

@RestControllerAdvice
public class ExceptionAdvice {
    // 仅限于空指针异常的异常处理
    @ExceptionHandler(NullPointerException.class)
    public ResultAjax doNullPointException(NullPointerException e) {
        ResultAjax resultAjax = new ResultAjax();
        resultAjax.setCode(-1);
        resultAjax.setMsg("异常"+e.getMessage());
        return resultAjax;
    }
    
    // 适用于所有异常的异常处理
    @ExceptionHandler(Exception.class)
    public ResultAjax doException(Exception e) {
        ResultAjax resultAjax = new ResultAjax();
        resultAjax.setCode(-1);
        resultAjax.setMsg("异常"+e.getMessage());
        return resultAjax;
    }
}

代码解析:

  1. @RestControllerAdvice 注解表示该类是一个全局控制器增强器,并且结合了 @ControllerAdvice 和 @ResponseBody 注解的功能。

  2. @ExceptionHandler 注解标注异常处理的方法。搭配 @RestControllerAdvice 注解可以在发生异常时统一处理异常并返回数据。

  3. doNullPointException 方法使用 @ExceptionHandler(NullPointerException.class) 注解来指定它处理的异常类型为
    NullPointerException。当发生空指针异常时,该方法会被调用。在方法体内,创建一个 ResultAjax
    对象并设置相应的错误信息,然后将其返回。

  4. doException 方法没有指定特定的异常类型,因此它将会处理所有类型的异常。当发生任何异常时,该方法会被调用。它的处理逻辑与 doNullPointException 方法类似,也是创建一个 ResultAjax 对象并设置错误信息,然后返回。

三、统一数据返回格式

统一数据的返回格式可以降低前端程序员和后端程序员的沟通成本,方便前端程序员更好的接收和解析后端数据接口返回的数据。

一般情况下,我们可以创建一个统一返回对象,提供一些成功和失败的返回接口,后续返回的数据直接调用接口即可返回约定的统一对象。具体实现如下:

// 定义统一返回对象
@Data
public class ResultAjax {
    // 状态码
    private int code;
    // 状态码的描述信息
    private String msg;
    // 返回数据
    private Object data;

    // 返回成功对象
    public static ResultAjax succ(Object data) {
        ResultAjax resultAjax = new ResultAjax();
        resultAjax.setCode(200);
        resultAjax.setMsg("");
        resultAjax.setData(data);
        return resultAjax;
    }
    public static ResultAjax succ(String msg, Object data) {
        ResultAjax resultAjax = new ResultAjax();
        resultAjax.setCode(200);
        resultAjax.setMsg(msg);
        resultAjax.setData(data);
        return resultAjax;
    }
    // 返回失败对象
    public static ResultAjax fail(int code,String msg){
        ResultAjax resultAjax = new ResultAjax();
        resultAjax.setCode(code);
        resultAjax.setMsg(msg);
        resultAjax.setData(null);
        return resultAjax;
    }

    public static ResultAjax fail(int code,String msg,Object data){
        ResultAjax resultAjax = new ResultAjax();
        resultAjax.setCode(code);
        resultAjax.setMsg(msg);
        resultAjax.setData(data);
        return resultAjax;
    }
}

之后业务中所有返回类型都设置为上述定义的统一返回对象:

@RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/hello")
    public ResultAjax sayHello(){
        return ResultAjax.succ("hello");
    }
    @RequestMapping("/hi")
    public ResultAjax sayHi(){
        return ResultAjax.succ("hi");
    }
}

当然虽然做出了上面的约定,但也不能保证在之后的业务代码不会误用其他返回类型,这个时候就需要使用到统一返回值的保底策略了。可以在 @ControllerAdvice 注解的类中实现 ResponseBodyAdvice 接口,对所有控制器方法的返回值进行统一的处理。

// 执行统一返回数据的保底策略
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {

    @Autowired
    private ObjectMapper objectMapper;

    //     * true -> 才会调用 beforeBodyWrite 方法,
    //     * 反之则永远不会调用 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) {
        // 对返回值进行判断
        // 如果返回值和统一返回格式一致直接返回
        if (body instanceof ResultAjax) {
            return body;
        }
        // 对字符串返回格式进行单独判断处理
        if (body instanceof String) {
            ResultAjax resultAjax = ResultAjax.succ(body);
            try {
                return objectMapper.writeValueAsString(resultAjax);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }
        // 其他情况
        return ResultAjax.succ(body);
    }
}

代码解析:

  1. @ControllerAdvice 是一个注解,用于声明一个类为全局控制器增强器。在 @ControllerAdvice 注解的类中实现 ResponseBodyAdvice 接口,对所有控制器方法的返回值进行统一的处理
  2. 实现 supports 方法,判断当前返回类型是否需要进行响应体重写处理。由于这里返回 true,因此所有返回值都会被拦截并进行响应体重写处理。
  3. 实现 beforeBodyWrite 方法,对所有返回值进行统一的响应体处理。

四、@ControllerAdvice 实现原理(了解)

通过上面统一异常处理和统一数据返回格式的介绍,我们发现二者都使用到了 @ControllerAdvice 这个注解,下面我们简单介绍一下它的底层是怎么实现的:

@ControllerAdvice 它更像是一个全局的拦截器,可以对控制器的行为进行统一的处理和管理:

当我们点击 @ControllerAdvice 的源码,可以看到 @ControllerAdvice 同样派生于 @Component 组件,而所有组件初始化都会调用 InitializingBean 接口,其中 Spring MVC 中的实现的子类中有一个 afterPropertiesSet() 方法,表示所有的参数设置完成之后执行的方法,这个方法中又有一个 initControllerAdviceCache 方法,当程序执行到特定事件发生的时候,比如返回数据前或发生异常时,Spring会根据规则查找所有使用了@ControllerAdvice 注解的类,并调用其中对应的 Advice 方法来执行相应的业务逻辑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不摸鱼的程序员

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值