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

实现

新建业务异常

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

@Data
@AllArgsConstructor
public final class BusinessException extends RuntimeException {

    private String errorCode;

    private String errorMsg;

}

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

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

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

配置

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

@RestControllerAdvice("com.example.unifiedreturn.api")
static class UnifiedExceptionHandler{

    @ExceptionHandler(BusinessException.class)
    public CommonResult<Void> handleBusinessException(BusinessException be){
        return CommonResult.errorResult(be.getErrorCode(), be.getErrorMsg());
    }
}

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

测试

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

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

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

在 Service 中抛出异常:

@Service
public class UserServiceImpl implements UserService {

    /**
     * 根据用户ID查询用户
     *
     * @param id
     * @return
     */
    @Override
    public UserVo getUserById(Long id) {
        throw new BusinessException("1001", "根据ID查询用户异常");
    }
}

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

解剖实现过程

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

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

@Bean
public HandlerExceptionResolver handlerExceptionResolver() {
    List<HandlerExceptionResolver> exceptionResolvers = new ArrayList<>();
    configureHandlerExceptionResolvers(exceptionResolvers);
    if (exceptionResolvers.isEmpty()) {
        addDefaultHandlerExceptionResolvers(exceptionResolvers);
    }
    extendHandlerExceptionResolvers(exceptionResolvers);
    HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
    composite.setOrder(0);
    composite.setExceptionResolvers(exceptionResolvers);
    return composite;
}

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

@Override
public void afterPropertiesSet() {
    // Do this first, it may add ResponseBodyAdvice beans
    initExceptionHandlerAdviceCache();
    ...
}

private void initExceptionHandlerAdviceCache() {
    if (getApplicationContext() == null) {
        return;
    }

    List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
    AnnotationAwareOrderComparator.sort(adviceBeans);

    for (ControllerAdviceBean adviceBean : adviceBeans) {
        Class<?> beanType = adviceBean.getBeanType();
        if (beanType == null) {
            throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
        }
        // 重点看这个构造方法
        ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
        if (resolver.hasExceptionMappings()) {
            this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
        }
        if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
            this.responseBodyAdvice.add(adviceBean);
        }
    }
}

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

/**
 * A constructor that finds {@link ExceptionHandler} methods in the given type.
 * @param handlerType the type to introspect
 */
public ExceptionHandlerMethodResolver(Class<?> handlerType) {
    for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
        for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
            addExceptionMapping(exceptionType, method);
        }
    }
}

/**
 * Extract exception mappings from the {@code @ExceptionHandler} annotation first,
 * and then as a fallback from the method signature itself.
 */
@SuppressWarnings("unchecked")
private List<Class<? extends Throwable>> detectExceptionMappings(Method method) {
    List<Class<? extends Throwable>> result = new ArrayList<>();
    detectAnnotationExceptionMappings(method, result);
    if (result.isEmpty()) {
        for (Class<?> paramType : method.getParameterTypes()) {
            if (Throwable.class.isAssignableFrom(paramType)) {
                result.add((Class<? extends Throwable>) paramType);
            }
        }
    }
    if (result.isEmpty()) {
        throw new IllegalStateException("No exception types mapped to " + method);
    }
    return result;
}

private void detectAnnotationExceptionMappings(Method method, List<Class<? extends Throwable>> result) {
    ExceptionHandler ann = AnnotatedElementUtils.findMergedAnnotation(method, ExceptionHandler.class);
    Assert.state(ann != null, "No ExceptionHandler annotation");
    result.addAll(Arrays.asList(ann.value()));
}

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

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

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    ...

    try {
        ModelAndView mv = null;
        Exception dispatchException = null;

        try {
            ...
            // 当请求发生异常,该方法会通过 catch 捕获异常
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
        ...

        }
        catch (Exception ex) {
            dispatchException = ex;
        }
        catch (Throwable err) {
            // As of 4.3, we're processing Errors thrown from handler methods as well,
            // making them available for @ExceptionHandler methods and other scenarios.
            dispatchException = new NestedServletException("Handler dispatch failed", err);
        }
        // 调用该方法分析捕获的异常
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }
    ...
}

接下来,我们来看 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. Skyline WEB端开发2——添加一个定位点、文本标签

    Skyline 添加定位点 sgworld.Creator.CreatePosition CreatePosition( X, //兴趣点的东西方向坐标,即经度 Y, //兴趣点的南北方向坐标,即纬度 ...

  2. MyBatis从入门到精通(十三):使用discriminator鉴别器映射

    最近在读刘增辉老师所著的<MyBatis从入门到精通>一书,很有收获,于是将自己学习的过程以博客形式输出,如有错误,欢迎指正,如帮助到你,不胜荣幸! 本篇博客主要讲解鉴别器映射discri ...

  3. Excel公式中问题-记住不要忽略空格!

    总结一下之前犯得愚蠢的小问题: 程序:每日报表:从DB下载数据填充到excel,包括3个sheet,sheet1:总结<模板,公式填充,数据源为sheet2,sheet3>;sheet2: ...

  4. [Spring-Cloud-Alibaba] Sentinel 整合RestTemplate & Feign

    Sentinel API Github : WIKI Sphu (指明要保护的资源名称) Tracer (指明调用来源,异常统计接口) ContextUtil(标示进入调用链入口) 流控规则(针对来源 ...

  5. NameNode和SecondaryNameNode的工作机制

    NameNode&Secondary NameNode 工作机制 NameNode: 1.启动时,加载编辑日志和镜像文件到内存 2.当客户端对元数据进行增删改,请求NameNode 3.Nam ...

  6. sql注入篇1

    一.前言 学习了感觉很久的渗透,总结一下sql注入,系统整理一下sql注入思路. 二.关于sql注入 所谓SQL注入,就是通过把SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到 ...

  7. 原生 js基础常用的判断和循环

    原生 js基础常用的判断和循环 以下部分是个人实践及和搜集的资料: 最常用的if判断语句: if (/* 条件表达式 */){ // 成立执行语句 } else { // 否则执行语句 } 原生js的 ...

  8. vue教程(五)--路由router介绍

    一.html页面中如何使用 1.引入 vue-router.js 2.安装插件 Vue.use(VueRouter) 3.创建路由对象 var router = new VueRouter({ // ...

  9. python课堂整理4---列表的魔法

    一.list   类, 列表 li = [1, 12, 9, "age", ["大白", "小黑"], "alex"] ...

  10. Redis项目实战---应用及理论(上)---redis基础知识介绍

    redis(Remote Dictionary Server)   一.原理及特性层面:     1.优势:        1)数据加载在内存中,执行速度快, 数据结构类似于HashMap,HashM ...