【统一功能处理】SpringBoot 统一功能专题:拦截器、数据封装、异常处理及 DispatcherServlet 源码初探

文章目录

  • [2. 统一数据返回格式](#2. 统一数据返回格式)
  • [3. 统一异常处理](#3. 统一异常处理)
  • [4. @ControllerAdvice 源码分析](#4. @ControllerAdvice 源码分析)
    • [1. initHandlerAdapters(context)](#1. initHandlerAdapters(context))
    • [2. initHandlerExceptionResolvers(context)](#2. initHandlerExceptionResolvers(context))
  • 总结

2. 统一数据返回格式

强制登录案例中,我们共做了两部分工作

  1. 通过Session来判断用户是否登录
  2. 对后端返回数据进行封装,告知前端处理的结果

回顾

后端统一返回结果

java 复制代码
@Data
public class Result<T> {
    private int status;
    private String errorMessage;
    private T data;
}

后端逻辑处理

java 复制代码
@RequestMapping("/getListByPage")
public Result getListByPage(PageRequest pageRequest) {
    log.info("获取图书列表,pageRequest:{}", pageRequest);
    //用户登录,返回图书列表
    PageResult<BookInfo> pageResult = bookService.getBookListByPage(pageRequest);
    log.info("获取图书列表222,pageRequest:{}", pageResult);
    return Result.success(pageResult);
}

Result.success(pageResult) 就是对返回数据进行了封装

拦截器帮我们实现了第一个功能,接下来看SpringBoot对第二个功能如何支持,如何对后端返回数据进行封装,告知前端处理的结果

2.1 快速入门

统一的数据返回格式使用 @ControllerAdvice 和 ResponseBodyAdvice 的方式实现

@ControllerAdvice 表示控制器通知类

添加类 ResponseAdvice ,实现 ResponseBodyAdvice 接口,并在类上添加@ControllerAdvice 注解

java 复制代码
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {

    @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) {
        return Result.success(body);
    }
}

测试

测试接口:http://127.0.0.1:8080/book/queryBookById?bookId=1

添加统一数据返回格式之前:

添加统一数据返回格式之后:

2.2 存在问题

问题现象:

我们继续测试修改图书的接口:http://127.0.0.1:8080/book/updateBook

结果显示,发生内部错误

查看数据库,发现数据操作成功

查看日志,日志报错

多测试几种不同的返回结果,发现只有返回结果为String类型时才有这种错误发生

java 复制代码
@RequestMapping("/test")
@RestController


public class TestController {
    @RequestMapping("/t1")
    public String t1(){
        return "t1";
    }
    @RequestMapping("/t2")
    public boolean t2(){
        return true;
    }
    @RequestMapping("/t3")
    public Integer t3(){
        return 200;
    }
}
  • 解决方案
java 复制代码
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {

        if (body instanceof String){
            return objectMapper.writeValueAsString(Result.success(body));
        }
        if (body instanceof Result) {
            return body;
        }
        return Result.success(body);
    }
}
  • 原因分析:
    SpringMVC默认会注册一些自带的 HttpMessageConverter
    (从先后顺序排列分别为
    ByteArrayHttpMessageConverter
    StringHttpMessageConverteSourceHttpMessageConverter
    SourceHttpMessageConverterAllEncompassingFormHttpMessageConverter )
java 复制代码
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
    implements BeanFactoryAware, InitializingBean {

    //...

    public RequestMappingHandlerAdapter() {
        this.messageConverters = new ArrayList<>(4);
        this.messageConverters.add(new ByteArrayHttpMessageConverter());
        this.messageConverters.add(new StringHttpMessageConverter());
        if (!shouldIgnoreXml) {
            try {
                this.messageConverters.add(new SourceHttpMessageConverter<>());
            }
            catch (Error err) {
                // Ignore when no TransformerFactory implementation is available
            }
        }

        this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
    }
    //...
}

其中AllEncompassingFormHttpMessageConverter 会根据项目依赖情况 添加对应的
HttpMessageConverter

Spring会根据返回的数据类型, 从 messageConverters 链选择合适的
HttpMessageConverter .

当返回的数据是非字符串时,使用的 MappingJackson2HttpMessageConverter 写入返回对象.

当返回的数据是字符串时, StringHttpMessageConverter 会先被遍历到,这时会认为
StringHttpMessageConverter 可以使用.

((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage)的处理中,调用父类的write方法

由于StringHttpMessageConverter重写了addDefaultHeaders方法,所以会执行子类的方法
然而子类 StringHttpMessageConverteraddDefaultHeaders方法定义接收参数为String,此时为Result类型,所以出现类型不匹配"Result cannot be cast to java.lang.String"的异常

2.3 优点

  1. 方便前端程序员更好的接收和解析后端数据接口返回的数据
  2. 降低前端程序员和后端程序员的沟通成本,按照某个格式实现就可以了,因为所有接口都是这样返回的.
  3. 有利于项目统一数据的维护和修改.
  4. 有利于后端技术部门的统一规范的标准制定,不会出现稀奇古怪的返回内容.

3. 统一异常处理

统一异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的,
@ControllerAdvice 表示控制器通知类,@ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执行某个通知,也就是执行某个方法事件

具体代码如下:

java 复制代码
```java
@ControllerAdvice
@ResponseBody
public class ErrorAdvice {

    @ExceptionHandler
    public Object handler(Exception e) {
        return Result.fail(e.getMessage());
    }
}

类名,方法名和返回值可以自定义,重要的是注解

接口返回为数据时,需要加 @ResponseBody 注解

以上代码表示,如果代码出现Exception异常(包括Exception的子类),就返回一个 Result的对象,Result对象的设置参考 Result.fail(e.getMessage())

我们可以针对不同的异常,返回不同的结果

java 复制代码
@Slf4j
@ControllerAdvice
@ResponseBody
//@RestControllerAdvice
public class ExceptionAdvice {
    //    @ExceptionHandler
//    public Result handler(Exception e){
//        log.error("发生异常, e:", e);
//        return Result.fail("内部错误, 请联系管理员");
//    }
//
//    @ExceptionHandler
//    public Result handler(NullPointerException e){
//        log.error("发生异常, e:", e);
//        return Result.fail("发生空指针异常, 请联系管理员");
//    }
//
//    @ExceptionHandler
//    public Result handler(IndexOutOfBoundsException e){
//        log.error("发生异常, e:", e);
//        return Result.fail("数组越界异常, 请联系管理员");
//    }

    @ExceptionHandler(Exception.class)
    public Result handler(Exception e){
        log.error("发生异常, e:", e);
        return Result.fail("内部错误, 请联系管理员");
    }

    @ExceptionHandler(NullPointerException.class)
    public Result handler2(Exception e){
        log.error("发生异常, e:", e);
        return Result.fail("发生空指针异常, 请联系管理员");
    }

    @ExceptionHandler(IndexOutOfBoundsException.class)
    public Result handler3(Exception e){
        log.error("发生异常, e:", e);
        return Result.fail("数组越界异常, 请联系管理员");
    }

}

4. @ControllerAdvice 源码分析

统一数据返回和统一异常都是基于 @ControllerAdvice 注解来实现的,通过分析@ControllerAdvice 的源码,可以知道他们的执行流程.

点击 @ControllerAdvice 实现源码如下:

java 复制代码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {

    @AliasFor("basePackages")
    String[] value() default {};

    @AliasFor("value")
    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};

    Class<?>[] assignableTypes() default {};

    Class<? extends Annotation>[] annotations() default {};
}

从上述源码可以看出 @ControllerAdvice 派生于 @Component 组件,这也就是为什么没有五大注解, ControllerAdvice 就生效的原因.

下面我们看看Spring是怎么实现的,还是从 DispatcherServlet 的代码开始分析.
DispatcherServlet 对象在创建时会初始化一系列的对象:

对于 @ControllerAdvice 注解,我们重点关注 initHandlerAdapters(context)initHandlerExceptionResolvers(context) 这两个方法.

1. initHandlerAdapters(context)

initHandlerAdapters(context) 方法会取得所有实现了 HandlerAdapter 接口的bean并保存起来,其中有一个类型为 RequestMappingHandlerAdapter 的bean,这个bean就是 @RequestMapping 注解能起作用的关键,这个bean在应用启动过程中会获取所有被 @ControllerAdvice 注解标注的bean对象,并做进一步处理

这个方法在执行时会查找使用所有的 @ControllerAdvice 类,把 ResponseBodyAdvice 类放在容器中,当发生某个事件时,调用相应的 Advice 方法,比如返回数据前调用统一数据封装

2. initHandlerExceptionResolvers(context)

接下来看 DispatcherServletinitHandlerExceptionResolvers(context) 方法,这个方法会取得所有实现了 HandlerExceptionResolver 接口的bean并保存起来,其中就有一个类型为 ExceptionHandlerExceptionResolver 的bean,这个bean在应用启动过程中会获取所有被 @ControllerAdvice 注解标注的bean对象做进一步处理

当Controller抛出异常时,DispatcherServlet 通过ExceptionHandlerExceptionResolver 来解析异常,而ExceptionHandlerExceptionResolver 又通过 ExceptionHandlerMethodResolver来解析异常,ExceptionHandlerMethodResolver 最终解析异常找到适用的@ExceptionHandler标注的方法

java 复制代码
public class ExceptionHandlerMethodResolver {

    //...

    private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
        List<Class<? extends Throwable>> matches = new ArrayList();
        //根据异常类型,查找匹配的异常处理方法
        //比如NullPointerException会匹配两个异常处理方法:
        //handler(Exception e) 和 handler(NullPointerException e)
        for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
            if (mappedException.isAssignableFrom(exceptionType)) {
                matches.add(mappedException);
            }
        }
        //如果找到多个匹配,就进行排序,找到最使用的方法。排序的规则依据抛出异常相对于声明异常的深度
        //比如抛出的是NullPointerException(继承于RuntimeException,RuntimeException又继承于Exception)
        //相对于handler(NullPointerException e) 声明的NullPointerException深度为0,
        //相对于handler(Exception e) 声明的Exception 深度 为2
        //所以 handler(NullPointerException e)标注的方法会排在前面
        if (!matches.isEmpty()) {
            if (matches.size() > 1) {
                matches.sort(new ExceptionDepthComparator(exceptionType));
            }
            return this.mappedMethods.get(matches.get(0));
        } else {
            return NO_MATCHING_EXCEPTION_HANDLER_METHOD;
        }
    }
    //...
}

总结

主要介绍了SpringBoot对一些统一功能的处理支持.

  1. 拦截器的实现主要分两部分: 1. 定义拦截器(实现HandlerInterceptor接口) 2. 配置拦截器
  2. 统一数据返回格式通过@ControllerAdvice + ResponseBodyAdvice 来实现
  3. 统一异常处理使用@ControllerAdvice + @ExceptionHandler 来实现,并且可以分异常来处理
  4. 了解了DispatcherServlet的一些源码.
相关推荐
Hilaku2 小时前
我为什么说全栈正在杀死前端?
前端·javascript·后端
恸流失2 小时前
集合练习1
java
LiLiYuan.2 小时前
Arrays类和List接口的关联
java·开发语言·windows·python
stay_awake__2 小时前
Maven+mybatis
java·maven
q***46522 小时前
如何使用Spring Boot框架整合Redis:超详细案例教程
spring boot·redis·后端
武子康2 小时前
Java-170 Neo4j 事务、索引与约束实战:语法、并发陷阱与速修清单
java·开发语言·数据库·sql·nosql·neo4j·索引
q***23573 小时前
在2023idea中如何创建SpringBoot
java·spring boot·后端
OlahOlah3 小时前
深入理解 Spring Bean 生命周期:从实例化到销毁
后端