统一数据返回格式 & 统一异常处理
- 1. 统一数据返回格式
- 1.1 快速入门
- 1.2 存在问题
- 1.3 案列代码修改
- 1.4 优点
- 2. 统一异常处理
1. 统一数据返回格式
强制登录案例中,我们共做了两部分⼯作
- 通过Session来判断⽤⼾是否登录
- 对后端返回数据进⾏封装,告知前端处理的结果
回顾
后端统⼀返回结果
@Data public class Result<T> {private int status;private String errorMessage;private T data; }
后端逻辑处理
@RequestMapping("/getListByPage") public Result getListByPage(PageRequest pageRequest) {log.info("获取图书列表, pageRequest:{}", pageRequest);//⽤⼾登录, 返回图书列表 PageResult<BookInfo> pageResult =bookService.getBookListByPage(pageRequest);log.info("获取图书列表222, pageRequest:{}", pageResult);return Result.success(pageResult); }
Result.success(pageResult)就是对返回数据进⾏了封装
拦截器帮我们实现了第⼀个功能,接下来看SpringBoot对第⼆个功能如何⽀持
1.1 快速入门
统⼀的数据返回格式使⽤@ControllerAdvice 和ResponseBodyAdvice 的⽅式实现 @ControllerAdvice 表⽰控制器通知类
添加类 ResponseAdvice ,实现 ResponseBodyAdvice 接⼝,并在类上添加 @ControllerAdvice 注解
import com.example.demo.model.Result;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
importorg.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType,MediaType selectedContentType, Class selectedConverterType, ServerHttpRequestrequest, ServerHttpResponse response) {return Result.success(body);}
}
- supports⽅法:判断是否要执⾏beforeBodyWrite⽅法.true为执⾏,false不执⾏.通过该⽅法可以 选择哪些类或哪些⽅法的response要进⾏处理,其他的不进⾏处理.
从returnType获取类名和⽅法名
//获取执⾏的类 Class<?> declaringClass = returnType.getMethod().getDeclaringClass();//获取执⾏的⽅法 Method method = returnType.getMethod();
- beforeBodyWrite⽅法:对response⽅法进⾏具体操作处理
测试
测试接⼝:http://127.0.0.1:8080/book/queryBookById?bookId=1
添加统⼀数据返回格式之前:
添加统一数据返回格式之后:
1.2 存在问题
我们继续测试修改图书的接口: http://127.0.0.1:8080/book/updateBook
结果显示, 发生内部错误
查看数据库, 发现数据库操作成功
查看日志, 日志报错:
多测试几种不同的返回结果, 发现只有返回结果为 String类型时才有这种错误发生.
测试代码:
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/test")
@RestController
public class TestController {@RequestMapping("/t1")public String t1(){return "t1";}@RequestMapping("/t2")public boolean t2(){return true;}@RequestMapping("/t3")public Integer t3(){return 200;}
}
解决方案:
import com.example.demo.model.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import
org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@Slf4j
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {private static ObjectMapper mapper = new ObjectMapper();@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}@SneakyThrows@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType,MediaType selectedContentType, Class selectedConverterType, ServerHttpRequestrequest, ServerHttpResponse response) {//如果返回结果为String类型, 使⽤SpringBoot内置提供的Jackson来实现信息的序列化if (body instanceof String){return mapper.writeValueAsString(Result.success(body));}return Result.success(body);}
}
重新测试, 结果返回正常:
原因分析:
SpringMVC默认会注册⼀些⾃带的 HttpMessageConverter
(从先后顺序排列分别为 ByteArrayHttpMessageConverter ,StringHttpMessageConverter , SourceHttpMessageConverter , SourceHttpMessageConverter ,AllEncompassingFormHttpMessageConverter )
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean {//...public RequestMappingHandlerAdapter() {this.messageConverters = new ArrayList<>(4);this.messageConverters.add(new ByteArrayHttpMessageConverter());this.messageConverters.add(new StringHttpMessageConverter());if (!shouldIgnoreXml) {try {this.messageConverters.add(new SourceHttpMessageConverter<>());}catch (Error err) {// Ignore when no TransformerFactory implementation is available}}this.messageConverters.add(newAllEncompassingFormHttpMessageConverter());}//...
}
其中AllEncompassingFormHttpMessageConverter会根据项⽬依赖情况添加对应的 HttpMessageConverter
public AllEncompassingFormHttpMessageConverter() {if (!shouldIgnoreXml) {try {addPartConverter(new SourceHttpMessageConverter<>());}catch (Error err) {// Ignore when no TransformerFactory implementation is available}if (jaxb2Present && !jackson2XmlPresent) {addPartConverter(new Jaxb2RootElementHttpMessageConverter());}}if (kotlinSerializationJsonPresent) {addPartConverter(new KotlinSerializationJsonHttpMessageConverter());}if (jackson2Present) {addPartConverter(new MappingJackson2HttpMessageConverter());}else if (gsonPresent) {addPartConverter(new GsonHttpMessageConverter());}else if (jsonbPresent) {addPartConverter(new JsonbHttpMessageConverter());}if (jackson2XmlPresent && !shouldIgnoreXml) {addPartConverter(new MappingJackson2XmlHttpMessageConverter());}if (jackson2SmilePresent) {addPartConverter(new MappingJackson2SmileHttpMessageConverter());}
}
在依赖中引⼊jackson包后,容器会把 MappingJackson2HttpMessageConverter ⾃动注册到 messageConverters 链的末尾.
Spring会根据返回的数据类型,从 messageConverters 链选择合适的 HttpMessageConverter .
当返回的数据是⾮字符串时,使⽤的 MappingJackson2HttpMessageConverter 写⼊返回对象. 当返回的数据是字符串时, StringHttpMessageConverter 会先被遍历到,这时会认为 StringHttpMessageConverter 可以使⽤.
public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver implements HandlerMethodReturnValueHandler {//...代码省略protected <T> void writeWithMessageConverters(@Nullable T value,MethodParameter returnType,ServletServerHttpRequest inputMessage, ServletServerHttpResponseoutputMessage)throws IOException, HttpMediaTypeNotAcceptableException,HttpMessageNotWritableException {//...代码省略if (selectedMediaType != null) {selectedMediaType = selectedMediaType.removeQualityValue();for (HttpMessageConverter<?> converter : this.messageConverters) {GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?(GenericHttpMessageConverter<?>) converter : null);if (genericConverter != null ?((GenericHttpMessageConverter)converter).canWrite(targetType, valueType, selectedMediaType) :converter.canWrite(valueType, selectedMediaType)) {//getAdvice().beforeBodyWrite 执⾏之后, body转换成了Result类型的结果body = getAdvice().beforeBodyWrite(body, returnType,selectedMediaType,(Class<? extends HttpMessageConverter<?>>) converter.getClass(), inputMessage, outputMessage);if (body != null) {Object theBody = body;LogFormatUtils.traceDebug(logger, traceOn ->"Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");addContentDispositionHeader(inputMessage, outputMessage);if (genericConverter != null) {genericConverter.write(body, targetType, selectedMediaType, outputMessage);}else {//此时cover为StringHttpMessageConverter ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);}}else {if (logger.isDebugEnabled()) {logger.debug("Nothing to write: null body");}}return;}}}//...代码省略 }//...代码省略
}
在 ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage) 的处理中,调⽤⽗类的write⽅法
由于 StringHttpMessageConverter 重写了addDefaultHeaders⽅法,所以会执⾏⼦类的⽅法
然⽽⼦类 StringHttpMessageConverter 的addDefaultHeaders⽅法定义接收参数为String,此 时t为Result类型,所以出现类型不匹配"Resultcannotbecasttojava.lang.String"的异常
1.3 案列代码修改
如果⼀些⽅法返回的结果已经是Result类型了,那就直接返回Result类型的结果即可
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,MediaType selectedContentType, Class selectedConverterType, ServerHttpRequestrequest, ServerHttpResponse response) {//返回结果更加灵活 if (body instanceof Result){return body;}//如果返回结果为String类型, 使⽤SpringBoot内置提供的Jackson来实现信息的序列化 if (body instanceof String){return mapper.writeValueAsString(Result.success(body));}return Result.success(body);
}
1.4 优点
- ⽅便前端程序员更好的接收和解析后端数据接⼝返回的数据
- 降低前端程序员和后端程序员的沟通成本,按照某个格式实现就可以了,因为所有接⼝都是这样返回 的.
- 有利于项⽬统⼀数据的维护和修改.
- 有利于后端技术部⻔的统⼀规范的标准制定,不会出现稀奇古怪的返回内容.
2. 统一异常处理
统⼀异常处理使⽤的是@ControllerAdvice +@ExceptionHandler 来实现的, @ControllerAdvice 表⽰控制器通知类, @ExceptionHandler 是异常处理器,两个结合表 ⽰当出现异常的时候执⾏某个通知,也就是执⾏某个⽅法事件.
具体代码如下:
import com.example.demo.model.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ControllerAdvice
@ResponseBody
public class ErrorAdvice {@ExceptionHandlerpublic Object handler(Exception e) {return Result.fail(e.getMessage());}
}
类名,⽅法名和返回值可以⾃定义,重要的是注解
接⼝返回为数据时,需要加 @ResponseBody 注解
以上代码表⽰,如果代码出现Exception异常(包括Exception的⼦类),就返回⼀个Result的对象,Result 对象的设置参考Result.fail(e.getMessage())
public static Result fail(String msg) {Result result = new Result();result.setStatus(ResultStatus.FAIL);result.setErrorMessage(msg);result.setData("");return result;
}
我们可以针对不同的异常,返回不同的结果.
import com.example.demo.model.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ResponseBody
@ControllerAdvice
public class ErrorAdvice {@ExceptionHandlerpublic Object handler(Exception e) {return Result.fail(e.getMessage());}@ExceptionHandlerpublic Object handler(NullPointerException e) {return Result.fail("发⽣NullPointerException:"+e.getMessage());}@ExceptionHandlerpublic Object handler(ArithmeticException e) {return Result.fail("发⽣ArithmeticException:"+e.getMessage());}
}
模拟制造异常:
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/test")
@RestController
public class TestController {@RequestMapping("/t1")public String t1(){return "t1";}@RequestMapping("/t2")public boolean t2(){int a = 10/0; //抛出ArithmeticExceptionreturn true;}@RequestMapping("/t3")public Integer t3(){String a =null;System.out.println(a.length()); //抛出NullPointerExceptionreturn 200;}
}
当有多个异常通知时,匹配顺序为当前类及其⼦类向上依次匹配
/test/t2抛出ArithmeticException,运⾏结果如下:
/test/t3抛出NullPointerException,运⾏结果如下: