上一篇文章RESTful API 返回统一JSON数据格式 说明了 RESTful API 统一返回数据格式问题,这是请求一切正常的情形,这篇文章将说明如何统一处理异常,以及其背后的实现原理,老套路,先实现,后说明原理,有了上一篇文章的铺底,相信,理解这篇文章就驾轻就熟了

实现

新建业务异常

新建 BusinessException.class 类表示业务异常,注意这是一个 Runtime 异常

  1. @Data
  2. @AllArgsConstructor
  3. public final class BusinessException extends RuntimeException {
  4. private String errorCode;
  5. private String errorMsg;
  6. }

添加统一异常处理静态方法

在 CommonResult 类中添加静态方法 errorResult 用于接收异常码和异常消息:

  1. public static <T> CommonResult<T> errorResult(String errorCode, String errorMsg){
  2. CommonResult<T> commonResult = new CommonResult<>();
  3. commonResult.errorCode = errorCode;
  4. commonResult.errorMsg = errorMsg;
  5. commonResult.status = -1;
  6. return commonResult;
  7. }

配置

同样要用到 @RestControllerAdvice 注解,将统一异常添加到配置中:

  1. @RestControllerAdvice("com.example.unifiedreturn.api")
  2. static class UnifiedExceptionHandler{
  3. @ExceptionHandler(BusinessException.class)
  4. public CommonResult<Void> handleBusinessException(BusinessException be){
  5. return CommonResult.errorResult(be.getErrorCode(), be.getErrorMsg());
  6. }
  7. }

三部搞定,到这里无论是 Controller 还是 Service 中,只要抛出 BusinessException, 我们都会返回给前端一个统一数据格式

测试

将 UserController 中的方法进行改造,直接抛出异常:

  1. @GetMapping("/{id}")
  2. public UserVo getUserById(@PathVariable Long id){
  3. throw new BusinessException("1001", "根据ID查询用户异常");
  4. }

浏览器中输入: http://localhost:8080/users/1

在 Service 中抛出异常:

  1. @Service
  2. public class UserServiceImpl implements UserService {
  3. /**
  4. * 根据用户ID查询用户
  5. *
  6. * @param id
  7. * @return
  8. */
  9. @Override
  10. public UserVo getUserById(Long id) {
  11. throw new BusinessException("1001", "根据ID查询用户异常");
  12. }
  13. }

运行是得到同样的结果,所以我们尽可能的抛出异常吧 (作为一个程序猿这种心理很可拍)

解剖实现过程

解剖这个过程是相当纠结的,为了更好的说(yin)明(wei)问(wo)题(lan),我要说重中之重了,真心希望看该文章的童鞋自己去案发现场发现线索

还是在 WebMvcConfigurationSupport 类中实例化了 HandlerExceptionResolver Bean

  1. @Bean
  2. public HandlerExceptionResolver handlerExceptionResolver() {
  3. List<HandlerExceptionResolver> exceptionResolvers = new ArrayList<>();
  4. configureHandlerExceptionResolvers(exceptionResolvers);
  5. if (exceptionResolvers.isEmpty()) {
  6. addDefaultHandlerExceptionResolvers(exceptionResolvers);
  7. }
  8. extendHandlerExceptionResolvers(exceptionResolvers);
  9. HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
  10. composite.setOrder(0);
  11. composite.setExceptionResolvers(exceptionResolvers);
  12. return composite;
  13. }

和上一篇文章一毛一样的套路,ExceptionHandlerExceptionResolver 实现了 InitializingBean 接口,重写了 afterPropertiesSet 方法:

  1. @Override
  2. public void afterPropertiesSet() {
  3. // Do this first, it may add ResponseBodyAdvice beans
  4. initExceptionHandlerAdviceCache();
  5. ...
  6. }
  7. private void initExceptionHandlerAdviceCache() {
  8. if (getApplicationContext() == null) {
  9. return;
  10. }
  11. List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
  12. AnnotationAwareOrderComparator.sort(adviceBeans);
  13. for (ControllerAdviceBean adviceBean : adviceBeans) {
  14. Class<?> beanType = adviceBean.getBeanType();
  15. if (beanType == null) {
  16. throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
  17. }
  18. // 重点看这个构造方法
  19. ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
  20. if (resolver.hasExceptionMappings()) {
  21. this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
  22. }
  23. if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
  24. this.responseBodyAdvice.add(adviceBean);
  25. }
  26. }
  27. }

重点看上面我用注释标记的构造方法,代码很好懂,仔细看看吧

  1. /**
  2. * A constructor that finds {@link ExceptionHandler} methods in the given type.
  3. * @param handlerType the type to introspect
  4. */
  5. public ExceptionHandlerMethodResolver(Class<?> handlerType) {
  6. for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
  7. for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
  8. addExceptionMapping(exceptionType, method);
  9. }
  10. }
  11. }
  12. /**
  13. * Extract exception mappings from the {@code @ExceptionHandler} annotation first,
  14. * and then as a fallback from the method signature itself.
  15. */
  16. @SuppressWarnings("unchecked")
  17. private List<Class<? extends Throwable>> detectExceptionMappings(Method method) {
  18. List<Class<? extends Throwable>> result = new ArrayList<>();
  19. detectAnnotationExceptionMappings(method, result);
  20. if (result.isEmpty()) {
  21. for (Class<?> paramType : method.getParameterTypes()) {
  22. if (Throwable.class.isAssignableFrom(paramType)) {
  23. result.add((Class<? extends Throwable>) paramType);
  24. }
  25. }
  26. }
  27. if (result.isEmpty()) {
  28. throw new IllegalStateException("No exception types mapped to " + method);
  29. }
  30. return result;
  31. }
  32. private void detectAnnotationExceptionMappings(Method method, List<Class<? extends Throwable>> result) {
  33. ExceptionHandler ann = AnnotatedElementUtils.findMergedAnnotation(method, ExceptionHandler.class);
  34. Assert.state(ann != null, "No ExceptionHandler annotation");
  35. result.addAll(Arrays.asList(ann.value()));
  36. }

到这里,我们用 @RestControllerAdvice@ExceptionHandler 注解就会被 Spring 扫描到上下文,供我们使用

让我们回到你最熟悉的调用的入口 DispatcherServlet 类的 doDispatch 方法:

  1. protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
  2. ...
  3. try {
  4. ModelAndView mv = null;
  5. Exception dispatchException = null;
  6. try {
  7. ...
  8. // 当请求发生异常,该方法会通过 catch 捕获异常
  9. mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
  10. ...
  11. }
  12. catch (Exception ex) {
  13. dispatchException = ex;
  14. }
  15. catch (Throwable err) {
  16. // As of 4.3, we're processing Errors thrown from handler methods as well,
  17. // making them available for @ExceptionHandler methods and other scenarios.
  18. dispatchException = new NestedServletException("Handler dispatch failed", err);
  19. }
  20. // 调用该方法分析捕获的异常
  21. processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
  22. }
  23. ...
  24. }

接下来,我们来看 processDispatchResult 方法,这里只要展示调用栈你就会眼前一亮了:

总结

上一篇文章的返回统一数据格式是基础,当异常情况发生时,只不过需要将异常信息提取出来。文章的好多地方设计方式不可取,比如我们最好将异常封装在一个 Enum 类,通过 enum 对象抛出异常等,如果你用到这些,去完善你的设计方案吧

回复 「demo」,打开链接,查看文件夹 「unifiedreturn」下内容,获取完整代码

灵魂追问

  1. 这两篇文章,你学到了哪些设计模式?
  2. 你能熟练的使用反射吗?当看源码是会看到很多反射的应用
  3. 你了解 Spring CGLIB 吗?它的工作原理是什么?

提高效率工具

JSON-Viewer

JSON-Viewer 是 Chrome 浏览器的插件,用于快速解析及格式化 json 内容,在 Chrome omnibox(多功能输入框)输入json-viewer + TAB ,将 json 内容拷贝进去,然后输入回车键,将看到结构清晰的 json 数据,同时可以自定义主题

另外,前端人员打开开发者工具,双击请求链接,会自动将 response 中的 json 数据解析出来,非常方便

推荐阅读


欢迎持续关注公众号:「日拱一兵」

  • 前沿 Java 技术干货分享
  • 高效工具汇总 | 回复「工具」
  • 面试问题分析与解答
  • 技术资料领取 | 回复「资料」

以读侦探小说思维轻松趣味学习 Java 技术栈相关知识,本着将复杂问题简单化,抽象问题具体化和图形化原则逐步分解技术问题,技术持续更新,请持续关注......

每天用SpringBoot,还不懂RESTful API返回统一数据格式是怎么实现的?的更多相关文章

  1. Spring Boot 无侵入式 实现RESTful API接口统一JSON格式返回

    前言 现在我们做项目基本上中大型项目都是选择前后端分离,前后端分离已经成了一个趋势了,所以总这样·我们就要和前端约定统一的api 接口返回json 格式, 这样我们需要封装一个统一通用全局 模版api ...

  2. 使用 SpringBoot 构建一个RESTful API

    目录 背景 创建 SpringBoot 项目/模块 SpringBoot pom.xml api pom.xml 创建 RESTful API 应用 @SpringBootApplication @C ...

  3. 只需一步,在Spring Boot中统一Restful API返回值格式与统一处理异常

    ## 统一返回值 在前后端分离大行其道的今天,有一个统一的返回值格式不仅能使我们的接口看起来更漂亮,而且还可以使前端可以统一处理很多东西,避免很多问题的产生. 比较通用的返回值格式如下: ```jav ...

  4. Restful api 返回值重复的问题

    Spring boot全家桶前后端分离的项目,在扩充某一个列表形式的返回值时,发现返回值出现了一批的重复. 正常的数据返回: 数值完全一致只是参数名称区分了大小写,如下图: 推测可能是restful格 ...

  5. 从 0 使用 SpringBoot MyBatis MySQL Redis Elasticsearch打造企业级 RESTful API 项目实战

    大家好!这是一门付费视频课程.新课优惠价 699 元,折合每小时 9 元左右,需要朋友的联系爱学啊客服 QQ:3469271680:我们每课程是明码标价的,因为如果售价为现在的 2 倍,然后打 5 折 ...

  6. Django编写RESTful API(二):请求和响应

    欢迎访问我的个人网站:www.comingnext.cn 前言 在上一篇文章,已经实现了访问指定URL就返回了指定的数据,这也体现了RESTful API的一个理念,每一个URL代表着一个资源.当然我 ...

  7. 使用ASP.NET Core 3.x 构建 RESTful API - 3.1 资源命名

    之前讲了RESTful API的统一资源接口这个约束,里面提到了资源是通过URI来进行识别的,每个资源都有自己的URI.URI里还涉及到资源的名称,而针对资源的名称却没有一个标准来进行规范,但是业界还 ...

  8. 使用ASP.NET Core 3.x 构建 RESTful API - 3.4 内容协商

    现在,当谈论起 RESTful Web API 的时候,人们总会想到 JSON.但是实际上,JSON 和 RESTful API 没有半毛钱关系,只不过 JSON 恰好是RESTful API 结果的 ...

  9. PHPer的项目RESTful API设计规范是怎样的?

    RESTful 是目前最流行的 API 设计规范,用于 Web 数据接口的设计. 什么是RESTful RESTful是一种软件设计风格, 主要用于客户端与服务端交互的软件. 一般来说RESTful ...

随机推荐

  1. scrapy基础知识之防止爬虫被反的几个策略::

    动态设置User-Agent(随机切换User-Agent,模拟不同用户的浏览器信息) 禁用Cookies(也就是不启用cookies middleware,不向Server发送cookies,有些网 ...

  2. SFC20 功能例子 注解

    谁能够把这注解一下,给大家分享一下,谢谢了 LAR1  P##SOURCE       L     B#16#10       T     LB [AR1,P#0.0]       L     B#1 ...

  3. Discuz ML! V3.X 代码注入漏洞

    Discuz ML! V3.X 代码注入漏洞 前言 Discuz!ML是一个由CodersClub.org创建的多语言,集成,功能齐全的开源网络平台,用于构建像"社交网络"这样的互 ...

  4. Python 定义自己的常量类

    在实际的程序开发中,我们通常会将一个不可变的变量声明为一个常量.在很多高级语言中都会提供常量的关键字来定义常量,如 C++ 中的 const , Java 中的 final 等,但是 Python 语 ...

  5. [Lydsy2017年4月月赛]抵制克苏恩题解

    考试的时候以为就是简单的概率期望题,考完后知道是简单的概率期望DP题,完美爆零. 这道题数据范围很小,很容易让人想到状压,不过貌似没什么可压的.那么只能说明这道题复杂度很高了,状态数组f[o][i][ ...

  6. [NOIP2009]靶形数独 题解

    407. [NOIP2009] 靶形数独 时间限制:5 s   内存限制:128 MB [问题描述] 小城和小华都是热爱数学的好学生,最近,他们不约而同地迷上了数独游戏,好胜的他们想用数独来一比高低. ...

  7. 【二分讲解及例题】火车站台连锁店-C++

    首先我们先来从一个小游戏理解一下二分.(摘自程序员小灰的博客) 为什么说这样效率最高呢?因为每一次选择数字,无论偏大还是偏小,都可以让剩下的选择范围缩小一半. 给定范围0到1000的整数: 第一次我们 ...

  8. windbg 配置符号路径

    (转)WINDBG的符号下载与符号路径问题 安装与配置 windbg 的 symbol (符号) 本篇是新手自己写的一点心得.建议新手看看.同时希望前辈多多指教. 写这篇的动机:在网上找了一上午的 w ...

  9. Kafka学习(三)-------- Kafka核心之Cosumer

    了解了什么是kafka( https://www.cnblogs.com/tree1123/p/11226880.html)以后 学习核心api之消费者,kafka的消费者经过几次版本变化,特别容易混 ...

  10. codemirror使用

    JS使用 使用bower下载 bower i codemirror 引入样式文件 <link rel="stylesheet" type="text/css&quo ...