当前位置: 首页 > news >正文

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

文章目录

  • 2. 统一数据返回格式
    • 回顾
      • 后端统一返回结果
      • 后端逻辑处理
    • 2.1 快速入门
    • 2.2 存在问题
    • 2.3 优点
  • 3. 统一异常处理
  • 4. @ControllerAdvice 源码分析
    • 1. initHandlerAdapters(context)
    • 2. initHandlerExceptionResolvers(context)
  • 总结

2. 统一数据返回格式

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

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

回顾

后端统一返回结果

@Data
public class Result<T> {private int status;private String errorMessage;private T data;
}

后端逻辑处理

@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 注解

@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}@Overridepublic 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类型时才有这种错误发生

@RequestMapping("/test")
@RestControllerpublic class TestController {@RequestMapping("/t1")public String t1(){return "t1";}@RequestMapping("/t2")public boolean t2(){return true;}@RequestMapping("/t3")public Integer t3(){return 200;}
}
  • 解决方案
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {@Autowiredprivate ObjectMapper objectMapper;@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}@SneakyThrows@Overridepublic 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 )
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapterimplements 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
@ControllerAdvice
@ResponseBody
public class ErrorAdvice {@ExceptionHandlerpublic Object handler(Exception e) {return Result.fail(e.getMessage());}
}

类名,方法名和返回值可以自定义,重要的是注解
接口返回为数据时,需要加 @ResponseBody 注解

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

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

@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 实现源码如下:

@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标注的方法

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的一些源码.
http://www.dtcms.com/a/605447.html

相关文章:

  • 使用SOM进行图像颜色量化
  • map的遍历
  • 百度站内搜索永久域名查询
  • 【Java Web学习 | 第九篇】JavaScript(3) 数组+函数
  • MANUS 数据手套:手部跟踪工作流程指南
  • Qt的信号槽机制是线程安全的吗?
  • Go语言编译:深入了解Go编译原理与性能优化 | 探索Go编译器背后的工作原理及性能提升技巧
  • Unity为什么推荐在FixedUpdate处理物理模拟?
  • 鄂城网站建设大连网站建设哪个公司好
  • 上海专业网站建设渠道用帝国cms做视频网站
  • RocketMQ消费组详解:构建高可用消息消费系统
  • leetcode 63 不同路径II
  • 网站的当前位置导航如何做免费域名注册免费空间
  • 研发管理知识库(12)阿里“云效”使用方案简介
  • 中文共情对话数据集2023年和2025年
  • 如何制作网站的步骤网店美工的作用
  • 网站标ico怎么做苏州seo推广公司
  • springboot实现跨服务调用/springboot调用另一台机器上的服务
  • 代价复杂度剪枝(CCP)详解:原理、实现与应用
  • 温州微网站公司看网红直播做爰的网站
  • Katalon Studio的功能介绍
  • 电子商务公司的经营范围企业seo关键词优化
  • 定义 LLM 格局:开源与闭源两大阵营的较量
  • 数智化时代:AI技术重构企业财务管理系统的底层逻辑与实践
  • 阿里“千问”破局C端AI:开源基石与B端势能的双重革命
  • 从生成内容角度介绍开源AI大模型
  • 碳中和终极武器——嵌入式AI重构能源管理战局
  • RikkaHub 1.6.11 | 开源的本地大型语言模型聚合应用,支持多种AI服务提供商
  • 企业网站及公众号建设方案河南郑州解封通知
  • ios包体积管理方案