SpringBoot | 拦截器 | 统一数据返回格式 | 统一异常处理 | 适配器模式
拦截器
拦截器是Spring框架提供的核心功能之一, 主要用来拦截用户的请求, 在指定方法前后, 根据业务需要执行预先设定的代码.
也就是说, 允许开发人员提前预定义一些逻辑, 在用户的请求响应前后执行. 也可以在用户请求前阻止其执行.
在拦截器当中,开发人员可以在应用程序中做一些通用性的操作, 比如通过拦截器来拦截前端发来的请求, 判断Session中是否有登录用户的信息. 如果有就可以放行, 如果没有就进行拦截.
基本使用
- 定义拦截器
- 注册配置拦截器
自定义拦截器:实现HandlerInterceptor接口,并重写其所有方法
package com.example.booksmanagementsystem.interceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("LoginInterceptor 目标方法执行前执行...");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
log.info("LoginInterceptor 目标方法执行后执行");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
log.info("LoginInterceptor 视图渲染完毕后执行,最后执行");
}
}
- preHandle()方法:目标方法执行前执行. 返回true: 继续执行后续操作; 返回false: 中断后续操作.
- postHandle()方法:目标方法执行后执行
- afterCompletion()方法:视图渲染完毕后执行,最后执行(后端开发现在几乎不涉及视图, 暂不了解)
注册配置拦截器:实现WebMvcConfigurer接口,并重写addInterceptors方法
package com.example.booksmanagementsystem.config;
import com.example.booksmanagementsystem.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册自定义拦截器对象
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**"); // 设置拦截器拦截的请求路径(/**表示拦截所有请求
}
}
可以看到preHandle 方法执行之后就放行了, 开始执行目标方法, 目标方法执行完成之后执行postHandle和afterCompletion方法.
把preHandle方法的返回值改为false,在观察运行结果
拦截器拦截了请求,没有进行响应
拦截器详解
拦截路径
拦截路径是指我们定义的这个拦截器, 对哪些请求生效.我们在注册配置拦截器的时候, 通过addPathPatterns() 方法指定要拦截哪些请求. 也可以通过excludePathPatterns() 指定不拦截哪些请求.
上面的代码中,配置的是/**,表示拦截所有请求
比如用户登录校验, 我们希望可以对除了登录之外所有的路径生效.
package com.example.booksmanagementsystem.config;
import com.example.booksmanagementsystem.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
//自定义的拦截器对象
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
.excludePathPatterns("/user/login");
}
}
拦截路径 | 含义 | 举例 |
/* | 一级路径 | 能匹配/user,/book,/login,不能匹配 /user/login |
/** | 任意级路径 | 能匹配/user,/user/login,/user/reg |
/book/* | /book下的一级路径 | 能匹配/book/addBook,不能匹配/book/addBook/1,/book |
/book/** | /book下的任意级路径 | 能匹配/book,/book/addBook,/book/addBook/2,不能匹配/user/login |
拦截器执行流程
正常的调用顺序
有了拦截器之后,会在调用 Controller 之前进行相应的业务处理,执行的流程如下图
- 添加拦截器后, 执行Controller的方法之前, 请求会先被拦截器拦截住. 执行 preHandle() 方法,这个方法需要返回一个布尔类型的值. 如果返回true, 就表示放行本次操作, 继续访问controller中的方法. 如果返回false,则不会放行(controller中的方法也不会执行).
- controller当中的方法执行完毕后,再回过来执行 postHandle() 这个方法以及afterCompletion() 方法,执行完毕之后,最终给浏览器响应数据.
登录校验
之前实现过一个图书管理系统,只不过是残次品,只有登录功能和查询功能,现在给这个系统实现以下登录校验。
定义拦截器
从session中获取用户信息,如果session中不存在,则返回false,并设置http状态码为401,否则返回true
package com.example.booksmanagementsystem.interceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("LoginInterceptor 目标方法执行前执行...");
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userName") != null) {
return false;
}
response.setStatus(401);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("LoginInterceptor 目标方法执行后执行");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("LoginInterceptor 视图渲染完毕后执行,最后执行");
}
}
注册配置拦截器
package com.example.booksmanagementsystem.config;
import com.example.booksmanagementsystem.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
//自定义的拦截器对象
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
.excludePathPatterns("/user/login")
.excludePathPatterns("/**/*.js")
.excludePathPatterns("/**/*.css")
.excludePathPatterns("/**/*.png")
.excludePathPatterns("/**/*.html");
}
}
也可以改成
package com.example.booksmanagementsystem.config;
import com.example.booksmanagementsystem.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Arrays;
import java.util.List;
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
//自定义的拦截器对象
@Autowired
private LoginInterceptor loginInterceptor;
private List<String> excludePaths = Arrays.asList(
"/user/login",
"/**/*.js",
"/**/*.css",
"/**/*.png",
"/**/*.html"
);
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
.excludePathPatterns(excludePaths)
}
}
未经过认证. 指示身份验证是必需的, 没有提供身份验证或身份验证失败. 如果请求已经包
含授权凭据,那么401状态码表示不接受这些凭据
DispatcherServlet源码分析
在项目启动之后,有一个核心的类DispatcherServlet,它来控制程序的执行顺序,所有的请求都会现进到DispatcherServlet,执行doDispatch调度方法,如果有拦截器,会先执行拦截器preHandle方法的代码,如果preHandle返回true,继续访问controller中的方法,conatroller当中的方法执行完毕之后,再回过来执行postHandle和afterCompletion,返回给DispatcherServlet,最终给浏览器响应数据
初始化
DispatcherServlet的初始化方法 init() 在其父类 HttpServletBean 中实现的.主要作用是加载 web.xml 中 DispatcherServlet 的 配置, 并调用子类的初始化.web.xml是web项目的配置文件,一般的web工程都会用到web.xml来配置,主要用来配置Listener,Filter,Servlet等, Spring框架从3.1版本开始支持Servlet3.0, 并且从3.2版本开始通过配置DispatcherServlet, 实现不再使用web.xml
/**
* Map config parameters onto bean properties of this servlet, and
* invoke subclass initialization.
* @throws ServletException if bean properties are invalid (or required
* properties are missing), or if subclass initialization fails.
*/
@Override
public final void init() throws ServletException {
// Set bean properties from init parameters.
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
}
throw ex;
}
}
// Let subclasses do whatever initialization they like.
initServletBean();
}
在 HttpServletBean 的 init() 中调用了 initServletBean() , 它是在FrameworkServlet 类中实现的, 主要作用是建立 WebApplicationContext 容器(有时也称上下文), 并加载 SpringMVC 配置文件中定义的 Bean到该容器中, 最后将该容器添加到 ServletContext 中. 下面是initServletBean() 的具体代码:
/**
* Overridden method of {@link HttpServletBean}, invoked after any bean properties
* have been set. Creates this servlet's WebApplicationContext.
*/
@Override
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
if (logger.isInfoEnabled()) {
logger.info("Initializing Servlet '" + getServletName() + "'");
}
long startTime = System.currentTimeMillis();
try {
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
catch (ServletException | RuntimeException ex) {
logger.error("Context initialization failed", ex);
throw ex;
}
if (logger.isDebugEnabled()) {
String value = this.enableLoggingRequestDetails ?
"shown which may lead to unsafe logging of potentially sensitive data" :
"masked to prevent unsafe logging of potentially sensitive data";
logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +
"': request parameters and headers will be " + value);
}
if (logger.isInfoEnabled()) {
logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
}
}
此处打印的日志, 也正是控制台打印出来的日志
进入这个initWebApplicationcontext方法中,有一个onRefresh方法,在初始化web容器中,会通过onRefresh来初始化SpringMVC容器
/**
* Initialize and publish the WebApplicationContext for this servlet.
* <p>Delegates to {@link #createWebApplicationContext} for actual creation
* of the context. Can be overridden in subclasses.
* @return the WebApplicationContext instance
* @see #FrameworkServlet(WebApplicationContext)
* @see #setContextClass
* @see #setContextConfigLocation
*/
protected WebApplicationContext initWebApplicationContext() {
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
if (this.webApplicationContext != null) {
// A context instance was injected at construction time -> use it
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) {
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
if (cwac.getParent() == null) {
// The context instance was injected without an explicit parent -> set
// the root application context (if any; may be null) as the parent
cwac.setParent(rootContext);
}
configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
// No context instance was injected at construction time -> see if one
// has been registered in the servlet context. If one exists, it is assumed
// that the parent context (if any) has already been set and that the
// user has performed any initialization such as setting the context id
wac = findWebApplicationContext();
}
if (wac == null) {
// No context instance is defined for this servlet -> create a local one
wac = createWebApplicationContext(rootContext);
}
if (!this.refreshEventReceived) {
// Either the context is not a ConfigurableApplicationContext with refresh
// support or the context injected at construction time had already been
// refreshed -> trigger initial onRefresh manually here.
synchronized (this.onRefreshMonitor) {
onRefresh(wac);
}
}
if (this.publishContext) {
// Publish the context as a servlet context attribute.
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}
return wac;
}
在initStrategies()中进行9大组件的初始化, 如果没有配置相应的组件,就使用默认定义的组件(在
DispatcherServlet.properties中有配置默认的策略)
/**
* This implementation calls {@link #initStrategies}.
*/
@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) {
1 initMultipartResolver(context);
2 initLocaleResolver(context);
3 initThemeResolver(context);
4 initHandlerMappings(context);
5 initHandlerAdapters(context);
6 initHandlerExceptionResolvers(context);
7 initRequestToViewNameTranslator(context);
8 initViewResolvers(context);
9 initFlashMapManager(context);
}
方法initMultipartResolver、initLocaleResolver、initThemeResolver、initRequestToViewNameTranslator、initFlashMapManager的处理方式几乎都一样(1.2.3.7.8,9),从应用文中取出指定的Bean, 如果没有, 就使用默认的.方法initHandlerMappings、initHandlerAdapters、initHandlerExceptionResolvers的处理方式几乎都一样(4,5,6)
- 初始化文件上传解析器MultipartResolver:从应用上下文中获取名称为multipartResolver的Bean,如果没有名为multipartResolver的Bean,则没有提供上传文件的解析器
- 初始化区域解析器LocaleResolver:从应用上下文中获取名称为localeResolver的Bean,如果没有这个Bean,则默认使用AcceptHeaderLocaleResolver作为区域解析器
- 初始化主题解析器ThemeResolver:从应用上下文中获取名称为themeResolver的Bean,如果没有这个Bean,则默认使用FixedThemeResolver作为主题解析器
- 初始化处理器映射器HandlerMappings:处理器映射器作用,1)通过处理器映射器找到对应的处理器适配器,将请求交给适配器处理;2)缓存每个请求地址URL对应的位置(Controller.xxx方法);如果在ApplicationContext发现有HandlerMappings,则从ApplicationContext中获取到所有的HandlerMappings,并进行排序;如果在ApplicationContext中没有发现有处理器映射器,则默认BeanNameUrlHandlerMapping作为处理器映射器
- 初始化处理器适配器HandlerAdapter:作用是通过调用具体的方法来处理具体的请求;如果在ApplicationContext发现有handlerAdapter,则从ApplicationContext中获取到所有的HandlerAdapter,并进行排序;如果在ApplicationContext中没有发现处理器适配器,则默SimpleControllerHandlerAdapter作为处理器适配器
- 初始化异常处理器解析器HandlerExceptionResolver:如果在ApplicationContext发现有handlerExceptionResolver,则从ApplicationContext中获取到所有的HandlerExceptionResolver,并进行排序;如果在ApplicationContext中没有发现异常处理器解析器,则不设置异常处理器
- 初始化RequestToViewNameTranslator:其作用是从Request中获取viewName,从ApplicationContext发现有viewNameTranslator的Bean,如果没有,则默认使用DefaultRequestToViewNameTranslator
- 初始化视图解析器ViewResolvers:先从ApplicationContext中获取名为viewResolver的Bean,如果没有,则默认InternalResourceViewResolver作为视图解析器
- 初始化FlashMapManager:其作用是用于检索和保存FlashMap(保存从一个URL重定向到另一个URL时的参数信息),从ApplicationContext发现有flashMapManager的Bean,如果没有,则默认使用DefaultFlashMapManager
处理请求
DispatcherServlet 接收到请求后, 执行doDispatch 调度方法, 再将请求转给Controller.
/**
* Process the actual dispatching to the handler.
* <p>The handler will be obtained by applying the servlet's HandlerMappings in order.
* The HandlerAdapter will be obtained by querying the servlet's installed HandlerAdapters
* to find the first that supports the handler class.
* <p>All HTTP methods are handled by this method. It's up to HandlerAdapters or handlers
* themselves to decide which methods are acceptable.
* @param request current HTTP request
* @param response current HTTP response
* @throws Exception in case of any kind of processing failure
*/
@SuppressWarnings("deprecation")
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
if (isGet || HttpMethod.HEAD.matches(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
HandlerAdapter 在 Spring MVC 中使用了适配器模式
适配器模式, 也叫包装器模式. 简单来说就是目标类不能直接使用, 通过一个新类进行包装一下, 适配调用方使用.把两个不兼容的接口通过一定的方式使之兼容.HandlerAdapter 主要用于支持不同类型的处理器(如 Controller、HttpRequestHandler 或者Servlet 等),让它们能够适配统一的请求处理流程。这样,Spring MVC 可以通过一个统一的接口来处理来自各种处理器的请求.
在开始执行 Controller 之前,会先调用 预处理方法 applyPreHandle,而applyPreHandle 方法的实现源码如下:
/**
* Apply preHandle methods of registered interceptors.
* @return {@code true} if the execution chain should proceed with the
* next interceptor or the handler itself. Else, DispatcherServlet assumes
* that this interceptor has already dealt with the response itself.
*/
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for (int i = 0; i < this.interceptorList.size(); i++) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
if (!interceptor.preHandle(request, response, this.handler)) {
triggerAfterCompletion(request, response, null);
return false;
}
this.interceptorIndex = i;
}
return true;
}
在 applyPreHandle 中会获取所有的拦截器 HandlerInterceptor , 并执行拦截器中的preHandle 方法,这样就会咱们前面定义的拦截器对应上了,
如果拦截器返回true, 整个发放就返回true, 继续执行后续逻辑处理如果拦截器返回fasle, 则中断后续操作
适配器模式
适配器模式, 也叫包装器模式. 将一个类的接口,转换成客户期望的另一个接口, 适配器让原本接口不兼容的类可以合作无间.简单来说就是目标类不能直接使用, 通过一个新类进行包装一下, 适配调用方使用. 把两个不兼容的接口通过一定的方式使之兼容.比如下面两个接口, 本身是不兼容的(参数类型不一样, 参数个数不一样等等)
适配器模式角色
• Target: 目标接口 (可以是抽象类或接口), 客户希望直接用的接口
• Adaptee: 适配者, 但是与Target不兼容
• Adapter: 适配器类, 此模式的核心. 通过继承或者引用适配者的对象, 把适配者转为目标接口
• client: 需要使用适配器的对象
前面学习的slf4j 就使用了适配器模式, slf4j提供了一系列打印日志的api, 底层调用的是log4j 或者logback来打日志, 我们作为调用者, 只需要调用slf4j的api就行了.
/**
* slf4j接口
*/
interface Slf4jApi{
void log(String message);
}
/**
* log4j
log4j 接口
*/
class Log4j{
void log4jLog(String message){
System.out.println("Log4j打印:"+message);
}
}
/**
* slf4j和log4j适配器
*/
class Slf4jLog4JAdapter implements Slf4jApi{
private Log4j log4j;
public Slf4jLog4JAdapter(Log4j log4j) {
this.log4j = log4j;
}
@Override
public void log(String message) {
log4j.log4jLog(message);
}
}
/**
* 客户端调用
*/
public class Slf4jDemo {
public static void main(String[] args) {
Slf4jApi slf4jApi = new Slf4jLog4JAdapter(new Log4j());
slf4jApi.log("使用slf4j打印日志");
}
}
可以看出, 我们不需要改变log4j的api,只需要通过适配器转换下, 就可以更换日志框架, 保障系统的平稳运行.
适配器模式的实现并不在slf4j-core中(只定义了Logger), 具体实现是在针对log4j的桥接器项目slf4jlog4j12中
统一数据返回格式
统一的数据返回格式使用@ControllerAdvice 和 ResponseBodyAdvice 的方式实现@ControllerAdvice 表示控制器通知类添加类ResponseAdvice , 实现ResponseBodyAdvice 接口, 并在类上添加@ControllerAdvice 注解
// 后端统一返回结果
@Data
public class Result<T> {
private int status;
private String errorMessage;
private T data;
}
package com.bite.book.config;
import com.bite.book.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;
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;
}
@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方法进行具体操作处理
存在问题
测试修改图书的接口
解决方法
package com.bite.book.config;
import com.bite.book.model.Result;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
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 {
private static ObjectMapper mapper = new ObjectMapper();
@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) {
// 这里可以自定义返回结果
if (body instanceof String) {
try {
return mapper.writeValueAsString(Result.success(body))
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
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(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 可以使用.
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
Object body;
Class valueType;
Object targetType;
if (value instanceof CharSequence) {
body = value.toString();
valueType = String.class;
targetType = String.class;
} else {
body = value;
valueType = this.getReturnValueType(value, returnType);
targetType = GenericTypeResolver.resolveType(this.getGenericType(returnType), returnType.getContainingClass());
}
if (this.isResourceType(value, returnType)) {
outputMessage.getHeaders().set("Accept-Ranges", "bytes");
if (value != null && inputMessage.getHeaders().getFirst("Range") != null && outputMessage.getServletResponse().getStatus() == 200) {
Resource resource = (Resource)value;
try {
List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();
outputMessage.getServletResponse().setStatus(HttpStatus.PARTIAL_CONTENT.value());
body = HttpRange.toResourceRegions(httpRanges, resource);
valueType = body.getClass();
targetType = RESOURCE_REGION_LIST_TYPE;
} catch (IllegalArgumentException var19) {
outputMessage.getHeaders().set("Content-Range", "bytes */" + resource.contentLength());
outputMessage.getServletResponse().setStatus(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value());
}
}
}
MediaType selectedMediaType = null;
MediaType contentType = outputMessage.getHeaders().getContentType();
boolean isContentTypePreset = contentType != null && contentType.isConcrete();
if (isContentTypePreset) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Found 'Content-Type:" + contentType + "' in response");
}
selectedMediaType = contentType;
} else {
HttpServletRequest request = inputMessage.getServletRequest();
List acceptableTypes;
try {
acceptableTypes = this.getAcceptableMediaTypes(request);
} catch (HttpMediaTypeNotAcceptableException var20) {
int series = outputMessage.getServletResponse().getStatus() / 100;
if (body != null && series != 4 && series != 5) {
throw var20;
}
if (this.logger.isDebugEnabled()) {
this.logger.debug("Ignoring error response content (if any). " + var20);
}
return;
}
List<MediaType> producibleTypes = this.getProducibleMediaTypes(request, valueType, (Type)targetType);
if (body != null && producibleTypes.isEmpty()) {
throw new HttpMessageNotWritableException("No converter found for return value of type: " + valueType);
}
List<MediaType> mediaTypesToUse = new ArrayList();
Iterator var15 = acceptableTypes.iterator();
MediaType mediaType;
while(var15.hasNext()) {
mediaType = (MediaType)var15.next();
Iterator var17 = producibleTypes.iterator();
while(var17.hasNext()) {
MediaType producibleType = (MediaType)var17.next();
if (mediaType.isCompatibleWith(producibleType)) {
mediaTypesToUse.add(this.getMostSpecificMediaType(mediaType, producibleType));
}
}
}
if (mediaTypesToUse.isEmpty()) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes);
}
if (body != null) {
throw new HttpMediaTypeNotAcceptableException(producibleTypes);
}
return;
}
MediaType.sortBySpecificityAndQuality(mediaTypesToUse);
var15 = mediaTypesToUse.iterator();
while(var15.hasNext()) {
mediaType = (MediaType)var15.next();
if (mediaType.isConcrete()) {
selectedMediaType = mediaType;
break;
}
if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
}
if (this.logger.isDebugEnabled()) {
this.logger.debug("Using '" + selectedMediaType + "', given " + acceptableTypes + " and supported " + producibleTypes);
}
}
HttpMessageConverter converter;
GenericHttpMessageConverter genericConverter;
label183: {
if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
Iterator var23 = this.messageConverters.iterator();
while(var23.hasNext()) {
converter = (HttpMessageConverter)var23.next();
genericConverter = converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter)converter : null;
if (genericConverter != null) {
if (((GenericHttpMessageConverter)converter).canWrite((Type)targetType, valueType, selectedMediaType)) {
break label183;
}
} else if (converter.canWrite(valueType, selectedMediaType)) {
break label183;
}
}
}
if (body != null) {
Set<MediaType> producibleMediaTypes = (Set)inputMessage.getServletRequest().getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
if (!isContentTypePreset && CollectionUtils.isEmpty(producibleMediaTypes)) {
throw new HttpMediaTypeNotAcceptableException(this.getSupportedMediaTypes(body.getClass()));
}
throw new HttpMessageNotWritableException("No converter for [" + valueType + "] with preset Content-Type '" + contentType + "'");
}
return;
}
body = this.getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, converter.getClass(), inputMessage, outputMessage);
if (body != null) {
LogFormatUtils.traceDebug(this.logger, (traceOn) -> {
return "Writing [" + LogFormatUtils.formatValue(body, !traceOn) + "]";
});
this.addContentDispositionHeader(inputMessage, outputMessage);
if (genericConverter != null) {
genericConverter.write(body, (Type)targetType, selectedMediaType, outputMessage);
} else {
converter.write(body, selectedMediaType, outputMessage);
}
} else if (this.logger.isDebugEnabled()) {
this.logger.debug("Nothing to write: null body");
}
}
在 ((HttpMessageConverter) converter).write(body, selectedMediaType,outputMessage) 的处理中, 调用父类的write方法由于StringHttpMessageConverter 重写了addDefaultHeaders方法, 所以会执行子类的方法。
然而子类StringHttpMessageConverter 的addDefaultHeaders方法定义接收参数为String, 此时t为Result类型, 所以出现类型不匹配"Result cannot be cast to java.lang.String"的异常
统一异常处理
统一异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的,@ControllerAdvice 表示控制器通知类, @ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执行某个通知,也就是执行某个方法事件
package com.bite.book.config;
import com.bite.book.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 ErrorHandle {
//全局异常处理
@ExceptionHandler
public 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 {
@ExceptionHandler
public Object handler(Exception e) {
return Result.fail(e.getMessage());
}
@ExceptionHandler
public Object handler(NullPointerException e) {
return Result.fail("发生NullPointerException:"+e.getMessage());
}
@ExceptionHandler
public Object handler(ArithmeticException e) {
return Result.fail("发生ArithmeticException:"+e.getMessage());
}
}
当异常不属于上面三个异常的时候,出现的异常离上面三个异常哪个最近,就报哪个异常,就近原则
@ControllerAdvice 源码分析
统一数据返回和统一异常都是基于@ControllerAdvice 注解来实现的, 通过分析@ControllerAdvice 的源码, 可以知道他们的执行流程.点击 @ControllerAdvice 实现源码如下:
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.bind.annotation;
import java.lang.annotation.Annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Component;
/**
* Specialization of {@link Component @Component} for classes that declare
* {@link ExceptionHandler @ExceptionHandler}, {@link InitBinder @InitBinder}, or
* {@link ModelAttribute @ModelAttribute} methods to be shared across
* multiple {@code @Controller} classes.
*
* <p>Classes annotated with {@code @ControllerAdvice} can be declared explicitly
* as Spring beans or auto-detected via classpath scanning. All such beans are
* sorted based on {@link org.springframework.core.Ordered Ordered} semantics or
* {@link org.springframework.core.annotation.Order @Order} /
* {@link javax.annotation.Priority @Priority} declarations, with {@code Ordered}
* semantics taking precedence over {@code @Order} / {@code @Priority} declarations.
* {@code @ControllerAdvice} beans are then applied in that order at runtime.
* Note, however, that {@code @ControllerAdvice} beans that implement
* {@link org.springframework.core.PriorityOrdered PriorityOrdered} are <em>not</em>
* given priority over {@code @ControllerAdvice} beans that implement {@code Ordered}.
* In addition, {@code Ordered} is not honored for scoped {@code @ControllerAdvice}
* beans — for example if such a bean has been configured as a request-scoped
* or session-scoped bean. For handling exceptions, an {@code @ExceptionHandler}
* will be picked on the first advice with a matching exception handler method. For
* model attributes and data binding initialization, {@code @ModelAttribute} and
* {@code @InitBinder} methods will follow {@code @ControllerAdvice} order.
*
* <p>Note: For {@code @ExceptionHandler} methods, a root exception match will be
* preferred to just matching a cause of the current exception, among the handler
* methods of a particular advice bean. However, a cause match on a higher-priority
* advice will still be preferred over any match (whether root or cause level)
* on a lower-priority advice bean. As a consequence, please declare your primary
* root exception mappings on a prioritized advice bean with a corresponding order.
*
* <p>By default, the methods in an {@code @ControllerAdvice} apply globally to
* all controllers. Use selectors such as {@link #annotations},
* {@link #basePackageClasses}, and {@link #basePackages} (or its alias
* {@link #value}) to define a more narrow subset of targeted controllers.
* If multiple selectors are declared, boolean {@code OR} logic is applied, meaning
* selected controllers should match at least one selector. Note that selector checks
* are performed at runtime, so adding many selectors may negatively impact
* performance and add complexity.
*
* @author Rossen Stoyanchev
* @author Brian Clozel
* @author Sam Brannen
* @since 3.2
* @see org.springframework.stereotype.Controller
* @see RestControllerAdvice
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
/**
* Alias for the {@link #basePackages} attribute.
* <p>Allows for more concise annotation declarations — for example,
* {@code @ControllerAdvice("org.my.pkg")} is equivalent to
* {@code @ControllerAdvice(basePackages = "org.my.pkg")}.
* @since 4.0
* @see #basePackages
*/
@AliasFor("basePackages")
String[] value() default {};
/**
* Array of base packages.
* <p>Controllers that belong to those base packages or sub-packages thereof
* will be included — for example,
* {@code @ControllerAdvice(basePackages = "org.my.pkg")} or
* {@code @ControllerAdvice(basePackages = {"org.my.pkg", "org.my.other.pkg"})}.
* <p>{@link #value} is an alias for this attribute, simply allowing for
* more concise use of the annotation.
* <p>Also consider using {@link #basePackageClasses} as a type-safe
* alternative to String-based package names.
* @since 4.0
*/
@AliasFor("value")
String[] basePackages() default {};
/**
* Type-safe alternative to {@link #basePackages} for specifying the packages
* in which to select controllers to be advised by the {@code @ControllerAdvice}
* annotated class.
* <p>Consider creating a special no-op marker class or interface in each package
* that serves no purpose other than being referenced by this attribute.
* @since 4.0
*/
Class<?>[] basePackageClasses() default {};
/**
* Array of classes.
* <p>Controllers that are assignable to at least one of the given types
* will be advised by the {@code @ControllerAdvice} annotated class.
* @since 4.0
*/
Class<?>[] assignableTypes() default {};
/**
* Array of annotation types.
* <p>Controllers that are annotated with at least one of the supplied annotation
* types will be advised by the {@code @ControllerAdvice} annotated class.
* <p>Consider creating a custom composed annotation or use a predefined one,
* like {@link RestController @RestController}.
* @since 4.0
*/
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) 这两个方法.
initHandlerAdapters(context)
initHandlerAdapters(context) 方法会取得所有实现了HandlerAdapter 接口的bean并保存起来,其中有一个类型为RequestMappingHandlerAdapter 的bean,这个bean就是@RequestMapping 注解能起作用的关键,这个bean在应用启动过程中会获取所有被@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 方法,比如返回数据前调用统一数据封装.
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;
}
}
//...
}