当前位置: 首页 > article >正文

SpringBoot | 拦截器 | 统一数据返回格式 | 统一异常处理 | 适配器模式

拦截器

拦截器是Spring框架提供的核心功能之一, 主要用来拦截用户的请求, 在指定方法前后, 根据业务需要执行预先设定的代码.

也就是说, 允许开发人员提前预定义一些逻辑, 在用户的请求响应前后执行. 也可以在用户请求前阻止其执行.

在拦截器当中,开发人员可以在应用程序中做一些通用性的操作, 比如通过拦截器来拦截前端发来的请求, 判断Session中是否有登录用户的信息. 如果有就可以放行, 如果没有就进行拦截.

基本使用

  1. 定义拦截器
  2. 注册配置拦截器

自定义拦截器:实现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 视图渲染完毕后执行,最后执行");
    }
}
  1. preHandle()方法:目标方法执行前执行. 返回true: 继续执行后续操作; 返回false: 中断后续操作.
  2. postHandle()方法:目标方法执行后执行
  3. 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 之前进行相应的业务处理,执行的流程如下图

  1. 添加拦截器后, 执行Controller的方法之前, 请求会先被拦截器拦截住. 执行 preHandle() 方法,这个方法需要返回一个布尔类型的值. 如果返回true, 就表示放行本次操作, 继续访问controller中的方法. 如果返回false,则不会放行(controller中的方法也不会执行).
  2. 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)

  1. 初始化文件上传解析器MultipartResolver:从应用上下文中获取名称为multipartResolver的Bean,如果没有名为multipartResolver的Bean,则没有提供上传文件的解析器
  2. 初始化区域解析器LocaleResolver:从应用上下文中获取名称为localeResolver的Bean,如果没有这个Bean,则默认使用AcceptHeaderLocaleResolver作为区域解析器
  3. 初始化主题解析器ThemeResolver:从应用上下文中获取名称为themeResolver的Bean,如果没有这个Bean,则默认使用FixedThemeResolver作为主题解析器
  4. 初始化处理器映射器HandlerMappings:处理器映射器作用,1)通过处理器映射器找到对应的处理器适配器,将请求交给适配器处理;2)缓存每个请求地址URL对应的位置(Controller.xxx方法);如果在ApplicationContext发现有HandlerMappings,则从ApplicationContext中获取到所有的HandlerMappings,并进行排序;如果在ApplicationContext中没有发现有处理器映射器,则默认BeanNameUrlHandlerMapping作为处理器映射器
  5. 初始化处理器适配器HandlerAdapter:作用是通过调用具体的方法来处理具体的请求;如果在ApplicationContext发现有handlerAdapter,则从ApplicationContext中获取到所有的HandlerAdapter,并进行排序;如果在ApplicationContext中没有发现处理器适配器,则默SimpleControllerHandlerAdapter作为处理器适配器
  6. 初始化异常处理器解析器HandlerExceptionResolver:如果在ApplicationContext发现有handlerExceptionResolver,则从ApplicationContext中获取到所有的HandlerExceptionResolver,并进行排序;如果在ApplicationContext中没有发现异常处理器解析器,则不设置异常处理器
  7. 初始化RequestToViewNameTranslator:其作用是从Request中获取viewName,从ApplicationContext发现有viewNameTranslator的Bean,如果没有,则默认使用DefaultRequestToViewNameTranslator
  8. 初始化视图解析器ViewResolvers:先从ApplicationContext中获取名为viewResolver的Bean,如果没有,则默认InternalResourceViewResolver作为视图解析器
  9. 初始化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 &mdash; 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 &mdash; 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 &mdash; 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;
        }
    }
    //...
}

http://www.kler.cn/a/429853.html

相关文章:

  • 线程安全问题介绍
  • SpringCloud系列教程:微服务的未来(十二)OpenFeign连接池、最佳实践、日志、微服务拆分
  • LLaMA-Factory web微调大模型并导出大模型
  • 链式设计模式:装饰模式,职责链模式
  • 一根网线如何用软路由给手机、电脑分配设置不同IP
  • 从watch、watchEffect、useEffect原理到vue、react响应原理
  • keepalived 各模式设置
  • 实时数据开发|Flink状态计算 有状态VS无状态,区别和优劣
  • NanoLog起步笔记-7-log解压过程初探
  • 什么是反向代理?作用、原理和实例详解
  • 反向代理-缓存篇
  • ubuntu22.04 使用可以用的镜像源获取你要的镜像
  • 数据结构与算法学习笔记----树与图的深度优先遍历
  • MACOS M1/M2芯片 Homebrew 安装教程
  • FastAPI解决跨域报错net::ERR_FAILED 200 (OK)
  • REDMI瞄准游戏赛道,推出小屏平板
  • 单片机C51--笔记8-STC89C51RC/RD-IIC协议
  • 太速科技-614-基于6U VPX FPGA VU9P 4路100G、32路10Gbps的光纤卡
  • 力扣第95题 不同的二叉搜索树 II
  • 【WebRTC】Android SDK使用教学
  • 如何使用靜態IP代理?【詳細教程】
  • 云原生周刊:在Docker上部署大语言模型
  • scala 身份证号码