Spring Boot 统一RESTful接口响应和统一异常处理

简介: 基于Spring Boot 框架开发的应用程序,大部分都是以提供RESTful接口为主要的目的。前端或者移动端开发人员通过调用后端提供的RESTful接口完成数据的交换。统一的RESTful接口响应数据结构是基本的开发规范。能够减少团队内部不必要的沟通;减轻接口消费者校验数据的负担;降低其他同事接手代码的难度;提高接口的健壮性和可扩展性。 统一的异常处理,是系统完备性的基本象征。通过对全局异常信息的捕获,能够避免将异常信息和系统敏感信息直接抛给客户端;针对特定类型异常捕获之后可以重新对输出数据做编排,提高交互友好度,同时可以记录异常信息以便监控和分析。

一、引言

基于Spring Boot 框架开发的应用程序,大部分都是以提供RESTful接口为主要的目的。前端或者移动端开发人员通过调用后端提供的RESTful接口完成数据的交换。

统一的RESTful接口响应数据结构是基本的开发规范。能够减少团队内部不必要的沟通;减轻接口消费者校验数据的负担;降低其他同事接手代码的难度;提高接口的健壮性和可扩展性。

常见的统一响应数据结构如下所示:

public class GlobalResponseEntity<T>{
    private Boolean success = true;
    private String code = "000000";
    private String message = "request successfully";
    private T data;
 }

统一的异常处理,是系统完备性的基本象征。通过对全局异常信息的捕获,能够避免将异常信息和系统敏感信息直接抛给客户端;针对特定类型异常捕获之后可以重新对输出数据做编排,提高交互友好度,同时可以记录异常信息以便监控和分析。

一般,在统一异常处理处会手动修改返回给客户端的http状态码,并编排响应给客户端的数据结构为GlobalResponseEntity,保证始终统一响应。

二、实现思路

使用RestControllerAdvice注解(或者ControllerAdvice注解)结合ResponseBodyAdvice接口,可以拦截响应结果并做统一RESTful接口响应和统一异常处理。

RestControllerAdvice注解导入了ControllerAdvice注解

@ControllerAdvice是在类上声明的注解,其用法主要有三点:

  • 和@ExceptionHandler注解配合使用,@ExceptionHandler标注的方法可以捕获Controller中抛出的的异常,从而达到异常统一处理的目的
  • 和@InitBinder注解配合使用,@InitBinder标注的方法可在请求中注册自定义参数的解析器,从而达到自定义请求参数格式化的目的
  • 和@ModelAttribute注解配合使用,@ModelAttribute标注的方法会在执行目标Controller方法之前执行,可在入参上增加自定义信息

实现ResponseBodyAdvice接口来自定义响应给前端的内容

用法举例:


// 这里@RestControllerAdvice等同于@ControllerAdvice + @ResponseBody

@RestControllerAdvice
public class GlobalHandler {
    private final Logger logger = LoggerFactory.getLogger(GlobalHandler.class);
    
    // 这里@ModelAttribute("loginUserInfo")标注的modelAttribute()方法表示会在Controller方法之前
    // 执行,返回当前登录用户的UserDetails对象
    @ModelAttribute("loginUserInfo")
    public UserDetails modelAttribute() {
        return (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    }
    
    // @InitBinder标注的initBinder()方法表示注册一个Date类型的类型转换器,用于将类似这样的2019-06-10
    // 日期格式的字符串转换成Date对象
    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        dateFormat.setLenient(false);
        binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
    } 
    
    // 这里表示Controller抛出的MethodArgumentNotValidException异常由这个方法处理
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result exceptionHandler(MethodArgumentNotValidException e) {
        Result result = new Result(BizExceptionEnum.INVALID_REQ_PARAM.getErrorCode(),
                BizExceptionEnum.INVALID_REQ_PARAM.getErrorMsg());
        logger.error("req params error", e);
        return result;
    }
    
    // 这里表示Controller抛出的BizException异常由这个方法处理
    @ExceptionHandler(BizException.class)
    public Result exceptionHandler(BizException e) {
        BizExceptionEnum exceptionEnum = e.getBizExceptionEnum();
        Result result = new Result(exceptionEnum.getErrorCode(), exceptionEnum.getErrorMsg());
        logger.error("business error", e);
        return result;
    }
    
    // 这里就是通用的异常处理器了,所有预料之外的Exception异常都由这里处理
    @ExceptionHandler(Exception.class)
    public Result exceptionHandler(Exception e) {
        Result result = new Result(1000, "网络繁忙,请稍后再试");
        logger.error("application error", e);
        return result;
    }

}

在Controller里取出@ModelAttribute标注的方法返回的UserDetails对象。(这里只是用UserDetails举例,实际开发过程中,建议按照spring security的做法,将用户信息存放到spring上下文中,然后在controller层进行消费)

当入参为examOpDate=2019-06-10 时,Spring会使用我们上面@InitBinder注册的时间类型转换器将2019-06-10转换examOpDate对象


@RestController
@RequestMapping("/example")
@Validated
public class ExampleController {

    @Autowired
    private ExampleService exampleService;
    
    @PostMapping("/list")
    public List<ExampleListDto> findList( @NotNull Date startDate,           @ModelAttribute("loginUserInfo") UserDetails userDetails) {
        List<ExampleListDto> result = exampleService.findList(startDate, userDetails);
        return result;
    }

}

@ExceptionHandler标注的多个方法分别表示只处理特定的异常。这里需要注意的是当Controller抛出的某个异常多个@ExceptionHandler标注的方法都适用时,Spring会选择最具体的异常处理方法来处理,也就是说@ExceptionHandler(Exception.class)这个标注的方法优先级最低,只有当其它方法都不适用时,才会来到这里处理。

这里仅列举了RestControllerAdvice简单用法,为了学习RestControllerAdvice注解使用

三、统一的响应处理

工程目录结构如下:
project-struct

GlobalResponse是一个处理器类(handle),用来处理统一响应。

  • GlobalResponse类需要实现ResponseBodyAdvice接口
  • 重写supports方法,可对响应进行过滤。实际开发中不一定所有的方法返回值都是相同的模板,这里可以根据MethodParameter进行过滤,此方法返回true则会走过滤,即会调用beforeBodyWrite方法,否则不会调用。
  • 重写beforeBodyWrite方法,编写具体的响应数据逻辑

代码如下:

package com.naylor.globalresponsebody.handler.response;

import com.alibaba.fastjson.JSON;
import com.naylor.globalresponsebody.handler.GlobalResponseEntity;
import org.springframework.core.MethodParameter;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

/**
 * @BelongsProject: debris-app
 * @BelongsPackage: com.naylor.globalresponsebody.response
 * @Author: Chenml
 * @CreateTime: 2020-09-02 15:26
 * @Description: 全局响应
 */

@RestControllerAdvice("com.naylor")
public class GlobalResponse implements ResponseBodyAdvice<Object> {

    /**
     * 拦截之前业务处理,请求先到supports再到beforeBodyWrite
     * 用法1:自定义是否拦截。若方法名称(或者其他维度的信息)在指定的常量范围之内,则不拦截。
     *
     * @param methodParameter
     * @param aClass
     * @return 返回true会执行拦截;返回false不执行拦截
     */
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        //TODO 过滤
        return true;
    }

    /**
     * 向客户端返回响应信息之前的业务逻辑处理
     * 用法1:对响应结果编排统一的数据结构
     * 用法2:在写入客户端响应之前统一加密
     *
     * @param responseObject     响应内容
     * @param methodParameter
     * @param mediaType
     * @param aClass
     * @param serverHttpRequest
     * @param serverHttpResponse
     * @return
     */
    @Override
    public Object beforeBodyWrite(Object responseObject, MethodParameter methodParameter,
                                  MediaType mediaType,
                                  Class<? extends HttpMessageConverter<?>> aClass,
                                  ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        //responseObject是否为null
        if (null == responseObject) {
            return new GlobalResponseEntity<>("55555", "response is empty.");
        }
        
        //responseObject是否是文件
        if (responseObject instanceof Resource) {
            return responseObject;
        }
        
        //该方法返回值类型是否是void
        //if ("void".equals(methodParameter.getParameterType().getName())) {
        //  return new GlobalResponseEntity<>("55555", "response is empty.");
        //}
        if (methodParameter.getMethod().getReturnType().isAssignableFrom(Void.TYPE)) {
            return new GlobalResponseEntity<>("55555", "response is empty.");
        }
        
        //该方法返回值类型是否是GlobalResponseEntity。若是直接返回,无需再包装一层
        if (responseObject instanceof GlobalResponseEntity) {
            return responseObject;
        }
        
        //处理string类型的返回值
        //当返回类型是String时,用的是StringHttpMessageConverter转换器,无法转换为Json格式
        //必须在方法体上标注 @PostMapping(value = "/test2", produces = "application/json; charset=UTF-8")
        if (responseObject instanceof String) {
            String responseString = JSON.toJSONString(new GlobalResponseEntity<>(responseObject));
            return responseString;
        }
        
        //该方法返回的媒体类型是否是application/json。若不是,直接返回响应内容
        if (!mediaType.includes(MediaType.APPLICATION_JSON)) {
            return responseObject;
        }

        return new GlobalResponseEntity<>(responseObject);
    }
}

GlobalResponseEntity是一个实体类,用来封装统一响应和统一异常处理的返回值模板
 

  • GlobalResponseEntity类为一个泛型类,T为接口具体的返回数据
  • success表示接口响应是否成功,一般的,这个是业务叫法,和http状态码无关
  • code表示接口响应状态码,可以根据特定业务场景自己定义,如前两位标识业务功能板块,中间两位标识模块,后两位标识操作结果,也和http状态码无关
  • message是描述信息
  • 实际开发中code和mesage的具体值可以用枚举来维护

具体代码如下:


@Data
@Accessors(chain = true)
public class GlobalResponseEntity<T> implements Serializable {

    private Boolean success = true;
    private String code = "000000";
    private String message = "request successfully";
    private T data;

    public GlobalResponseEntity() {
        super();
    }

    public GlobalResponseEntity(T data) {
        this.data = data;
    }

    public GlobalResponseEntity(String code, String message) {
        this.code = code;
        this.message = message;
        this.data = null;
    }

    public GlobalResponseEntity(String code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    public GlobalResponseEntity(Boolean success, String code, String message) {
        this.success = success;
        this.code = code;
        this.message = message;
    }

    public GlobalResponseEntity(Boolean success, String code, String message, T data) {
        this.success = success;
        this.code = code;
        this.message = message;
        this.data = data;
    }

    public static GlobalResponseEntity<?> badRequest(String code, String message) {
        return new GlobalResponseEntity<>(false, code, message);
    }

    public static GlobalResponseEntity<?> badRequest() {
        return new GlobalResponseEntity<>(false, "404000", "无法找到您请求的资源");
    }

}

四、统一的异常处理

新增GlobalException类,编写统一异常处理。类上面添加@RestControllerAdvice("com.naylor")和
@ResponseBody注解,ResponseBody用来对响应内容进行编排,如前面提到的统一响应结构体和http状态码。

ResponseEntity是org.springframework.http包下的一个类,在controller返回数据的时候包装返回结果,此类继承HttpEntity。通过实例化此类的有参构造函数可以达到统一响应结构体和自定义http状态码的目的。源码如下:

public class ResponseEntity<T> extends HttpEntity<T> {

//......

    public ResponseEntity(@Nullable T body, HttpStatus status) {        this(body, null, status);}

//......

}

示例代码如下:


@RestControllerAdvice("com.naylor")
@ResponseBody
@Slf4j
public class GlobalException {

    /**
     * 处理404异常
     * @return
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    public ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException e) {
        return new ResponseEntity<>(
                new GlobalResponseEntity(false, "404000",
                        e.getMessage() == null ? "请求的资源不存在" : e.getMessage()),
                HttpStatus.NOT_FOUND);
    }

    /**
     * 捕获业务异常,捕获自定义异常
     * @param e
     * @return
     */
    @ExceptionHandler(BizServiceException.class)
    public ResponseEntity<Object> handleBizServiceException(BizServiceException e) {
        return new ResponseEntity<>(
                new GlobalResponseEntity(false, e.getErrorCode(), e.getMessage()),
                HttpStatus.INTERNAL_SERVER_ERROR);
    }


    /**
     * 捕获参数校验异常,javax.validation.constraints
     * @param e
     * @return
     */
    @ExceptionHandler({MethodArgumentNotValidException.class})
    public ResponseEntity<Object> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        String msg = "参数校验失败";
        List<FieldFailedValidate> fieldFailedValidates = this.extractFailedMessage(e.getBindingResult().getFieldErrors());
        if (null != fieldFailedValidates && fieldFailedValidates.size() > 0) {
            msg = fieldFailedValidates.get(0).getMessage();
        }
        return new ResponseEntity<>(
                new GlobalResponseEntity<>(false, "010200", msg, null),
                HttpStatus.BAD_REQUEST);
    }

    /**
     * 组装validate错误信息
     * @param fieldErrors
     * @return
     */
    private List<FieldFailedValidate> extractFailedMessage(List<FieldError> fieldErrors) {
        List<FieldFailedValidate> fieldFailedValidates = new ArrayList<>();
        if (null != fieldErrors && fieldErrors.size() > 0) {
            FieldFailedValidate fieldFailedValidate = null;
            for (FieldError fieldError : fieldErrors) {
                fieldFailedValidate = new FieldFailedValidate();
                fieldFailedValidate.setMessage(fieldError.getDefaultMessage());
                fieldFailedValidate.setName(fieldError.getField());
                fieldFailedValidates.add(fieldFailedValidate);
            }
        }
        return fieldFailedValidates;
    }
    
     /**
     * 捕获运行时异常
     * @param e
     * @return
     */
    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<Object> handleRuntimeException(RuntimeException e) {
        log.error("handleRuntimeException:", e);
        return new ResponseEntity<>(
                new GlobalResponseEntity(false, "rt555",
                        e.getMessage() == null ? "运行时异常" : e.getMessage().replace("java.lang.RuntimeException: ", "")),
                HttpStatus.INTERNAL_SERVER_ERROR);
    }
    
        /**
     * 捕获一般异常
     * @param e
     * @return
     */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Object> handleException(Exception e) {
        return new ResponseEntity<>(
                new GlobalResponseEntity<>(false, "555",
                        e.getMessage() == null ? "未知异常" : e.getMessage()),
                HttpStatus.INTERNAL_SERVER_ERROR);
    }
    
}

FieldFailedValidate


import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;

@Data
@Accessors(chain = true)
public class FieldFailedValidate implements Serializable {
    private String name;
    private String message;
}

BizServiceException


@Data
public class BizServiceException extends Exception {
    private String message;
    private Integer code;
    public BizServiceException(String message) {
        super(message);
        this.message = message;
    }
    public BizServiceException(String message, Integer code) {
        super(message);
        this.message = message;
        this.code = code;
    }
    @Override
    public String getMessage() {
        return message;
    }
}

常见问题

Swagger2-UI无法访问的问题

解决因增加了ResponseBodyAdvice导致Swagger2-UI无法访问的问题

报错提示:

Unable to infer base url. This is common when using dynamic servlet registration or when the API is behind an API Gateway. The base url is the root of where all the swagger resources are served. For e.g. if the api is available at http://example.org/api/v2/api-docs then the base url is http://example.org/api/. Please enter the location manually:

原因:swagger相当于是寄宿在应用程序中的一个web服务,统一响应处理器拦截了应用所有的响应,对swagger-ui的响应产生了影响。

解决方案:修改统一响应处理器拦截的范围,配置包路径。


@RestControllerAdvice(value={"com.naylor","org.spring"})
public class GlobalResponseHandler implements ResponseBodyAdvice<Object> {


//......

}

优先级问题

若在项目中写了好几个处理器类,都添加了@RestControllerAdvice的注解,由于加载存在先后顺序,可能会导致部分拦截器没有按照既定的方式工作,甚至出现一些奇奇怪怪的问题,此时可以在标注了RestControllerAdvice的类上增加@Order注解,来指定加载顺序。

引用

@RestControllerAdvice详解: https://zhuanlan.zhihu.com/p/73087879

目录
相关文章
|
1月前
|
Java API 数据库
如何使用Spring Boot构建RESTful API,以在线图书管理系统为例
【10月更文挑战第9天】本文介绍了如何使用Spring Boot构建RESTful API,以在线图书管理系统为例,从项目搭建、实体类定义、数据访问层创建、业务逻辑处理到RESTful API的实现,详细展示了每个步骤。通过Spring Boot的简洁配置和强大功能,开发者可以高效地开发出功能完备、易于维护的Web应用。
57 3
|
1月前
|
JSON 前端开发 Java
Spring MVC——获取参数和响应
本文介绍了如何在Spring框架中通过不同的注解和方法获取URL参数、上传文件、处理cookie和session、以及响应不同类型的数据。具体内容包括使用`@PathVariable`获取URL中的参数,使用`MultipartFile`上传文件,通过`HttpServletRequest`和`@CookieValue`获取cookie,通过`HttpSession`和`@SessionAttribute`获取session,以及如何返回静态页面、HTML代码片段、JSON数据,并设置HTTP状态码和响应头。
49 1
Spring MVC——获取参数和响应
|
19天前
|
Java 开发者 Spring
精通SpringBoot:16个扩展接口精讲
【10月更文挑战第16天】 SpringBoot以其简化的配置和强大的扩展性,成为了Java开发者的首选框架之一。SpringBoot提供了一系列的扩展接口,使得开发者能够灵活地定制和扩展应用的行为。掌握这些扩展接口,能够帮助我们写出更加优雅和高效的代码。本文将详细介绍16个SpringBoot的扩展接口,并探讨它们在实际开发中的应用。
37 1
|
25天前
|
存储 安全 Java
|
25天前
|
存储 算法 安全
SpringBoot 接口加密解密实现
【10月更文挑战第18天】
|
1月前
|
JSON 前端开发 Java
Spring Boot框架中的响应与分层解耦架构
在Spring Boot框架中,响应与分层解耦架构是两个核心概念,它们共同促进了应用程序的高效性、可维护性和可扩展性。
50 3
|
1月前
|
XML Java 应用服务中间件
【Spring】运行Spring Boot项目,请求响应流程分析以及404和500报错
【Spring】运行Spring Boot项目,请求响应流程分析以及404和500报错
168 2
|
24天前
|
监控 Java 开发者
掌握SpringBoot扩展接口:提升代码优雅度的16个技巧
【10月更文挑战第20天】 SpringBoot以其简化配置和快速开发而受到开发者的青睐。除了基本的CRUD操作外,SpringBoot还提供了丰富的扩展接口,让我们能够更灵活地定制和扩展应用。以下是16个常用的SpringBoot扩展接口,掌握它们将帮助你写出更加优雅的代码。
43 0
|
2月前
|
安全 API 开发者
Web 开发新风尚!Python RESTful API 设计与实现,让你的接口更懂开发者心!
在当前的Web开发中,Python因能构建高效简洁的RESTful API而备受青睐,大大提升了开发效率和用户体验。本文将介绍RESTful API的基本原则及其在Python中的实现方法。以Flask为例,演示了如何通过不同的HTTP方法(如GET、POST、PUT、DELETE)来创建、读取、更新和删除用户信息。此示例还包括了基本的路由设置及操作,为开发者提供了清晰的API交互指南。
107 6
|
2月前
|
SQL JSON Java
springboot 如何编写增删改查后端接口,小白极速入门,附完整代码
本文为Spring Boot增删改查接口的小白入门教程,介绍了项目的构建、配置YML文件、代码编写(包括实体类、Mapper接口、Mapper.xml、Service和Controller)以及使用Postman进行接口测试的方法。同时提供了SQL代码和完整代码的下载链接。
springboot 如何编写增删改查后端接口,小白极速入门,附完整代码