SpringBoot项目统一处理返回值和异常
简介
当使用SpringBoot开发Web项目的API时,为了与前端更好地通信,通常会约定好接口的响应格式。例如,以下是一个JSON格式的响应,通过返回码和返回信息告知前端具体的操作结果或错误信息。如果操作成功,前端可以通过"data"字段获取响应内容。
{
"code":"000000",
"message":"操作成功",
"data": true
}
如果所有接口都这样一个一个的封,那开发人员估计就先一个一个的疯了,为了减少手动的处理过程,让开发人员可以专注于业务本身,本文将向你展示一种非常优雅的方式。
除了统一处理接口的响应内容以外,在一般的业务流程中,不管是自定义的异常还是因为各种问题而抛出的异常,在前端接收到的接口响应状态都是500错误,接口响应的内容上也非常不友好,如下图。
而实际的业务中,我们更希望可以给用户一个相对友好的提示,之前展示了一个成功的响应,这里我们也可以将异常情况处理为同样的结构,例如:
{
"code":"010101",
"message":"订单当前不支持此操作",
"data": ""
}
这样也就可以让前端通过特定的方式将异常信息以更友好的方式展示给用户,而不是干巴巴的代码,接下来我们废话不多说,开干!!!
前期准备
首先我们需要准备一个示例工程用于实操演示,这里给大家准备了一个项目并开源到了Gitee,大家可以执行以下命令获取当前需要使用的示例项目,就是最基础的SpringBoot项目,大家也可以自己初始化一个,这个项目后续我会继续往里添加功能,大家可以关注下,说不定什么时候就可以用上了,也欢迎大家通过各种方式与我沟通相关问题。
git clone https://gitee.com/itartisans/itartisans-framework.git
git checkout base-springboot-web
统一封装报文
为了统一处理业务代码返回的逻辑,我们需要准备一个实体类,核心代码就是以下属性,分别对应着开头的报文各个属性,而其他的getter和setter方法则因为篇幅原因省略了。
public class Result {
private String code;
private String message;
private Object data;
// todo 添加的省略getter和setter方法
}
报文的统一处理需要依赖Spring提供的AOP支持,Spring官方针对响应内容的处理提供了ResponseBodyAdvice接口,对响应报文的封装则需要实现此接口。
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
Result result = new Result();
result.setCode("000000");
result.setMessage("操作成功");
result.setData(body);
return result;
}
}
说明:
- 第一行的@ControllerAdvice声明此类是一个切片处理类;
- supports方法用于标识切面的覆盖范围,如果返回true则会执行beforeBodyWrite方法里的代码,如果返回false则直接响应;
- beforeBodyWrite方法用于对响应值进行自定义操作,此处通过Result进行封装;
完成以上处理后,我们可以创建个Controller请求下看看返回结果,如果示例代码是返回的不是String类型的话,那么应该是可以正常响应的,但是如果你尝试的是String,你就会收到这么一个异常:
class org.itartisans.framework.model.entity.Result cannot be cast to class java.lang.String
收到这个异常的原因是Spring默认使用的序列化器是StringHttpMessageConverter,这个序列化器无法正常的将对象转换为String类型,需要手动指定使用MappingJackson2HttpMessageConverter进行处理,而指定序列化处理器则需要实现WebMvcConfigurer并通过重写configureMessageConverters方法实现,具体代码如下:
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(0, new MappingJackson2HttpMessageConverter());
}
}
完成序列化处理器的配置后即可愉快的测试所有类型的返回值了。
统一异常处理
在完成以上的报文封装后,如果代码执行过程中出现了异常,则会出现以下内容,此处是手动throw异常模拟的情况。
{
"code": "000000",
"message": "操作成功",
"data": {
"timestamp": 1691503634161,
"status": 500,
"error": "Internal Server Error",
"path": "/demo/getObject"
}
}
报文即使在失败时也显示了操作成功,这明显不是正常的情况,也不符合我们最初可以定制错误码和错误信息的需求,统一处理异常同样需要使用AOP,创建一个针对异常的切面处理器,代码如下:
@RestControllerAdvice
public class ExceptionAdvice {
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
Result result = new Result();
result.setCode("999999");
result.setMessage("操作失败");
return result;
}
}
这部分代码和封装报文时有共同指出,这里介绍下不一样的地方:
- 第一行的@RestControllerAdvice与@RestController类似,表示此类中的方法返回值均为JSON类型
- 第四行的@ExceptionHandler指定该方法时一个异常处理器,参数对应处理的异常,这里指定的Exception.class表示处理所有异常
经过切面处理后报文如下,可以返现data中的返回内容正是我们想要的响应报文。
{
"code": "000000",
"message": "操作成功",
"data": {
"code": "999999",
"message": "操作失败",
"data": null
}
}
这里则是因为上一步统一封装报文时我们没有对方法的返回值进行区分,一刀切的对所有返回值进行了封装,结果出现了这种情况。既然知道了问题所在,那就好解决了,在封装报文时我们知道ResponseAdvice中的supports方法是用于判断是否执行beforeBodyWrite的,那我们只需要在supports中判断返回值类型是否为Result即可,如果不是才进行封装,如果是则直接返回。代码如下:
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return !returnType.getParameterType().equals(Result.class);
}
修改后重启应用再次请求接口即可发现报文格式符合我们预期了。
{
"code": "999999",
"message": "操作失败",
"data": null
}
自定义异常信息
到此为止我们完成了异常的统一处理,但是目前所有的异常都会返回999999,无法进行自定义错误内容,显然还没有填完开头挖的坑,所以我们继续对程序进行改造,在介绍@ExceptionHandler我们提到参数里可以指定处理的异常类型,那我们就从此处入手,自定义一个异常,然后增加一个对应的异常处理器,在异常处理器中根据异常中的错误信息进行转换。
因为要自定义异常,不可能将异常信息散落在代码里,所以要选择一个地方统一维护异常代码和对应的异常信息,这里作者选择的是枚举类,如果项目有其他要求也可以选择properties文件等载体,只是读取信息时方式不一样而已。枚举类代码如下,添加此枚举之后大家也可以把之前涉及SUCCESS和FAILED的地方重构一下,这里就不展开了:
public enum ResultCode {
SUCCESS("000000", "操作成功"),
FAILED("999999", "操作失败"),
DEMO_ERROR("010101", "订单当前不支持此操作")
;
private final String code;
private final String message;
ResultCode(String code, String message) {
this.code = code;
this.message = message;
}
// TODO 添加getter和setter方法
}
有了异常信息列表后我们需要创建一个自定义异常作为信息载体,以便可以在代码中使用throw触发异常处理器,代码如下:
public class FrameworkException extends Exception{
private ResultCode resultCode;
public FrameworkException(ResultCode resultCode) {
super(resultCode.getMessage() + "(" + resultCode.getCode() + ")");
this.resultCode = resultCode;
}
// TODO 添加getter和setter方法
}
之后我们要添加对应的异常处理器,在ExceptionAdvice中增加如下方法,用于处理我们刚刚添加的自定义异常。
@ExceptionHandler(FrameworkException.class)
public Result handleFrameworkException(FrameworkException e) {
ResultCode code = e.getResultCode();
Result result = new Result();
result.setCode(code.getCode());
result.setMessage(code.getMessage());
return result;
}
之后我们在测试方法中任意位置添加如下测试代码,注意添加之后之前的代码会报错,因为异常后的代码无法执行到会导致编译报错,这里注释掉即可。
throw new FrameworkException(ResultCode.DEMO_ERROR);
更新后重启项目再次请求接口即可看到已经完成了异常信息的自定义,前端也可以针对性的进行业务处理了。完结!撒花~
{
"code": "010101",
"message": "订单当前不支持此操作",
"data": null
}