目录
1.统一数据返回格式
在图书管理系统中的强制登录的过程中, 我们共做了两步工作
1.通过 Session 来判断用户是否登录
2.对后端返回数据进行封装, 告知前端处理的结果
回顾
后端统一返回的结果格式如下:
@Data
public class Result<T> {
private ResultStatus code; //业务码 200 - 成功 -2 失败 -1 未登录
private String errMsg; //错误信息 业务成功, errMsg为空
private T data;
}
后端逻辑处理:
@RequestMapping("/getBookListByPage")
public Result<PageResult<BookInfo>> getBookListByPage(PageRequest pageRequest, HttpSession session) {
log.info("查询图书列表, 请求参数pageRequest:{}", pageRequest);
//用户登录, 返回图书列表
PageResult<BookInfo> bookList = bookService.getBookListByPage(pageRequest);
return Result.success(bookList);
}
Result.success(pageResult) 就是对返回数据进行了封装.
拦截器帮我们实现了第一个功能, 接下来看 SpringBoot 对第二个功能如何支持.
1.1快速入门
统一数据返回格式使用 @ControllerAdvice 和 ResponseBodyAdvice 的方式来实现.
@ControllerAdvice 表示控制器通知类
添加 ResponseAdvice, 实现 ResonseBodyAdvice 接口, 并在类上添加 @ControllerAdvice注解
import com.example.com.model.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
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;
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
return Result.success(body);
}
}
supports方法: 判断是否要执行 beforeBodyWrite 方法. true为执行, false不执行. 通过该方法可以
选择哪些类或哪些方法的 response 要进行处理,其他的不进行处理.
从 returnType 获取类名和方法名:
//获取执⾏的类
Class<?> declaringClass = returnType.getMethod().getDeclaringClass();
//获取执⾏的⽅法
Method method = returnType.getMethod();
beforeBodyWrite 方法: 对 response 方法进行具体操作处理
测试:
添加统一数据返回格式之前:
添加统一数据返回格式之后:
1.2存在问题
问题现象:
我们继续测试修改图书的接口:
结果显示, 发生内部错误
查看数据库, 发现数据操作成功
日志报错信息如下:
在多次测试不同的返回结果之后, 发现只有返回结果为 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 "string";
}
@RequestMapping("/t2")
public Integer t2() {
return 1;
}
@RequestMapping("/t3")
public Boolean t3() {
return true;
}
}
解决方案:
import com.example.com.model.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
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;
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//如果返回结果为String类型, 使用SpringBoot内置提供的Jackson来实现信息的序列化
if(body instanceof String) {
return objectMapper.writeValueAsString(Result.success(body));
}
return Result.success(body);
}
}
重新测试, 结果返回正常:
1.3 案例代码修改
如果一些方法返回的结果已经时 Result 类型了, 那就直接返回 Result 类型的结果即可
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//返回结果更加灵活
if(body instanceof Result) {
return body;
}
//如果返回结果为String类型, 使⽤SpringBoot内置提供的Jackson来实现信息的序列化
if(body instanceof String) {
return objectMapper.writeValueAsString(Result.success(body));
}
return Result.success(body);
}
}
1.4 优点
1.方便前端程序员更好的接收和解析后端数据接口返回的数据
2.降低前端程序员和后端程序员的沟通成本,按照某个格式实现就可以了, 因为所有接口都是这样返回的.
3.有利于项目统一数据的维护和修改.
4.有利于后端技术部门门]的统一规范的标准制定,不会出现稀奇古怪的返回内容.
2.统一异常处理
统一异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的, @ControllerAdvice表示控制器通知类, @ExceptionHandler 是异常处理器, 两个结合表示具体代码如下:
import com.example.com.model.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ResponseBody
@ControllerAdvice
@Slf4j
public class ExceptionAdvice {
@ExceptionHandler
public Result handlerException(Exception e) {
log.error("发生异常,e:", e);
return Result.fail("内部错误");
}
}
类名, 方法名和返回值可以自定义, 重要的是注解
接口返回为数据是, 需要加上 @ResponseBody 注解
以上代码表示, 如果代码出现 Exception 异常(包括Exception的子类), 就返回一个 Result 的对象, Result 对象的设置参考 Result.fail(e.getMessage())
public static Result fail(String msg) {
Result result = new Result<>();
result.setCode(ResultStatus.FAIL);
result.setErrMsg(msg);
return result;
}
我们可以针对不同的异常, 返回不同的结果:
import com.example.com.model.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ResponseBody
@ControllerAdvice
@Slf4j
public class ExceptionAdvice {
@ExceptionHandler
public Result handlerException(Exception e) {
log.error("发生异常,e:", e);
return Result.fail(e.getMessage());
}
@ExceptionHandler
public Result handlerException(NullPointerException e) {
log.error("发生异常,e:", e);
return Result.fail("发生空指针异常: " + e.getMessage());
}
@ExceptionHandler
public Result handlerException(ArithmeticException e) {
log.error("发生异常,e:", e);
return Result.fail("发生算数异常: " + 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() {
int a = 10/0; //抛出ArithmeticException
return "string";
}
@RequestMapping("/t2")
public Integer t2() {
String a =null;
System.out.println(a.length()); //抛出NullPointerException
return 1;
}
}
当有多个异常通知时, 匹配顺序为当前类及其子类向上一次匹配
/test/t1 抛出ArithmeticException, 运行结果如下:
/test/t2 抛出NullPointerException, 运行结果如下:
3.源码分析
3.1 @ControllerAdvice 源码分析
统一数据返回和统一异常都是基于 @ControllerAdvice 注解来实现的, 通过分析 @ControllerAdvice的源码, 可以知道它们的执行流程:
点击 @ControllerAdvice 实现源码如下:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
@AliasFor(
annotation = Component.class,
attribute = "value"
)
String name() default "";
@AliasFor("basePackages")
String[] value() default {};
@AliasFor("value")
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] assignableTypes() default {};
Class<? extends Annotation>[] annotations() default {};
}
从上述源码可以看出, @ControllerAdvice 派生与@Component 组件, 这也就是为什么没有五大注解, ControllerAdvice 就生效的原因.
下面我们看看Spring是怎么实现的, 还是从 DispatcherServlet 的代码开始分析.
DispatcherServlet 对象在创建时会初始化一系列的对象:
public class DispatcherServlet extends FrameworkServlet {
//...
@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
/**
* Initialize the strategy objects that this servlet uses.
* <p>May be overridden in subclasses in order to initialize further
* strategy objects.
*/
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
//...
}
对于 @ControllerAdvice 注解, 我们重点关注 initHandlerAdapters(context) 和 initHandlerExceptionResolvers(context) 这两个方法.
1.initHandlerAdapters(context)
initHandlerAdapters(context) 方法会取得所有实现了 HandlerAdapter 接口的 bean并保存起来, 其中有一个类型为 RequestMappingHandlerAdapter 的 bean , 这个 bean 就是 @RequestMapping 注解能起作用的关键, 这个在应用启动过程中会获取所有被 @ControllerAdvice 注解标注的 bean 对象, 并做进一步处理, 关键代码如下:
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
//...
/**
* 添加ControllerAdvice bean的处理
*/
private void initControllerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
//获取所有所有被 @ControllerAdvice 注解标注的bean对象
List<ControllerAdviceBean> adviceBeans =
ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
List<Object> requestResponseBodyAdviceBeans = new ArrayList<>();
for (ControllerAdviceBean adviceBean : adviceBeans) {
Class<?> beanType = adviceBean.getBeanType();
if (beanType == null) {
throw new IllegalStateException("Unresolvable type for
ControllerAdviceBean:" + adviceBean);
}
Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType,
MODEL_ATTRIBUTE_METHODS);
if (!attrMethods.isEmpty()) {
this.modelAttributeAdviceCache.put(adviceBean, attrMethods);
}
Set<Method> binderMethods =
MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);
if (!binderMethods.isEmpty()) {
this.initBinderAdviceCache.put(adviceBean, binderMethods);
}
if (RequestBodyAdvice.class.isAssignableFrom(beanType) ||
ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
requestResponseBodyAdviceBeans.add(adviceBean);
}
}
if (!requestResponseBodyAdviceBeans.isEmpty()) {
this.requestResponseBodyAdvice.addAll(0,
requestResponseBodyAdviceBeans);
}
if (logger.isDebugEnabled()) {
int modelSize = this.modelAttributeAdviceCache.size();
int binderSize = this.initBinderAdviceCache.size();
int reqCount = getBodyAdviceCount(RequestBodyAdvice.class);
int resCount = getBodyAdviceCount(ResponseBodyAdvice.class);
if (modelSize == 0 && binderSize == 0 && reqCount == 0 && resCount
== 0) {
logger.debug("ControllerAdvice beans: none");
} else {
logger.debug("ControllerAdvice beans: " + modelSize + "
@ModelAttribute," + binderSize +
" @InitBinder, " + reqCount + " RequestBodyAdvice, " +
resCount + " ResponseBodyAdvice");
}
}
}
//...
}
这个方法在执行时会查找使用所有的 @ControllerAdvice 类, 把ResponseBodyAdvice 类放在容器中, 当发生某个事件时, 调用相应的 Advice 方法, 比如返回数据前调用统一数据封装
至于 DispatcherServlet 和 RequestMappingHandlerAdapter 时如何交互的这就是另一个复杂的话题了, 此处就不再赘述, 以了解为主.
2.initHandlerExceptionResolvers(context)
接下来看 DispatcherServlet 的 initHandlerExceptionResolvers(context) 方法, 这个方法会取得所有实现了 HandlerExceptionResolver 接口的 bean 并保存起来, 其中就有一个类型为 ExceptionHandlerExceptionResolver 的 bean, 这个 bean 在应用启动过程中会获取所有被 @ControllerAdvice 注解标注的 bean 对象做进一步处理, 代码如下:
public class ExceptionHandlerExceptionResolver
extends AbstractHandlerMethodExceptionResolver
implements ApplicationContextAware, InitializingBean {
//...
private void initExceptionHandlerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
// 获取所有所有被 @ControllerAdvice 注解标注的bean对象
List<ControllerAdviceBean> adviceBeans =
ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
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);
}
}
if (logger.isDebugEnabled()) {
int handlerSize = this.exceptionHandlerAdviceCache.size();
int adviceSize = this.responseBodyAdvice.size();
if (handlerSize == 0 && adviceSize == 0) {
logger.debug("ControllerAdvice beans: none");
} else {
logger.debug("ControllerAdvice beans: " + handlerSize + " @ExceptionHandler, " +
adviceSize + " ResponseBodyAdvice");
}
}
}
//...
}
当 Controller 抛出异常时, DispatcherServlet 通过 ExceptionHandlerExceptionResolver 来解析异常, 而 ExceptionHandlerExceptionResolver 又通过 ExceptionHandlerMethodResolver 来解析异常, ExceptionHandlerMethodResolver 最终解析异常找到适用的 @ExceptionHandler 标注的方法是这里:
public class ExceptionHandlerMethodResolver {
//...
private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
List<Class<? extends Throwable>> matches = new ArrayList();
//根据异常类型, 查找匹配的异常处理⽅法
//⽐如NullPointerException会匹配两个异常处理⽅法:
//handler(Exception e) 和 handler(NullPointerException e)
for (Class<? extends Throwable> mappedException :
this.mappedMethods.keySet()) {
if (mappedException.isAssignableFrom(exceptionType)) {
matches.add(mappedException);
}
}
//如果查找到多个匹配, 就进⾏排序, 找到最使⽤的⽅法. 排序的规则依据抛出异常相对于声明异常的深度
//⽐如抛出的是NullPointerException(继承于RuntimeException, RuntimeException又继承于Exception)
//相对于handler(NullPointerException e) 声明的NullPointerException深度为0,
//相对于handler(Exception e) 声明的Exception 深度 为2
//所以 handler(NullPointerException e)标注的⽅法会排在前⾯
if (!matches.isEmpty()) {
if (matches.size() > 1) {
matches.sort(new ExceptionDepthComparator(exceptionType));
}
return this.mappedMethods.get(matches.get(0));
} else {
return NO_MATCHING_EXCEPTION_HANDLER_METHOD;
}
}
//...
}
3.2 1.2中返回String会报错原因分析
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(new
AllEncompassingFormHttpMessageConverter());
}
//...
}
其中 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, ServletServerHttpResponse
outputMessage)
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类型, 所以出现类型不匹配 "Result cannot be cast to java.lang.String" 异常.