1.概述
当下基于Spring Boot
框架开发的系统几乎都是前后端分离的,也都是基于RESTFUL
风格进行接口定义开发的,意味着前后端开发大部分数据的传输格式都是json,因此定义一个统一规范的数据格式返回有利于前后端的交互与UI的展示
Restful风格是什么?
RESTFUL(英文:Representational State Transfer,简称REST)可译为"表现层状态转化”,是一种网络应用程序的设计风格和开发方式,是资源定位和资源操作的一种风格。不是标准也不是协议。基于HTTP,可以使用 XML 格式定义或 JSON 格式定义。最常用的数据格式是JSON。由于JSON能直接被JavaScript读取,所以,使用JSON格式的REST风格的API具有简单、易读、易用的特点。Restful风格最大的特点为:资源、统一接口、URI和无状态。
对于我们Web开发人员而言,restful风格简单来说就是使用一个url地址表示一个唯一的资源。然后把原来的请求参数加入到请求资源地址中。把原来请求的增,删,改,查操作路径标识,改为使用HTTP协议中请求方式GET、POST、PUT、DELETE表示。
传统的方式是:http://127.0.0.1:8080/shepherd/user/add 表示新增用户的接口,需要在路径上加以增删改查标识,如果我们要修改:那么路径是:http://127.0.0.1:8080/shepherd/user/update 。
但是我们基于restful风格就比较优雅:http://127.0.0.1:8080/shepherd/user,增删改查都可以用这个路径,使用请求方法来进行区别即可,如`post`代表新增,`put`代表修改等。
项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用
Github地址:https://github.com/plasticene/plasticene-boot-starter-parent
Gitee地址:https://gitee.com/plasticene3/plasticene-boot-starter-parent
微信公众号:Shepherd进阶笔记
2.返回结果统一封装
定义一个统一的标准返回格式,有助于后端接口开发的规范性和通用性,同时也提高了前后端联调的效率,前端通过接收同一返回结构体进行相应映射处理,不用担心每个接口返回的格式都不一样而做一一适配了。
2.1 定义返回统一结构体
@Data
public class ResponseVO<T> implements Serializable {
private Integer code;
private String msg;
private T data;
public ResponseVO() {
}
public ResponseVO(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public ResponseVO(Integer code, T data) {
this.code = code;
this.data = data;
}
public ResponseVO(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
private ResponseVO(ResponseStatusEnum resultStatus, T data) {
this.code = resultStatus.getCode();
this.msg = resultStatus.getMsg();
this.data = data;
}
/**
* 业务成功返回业务代码和描述信息
*/
public static ResponseVO<Void> success() {
return new ResponseVO<Void>(ResponseStatusEnum.SUCCESS, null);
}
/**
* 业务成功返回业务代码,描述和返回的参数
*/
public static <T> ResponseVO<T> success(T data) {
return new ResponseVO<T>(ResponseStatusEnum.SUCCESS, data);
}
/**
* 业务成功返回业务代码,描述和返回的参数
*/
public static <T> ResponseVO<T> success(ResponseStatusEnum resultStatus, T data) {
if (resultStatus == null) {
return success(data);
}
return new ResponseVO<T>(resultStatus, data);
}
/**
* 业务异常返回业务代码和描述信息
*/
public static <T> ResponseVO<T> failure() {
return new ResponseVO<T>(ResponseStatusEnum.SYSTEM_ERROR, null);
}
/**
* 业务异常返回业务代码,描述和返回的参数
*/
public static <T> ResponseVO<T> failure(ResponseStatusEnum resultStatus) {
return failure(resultStatus, null);
}
/**
* 业务异常返回业务代码,描述和返回的参数
*/
public static <T> ResponseVO<T> failure(ResponseStatusEnum resultStatus, T data) {
if (resultStatus == null) {
return new ResponseVO<T>(ResponseStatusEnum.SYSTEM_ERROR, null);
}
return new ResponseVO<T>(resultStatus, data);
}
public static <T> ResponseVO<T> failure(Integer code, String msg) {
return new ResponseVO<T>(code, msg);
}
}
这里包含了三个字段信息:
- code 状态值:由后端统一定义各种返回结果的状态码, 比如说code=200代表接口调用成功
- msg 描述:本次接口调用的结果描述,比如说后端抛出的业务异常信息就在这里体现
- data 数据:本次返回的数据,泛型类型意味着可以支持任意类型的返回数据
成功返回如下:
{
"code": 200,
"msg": "OK",
"data": {
"id": 123,
"name": "shepherd"
}
}
业务异常返回如下:
{
"code": 400,
"msg": "当前用户不存在"
}
按照上面成功返回的示例我们接口定义如下:
@GetMapping("/test/user")
public ResponseVO<User> testUser() {
User user = new User();
user.setId(123l);
user.setName("shepherd");
return ResponseVO.success(user);
}
可以看到接口方法返回类型为ResponseVO<User>
,然后通过ResponseVO.success()
对返回结果进行包装后返回给前端。这就意味着写一个接口都需要调用ResultData.success()
这行代码对结果进行包装,有点重复劳动不够优雅的感觉。还有一种情况,有些项目服务前期为了赶时间开发时没有返回统一结构,等项目上线了有时间之后按照规范需要对后端接口返回结构进行统一,这时候如果复杂的系统已经有成百上千的接口了,如果一个个地像上面说的那样把接口返回类型改为ResponseVO<T>
,再用ResponseVO.success()
进行结果包装,工作量不小,也比较繁琐。
2.2 高级优雅实现统一结果封装
为了解决上面阐述的问题,我们借助于Spring Boot
提供的ResponseBodyAdvice
进行了高级实现。
ResponseBodyAdvice的作用:拦截Controller方法的返回值,统一处理返回值/响应体,一般用来统一返回格式,加解密,签名等等。我们在分享 Spring Boot如何对接口参数进行加解密就有提到过这个类进行返回结果参数的加密。
先来看下ResponseBodyAdvice
的源码
public interface ResponseBodyAdvice<T> {
/**
* 是否支持advice功能
* true 支持,false 不支持
*/
boolean supports(MethodParameter var1, Class<? extends HttpMessageConverter<?>> var2);
/**
* 对返回的数据进行处理
*/
@Nullable
T beforeBodyWrite(@Nullable T var1, MethodParameter var2, MediaType var3, Class<? extends HttpMessageConverter<?>> var4, ServerHttpRequest var5, ServerHttpResponse var6);
}
所以我们编写一个具体实现类即可:
@RestControllerAdvice
@Slf4j
public class ResponseResultBodyAdvice implements ResponseBodyAdvice<Object> {
@Resource
private ObjectMapper objectMapper;
private static final Class<? extends Annotation> ANNOTATION_TYPE = ResponseResultBody.class;
/**
* 判断类或者方法是否使用了 @ResponseResultBody
*/
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ANNOTATION_TYPE) || returnType.hasMethodAnnotation(ANNOTATION_TYPE);
}
/**
* 当类或者方法使用了 @ResponseResultBody 就会调用这个方法
*/
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//如果返回类型是string,那么springmvc是直接返回的,此时需要手动转化为json
// 当body都为null时,下面的if判断条件都不满足,如果接口返回类似为String,会报错com.shepherd.fast.global.ResponseVO cannot be cast to java.lang.String
Class<?> returnClass = returnType.getMethod().getReturnType();
if (body instanceof String || Objects.equals(returnClass, String.class)) {
String value = objectMapper.writeValueAsString(ResponseVO.success(body));
return value;
}
// 防止重复包裹的问题出现
if (body instanceof ResponseVO) {
return body;
}
return ResponseVO.success(body);
}
}
这里使用到一个自定义注解@ResponseResultBody
:
@Retention(RetentionPolicy.RUNTIME)
@Target({
ElementType.TYPE, ElementType.METHOD})
@Documented
@ResponseBody
public @interface ResponseResultBody {
}
注入bean
@Bean
public ResponseResultBodyAdvice responseResultBodyAdvice() {
return new ResponseResultBodyAdvice();
}
从上面我们自己定义实现类ResponseResultBodyAdvice
的#supports()
可以看到,只要我们的Controller类或者方法上使用了ResponseResultBody
注解,就会执行方法#beforeBodyWrite()
,使用ResponseVO
对结果进行包装统一返回。
实现类上使用了RestControllerAdvice
注解,@RestControllerAdvice
是一个组合注解,由@ControllerAdvice、@ResponseBody
组成,而@ControllerAdvice
继承了@Component
,因此@RestControllerAdvice
本质上是个Component
,用于定义@ExceptionHandler,@InitBinder和@ModelAttribute
方法,适用于所有使用@RequestMapping方法,该注解特点如下:
1.通过@ControllerAdvice注解可以将对于控制器的全局配置放在同一个位置。
2.注解了@RestControllerAdvice的类的方法可以使用@ExceptionHandler、@InitBinder、@ModelAttribute注解到方法上。
3.@RestControllerAdvice注解将作用在所有注解了@RequestMapping的控制器的方法上。
4.@ExceptionHandler:用于指定异常处理方法。当与@RestControllerAdvice配合使用时,用于全局处理控制器里的异常。
5.@InitBinder:用来设置WebDataBinder,用于自动绑定前台请求参数到Model中。
6.@ModelAttribute:本来作用是绑定键值对到Model中,当与@ControllerAdvice配合使用时,可以让全局的@RequestMapping都能获得在此处设置的键值对
实现类ResponseResultBodyAdvice
使用该注解就是满足上面的第3种情况,将拦截作用在所有注解了@RequestMapping的控制器的方法进行判断是否使用了注解@ResponseResultBody
,从而对接口结构进行统一ResponseVO
包装。
3.全局异常统一处理
使用统一返回结果时,还有一种情况,就是程序由于运行时异常导致报错的结果,有些异常我们可能无法提前预知,不能正常走到我们return的ResponseVO
对象返回,因此,我们需要定义一个统一的全局异常来捕获这些信息,并作为一种结果返回给控制层。
使用上面的@ControllerAdvice
和@ExceptionHandler
进行全局异常统一处理:
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 全局异常处理
* @param e
* @return
*/
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(Exception.class)
public ResponseVO exceptionHandler(Exception e){
// 处理业务异常
if (e instanceof BizException) {
BizException bizException = (BizException) e;
if (bizException.getCode() == null) {
bizException.setCode(ResponseStatusEnum.BAD_REQUEST.getCode());
}
return ResponseVO.failure(bizException.getCode(), bizException.getMessage());
} else if (e instanceof MethodArgumentNotValidException) {
// 参数检验异常
MethodArgumentNotValidException methodArgumentNotValidException = (MethodArgumentNotValidException) e;
Map<String, String> map = new HashMap<>();
BindingResult result = methodArgumentNotValidException.getBindingResult();
result.getFieldErrors().forEach((item)->{
String message = item.getDefaultMessage();
String field = item.getField();
map.put(field, message);
});
log.error("数据校验出现错误:", e);
return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST, map);
} else if (e instanceof HttpRequestMethodNotSupportedException) {
log.error("请求方法错误:", e);
return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST.getCode(), "请求方法不正确");
} else if (e instanceof MissingServletRequestParameterException) {
log.error("请求参数缺失:", e);
MissingServletRequestParameterException ex = (MissingServletRequestParameterException) e;
return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST.getCode(), "请求参数缺少: " + ex.getParameterName());
} else if (e instanceof MethodArgumentTypeMismatchException) {
log.error("请求参数类型错误:", e);
MethodArgumentTypeMismatchException ex = (MethodArgumentTypeMismatchException) e;
return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST.getCode(), "请求参数类型不正确:" + ex.getName());
} else if (e instanceof NoHandlerFoundException) {
NoHandlerFoundException ex = (NoHandlerFoundException) e;
log.error("请求地址不存在:", e);
return ResponseVO.failure(ResponseStatusEnum.NOT_EXIST, ex.getRequestURL());
} else {
//如果是系统的异常,比如空指针这些异常
log.error("【系统异常】", e);
return ResponseVO.failure(ResponseStatusEnum.SYSTEM_ERROR.getCode(), ResponseStatusEnum.SYSTEM_ERROR.getMsg());
}
}
}
注入bean
@Bean
public GlobalExceptionHandler globalExceptionHandler() {
return new GlobalExceptionHandler();
}
通过以上步骤就可以对异常进行全局异常统一处理,这样做的好处不仅是可以对未知异常进行处理之后按照统一结构返回给前端,同时还能对异常处理之后进行error
级别的日志输出,这样才能结合logback,log4j2等日志框架写入到日志文件中,以便后续查看异常错误日志排查追踪问题,否则异常信息不会被记录在error日志文件中。
4.总结
基于以上全部内容,我们讲述了如何优雅实现返回结果统一封装和全局异常统一处理,这样可以规范后端接口输出,同时也增强了项目服务的健壮性,可以说这两个统一处理事当下项目服务的必须要求,所以我们得了解一下哦。