【JavaEE】SpringBoot 统一功能处理:拦截器、统一数据返回与异常处理的综合应用与源码解析

news2025/4/17 3:07:46

目录

    • SpringBoot 统⼀功能处理
      • 拦截器
        • 拦截器快速⼊⻔
        • 拦截器详解
          • 拦截路径
          • 拦截器执⾏流程
        • 登录校验
          • 定义拦截器
          • 注册配置拦截器
        • DispatcherServlet 源码分析(了解)
          • 初始化(了解)
      • `DispatcherServlet`的初始化
        • 1. `HttpServletBean.init()`
        • 2. `FrameworkServlet.initServletBean()`
      • `WebApplicationContext`的建立和配置
        • 1. `FrameworkServlet.initWebApplicationContext()`
      • 初始化Spring MVC的9大组件
        • 1. `FrameworkServlet.onRefresh()`
        • 2. `DispatcherServlet.initStrategies()`
      • 总结
          • 处理请求(核⼼)
      • `DispatcherServlet.doDispatch()` 方法的流程
      • 拦截器(HandlerInterceptor)的作用
      • 总结
          • 适配器模式
      • 统⼀数据返回格式
        • 快速⼊⻔
        • 存在问题
        • 案例代码修改
        • 优点
      • 统⼀异常处理
      • @ControllerAdvice 源码分析
      • 案例代码
        • 登录⻚⾯
        • 图书列表
        • 其他
      • 总结


SpringBoot 统⼀功能处理

  1. 掌握拦截器的使⽤, 及其原理
  2. 学习统⼀数据返回格式和统⼀异常处理的操作
  3. 了解⼀些Spring的源码

拦截器

之前我们完成了强制登录的功能, 后端程序根据Session来判断⽤⼾是否登录, 但是实现⽅法是⽐较⿇烦的

  • 需要修改每个接⼝的处理逻辑
  • 需要修改每个接⼝的返回结果
  • 接⼝定义修改, 前端代码也需要跟着修改

有没有更简单的办法, 统⼀拦截所有的请求, 并进⾏Session校验呢, 这⾥⼀种新的解决办法: 拦截器

拦截器快速⼊⻔

什么是拦截器?

拦截器是Spring框架提供的核⼼功能之⼀, 主要⽤来拦截⽤⼾的请求, 在指定⽅法前后, 根据业务需要执⾏预先设定的代码.

拦截器的作用维度:URL

也就是说, 允许开发⼈员提前预定义⼀些逻辑, 在⽤⼾的请求响应前后执⾏. 也可以在⽤⼾请求前阻⽌其执⾏.

在拦截器当中,开发⼈员可以在应⽤程序中做⼀些通⽤性的操作, ⽐如通过拦截器来拦截前端发来的请求, 判断Session中是否有登录⽤⼾的信息. 如果有就可以放⾏, 如果没有就进⾏拦截.

在这里插入图片描述

⽐如我们去银⾏办理业务, 在办理业务前后, 就可以加⼀些拦截操作

办理业务之前, 先取号, 如果带⾝份证了就取号成功

业务办理结束, 给业务办理⼈员的服务进⾏评价.

这些就是"拦截器"做的⼯作

拦截器的基本使⽤:

拦截器的使⽤步骤分为两步:

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

⾃定义拦截器:实现HandlerInterceptor接⼝,并重写其所有⽅法

public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}
  • **preHandle()**⽅法:⽬标⽅法执⾏前执⾏. 返回true: 继续执⾏后续操作; 返回false: 中断后续操作.
  • **postHandle()**⽅法:⽬标⽅法执⾏后执⾏
  • afterCompletion()⽅法:视图渲染完毕后执⾏,最后执⾏(后端开发现在⼏乎不涉及视图, 暂不了解)
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("目标方法执行前");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("目标方法执行后");
    }
}

注册配置拦截器:实现WebMvcConfigurer接⼝,并重写addInterceptors⽅法

@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, 再观察运⾏结果

在这里插入图片描述

可以看到, 拦截器拦截了请求, 没有进⾏响应

拦截器详解

拦截器的⼊⻔程序完成之后,接下来我们来介绍拦截器的使⽤细节。拦截器的使⽤细节我们主要介绍两个部分:

  1. 拦截器的拦截路径配置
  2. 拦截器实现原理
拦截路径

拦截路径是指我们定义的这个拦截器, 对哪些请求⽣效.

我们在注册配置拦截器的时候, 通过 addPathPatterns() ⽅法指定要拦截哪些请求. 也可以通过excludePathPatterns() 指定不拦截哪些请求.

上述代码中, 我们配置的是 /** , 表⽰拦截所有的请求.

⽐如⽤⼾登录校验, 我们希望可以对除了登录之外所有的路径⽣效.

@Configuration
public class WebConfig 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

以上拦截规则可以拦截此项⽬中的使⽤ URL,包括静态⽂件(图⽚⽂件, JS 和 CSS 等⽂件).

拦截器执⾏流程

正常的调⽤顺序:

在这里插入图片描述

有了拦截器之后,会在调⽤ Controller 之前进⾏相应的业务处理,执⾏的流程如下图

在这里插入图片描述

  1. 添加拦截器后, 执⾏Controller的⽅法之前, 请求会先被拦截器拦截住. 执⾏ preHandle() ⽅法,这个⽅法需要返回⼀个布尔类型的值. 如果返回true, 就表⽰放⾏本次操作, 继续访问controller中的⽅法. 如果返回false,则不会放⾏(controller中的⽅法也不会执⾏).

  2. controller当中的⽅法执⾏完毕后,再回过来执⾏ postHandle() 这个⽅法以及afterCompletion() ⽅法,执⾏完毕之后,最终给浏览器响应数据.

登录校验

学习拦截器的基本操作之后,接下来我们需要完成最后⼀步操作:通过拦截器来完成图书管理系统中的登录校验功能

定义拦截器

从session中获取⽤⼾信息, 如果session中不存在, 则返回false,并设置http状态码为401, 否则返回true.

@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("登录拦截器校验...");
        //返回true表示放行,返回false表示拦截
        //检验用户是否登录
        HttpSession session = request.getSession(true);//true表示没有session就创建一个,false表示没有就直接返回
        UserInfo userInfo = (UserInfo) session.getAttribute(Constants.SESSION_USER_KEY);
        if (userInfo != null && userInfo.getId() >= 0) {
            return true;//放行
        }
        response.setStatus(401);//401表示未认证登录
        return false;//拦截
    }
}

http状态码401: Unauthorized

Indicates that authentication is required and was either not provided or has failed. If the request already included authorization credentials, then the 401 status code indicates that those credentials were not accepted.

中⽂解释: 未经过认证. 指⽰⾝份验证是必需的, 没有提供⾝份验证或⾝份验证失败. 如果请求已经包含授权凭据,那么401状态码表⽰不接受这些凭据。

注册配置拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;

    //包含一些不应该被拦截的的URL路径
    private static List<String> excludePath = Arrays.asList(
            "/user/login",//排除这个特定的路径
            //因为我们写的不是完全的前后端分离
            //下面是为了拦截前端部分的静态资源
            "/css/**",
            "/js/**",
            "/pic/**",
            "/**/*.html");

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)//添加了拦截器
                .addPathPatterns("/**")//  /**表示给所有方法添加拦截器,即匹配所有路径
                .excludePathPatterns(excludePath);
    }
}

删除之前的登录校验代码

@RequestMapping("/getBookListByPage")
    //为了方便更好拓展,最好返回结果也是一个对象
    public Result getBookListByPage(PageRequest pageRequest, HttpSession session) {
        log.info("查询翻页信息,pageRequest:{}", pageRequest);
        用户登录校验
        //UserInfo userInfo= (UserInfo) session.getAttribute("session_user_key");
        //if(userInfo==null||userInfo.getId()<=0||"".equals(userInfo.getUserName())){
        //    //用户未登录
        //    return Result.unLogin();
        //}
        //校验成功
        if (pageRequest.getPageSize() < 0 || pageRequest.getCurrentPage() < 1) {
            //每页显示条数为负或者当前页数不为正数则错误
            return Result.fail("参数校验失败");
        }
        PageResult<BookInfo> bookInfoPageResult = null;
        try {
            bookInfoPageResult = bookService.selectBookInfoByPage(pageRequest);
            return Result.success(bookInfoPageResult);
        } catch (Exception e) {
            log.error("查询翻页信息错误,e:{}", e);
            return Result.fail(e.getMessage());
        }
    }

运⾏程序, 通过Postman进⾏测试:

  1. 查看图书列表

http://127.0.0.1:8080/book/getBookListByPage

在这里插入图片描述

观察返回结果: http状态码401

也可以通过Fiddler抓包观察

在这里插入图片描述

  1. 登录

http://127.0.0.1:8080/user/login?name=admin&password=admin

在这里插入图片描述

  1. 再次查看图书列表

数据进⾏了返回

在这里插入图片描述

DispatcherServlet 源码分析(了解)

观察我们的服务启动⽇志:

在这里插入图片描述

当Spiring的Tomcat启动之后, 有⼀个核⼼的类 DispatcherServlet, 它来控制程序的执⾏顺序.

dispatcher:调度程序

servlet的生命周期

init
service
destroy

所有请求都会先进到 DispatcherServlet,执⾏ doDispatch 调度⽅法. 如果有拦截器, 会先执⾏拦截器preHandle() ⽅法的代码, 如果 preHandle() 返回true, 继续访问 controller 中的⽅法. controller 当中的⽅法执⾏完毕后,再回过来执⾏ 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

init() 具体代码如下:

	public final void init() throws ServletException {
        // ServletConfigPropertyValues 是静态内部类,使⽤ ServletConfig 获取 web.xml
        PropertyValues pvs = new ServletConfigPropertyValues(this.getServletConfig(), this.requiredProperties);
        if (!pvs.isEmpty()) {
            try {
                // 使⽤ BeanWrapper 来构造 DispatcherServlet
                BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
                ResourceLoader resourceLoader = new ServletContextResourceLoader(this.getServletContext());
                bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.getEnvironment()));
                this.initBeanWrapper(bw);
                bw.setPropertyValues(pvs, true);
            } catch (BeansException var4) {
                if (this.logger.isErrorEnabled()) {
                    this.logger.error("Failed to set bean properties on servlet '" + this.getServletName() + "'", var4);
                }

                throw var4;
            }
        }
		// 让⼦类实现的⽅法,这种在⽗类定义在⼦类实现的⽅式叫做模版⽅法模式
        this.initServletBean();
    }

HttpServletBeaninit() 中调⽤了 initServletBean() , 它是在FrameworkServlet 类中实现的, 主要作⽤是建⽴ WebApplicationContext 容器(有时也称上下⽂), 并加载 SpringMVC 配置⽂件中定义的 Bean到该容器中, 最后将该容器添加到 ServletContext 中. 下⾯是 initServletBean() 的具体代码:

	protected final void initServletBean() throws ServletException {
        this.getServletContext().log("Initializing Spring " + this.getClass().getSimpleName() + " '" + this.getServletName() + "'");
        if (this.logger.isInfoEnabled()) {
            this.logger.info("Initializing Servlet '" + this.getServletName() + "'");
        }

        long startTime = System.currentTimeMillis();

        try {
            //创建ApplicationContext容器
            this.webApplicationContext = this.initWebApplicationContext();
            this.initFrameworkServlet();
        } catch (RuntimeException | ServletException var4) {
            this.logger.error("Context initialization failed", var4);
            throw var4;
        }

        if (this.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";
            this.logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails + "': request parameters and headers will be " + value);
        }

        if (this.logger.isInfoEnabled()) {
            this.logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
        }

    }

此处打印的⽇志, 也正是控制台打印出来的⽇志

在这里插入图片描述

源码跟踪技巧:

在阅读框架源码的时候, ⼀定要抓住关键点, 找到核⼼流程.

切忌从头到尾⼀⾏⼀⾏代码去看, ⼀个⽅法的去研究, ⼀定要找到关键流程, 抓住关键点, 先在宏观上对整个流程或者整个原理有⼀个认识, 有精⼒再去研究其中的细节.

初始化web容器的过程中, 会通过 onRefresh 来初始化SpringMVC的容器

	protected WebApplicationContext initWebApplicationContext() {
        //...
        if (!this.refreshEventReceived) {
            //初始化Spring MVC
            synchronized(this.onRefreshMonitor) {
                this.onRefresh(wac);
            }
        }
    	//...
        return wac;
    }
	protected void onRefresh(ApplicationContext context) {
        this.initStrategies(context);
    }

    protected void initStrategies(ApplicationContext context) {
        this.initMultipartResolver(context);
        this.initLocaleResolver(context);
        this.initThemeResolver(context);
        this.initHandlerMappings(context);
        this.initHandlerAdapters(context);
        this.initHandlerExceptionResolvers(context);
        this.initRequestToViewNameTranslator(context);
        this.initViewResolvers(context);
        this.initFlashMapManager(context);
    }

在initStrategies()中进⾏9⼤组件的初始化, 如果没有配置相应的组件,就使⽤默认定义的组件(在DispatcherServlet.properties中有配置默认的策略, ⼤致了解即可)

⽅法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

以下是ChatGPT的回答:

这个过程涉及到加载和配置DispatcherServlet以及初始化与之相关的一系列Spring MVC组件。下面是对这个过程的分析和解释:

DispatcherServlet的初始化

DispatcherServlet的初始化是在其父类HttpServletBeaninit()方法中完成的。这个方法的关键任务是从web.xml或者通过编程式配置加载DispatcherServlet的配置,并调用子类(如FrameworkServletDispatcherServlet)的初始化方法。

1. HttpServletBean.init()
  • 加载Servlet配置:使用ServletConfigPropertyValuesServletConfig中读取配置,这可能包括对Spring MVC组件的定制配置。
  • 属性设置:通过BeanWrapper为Servlet设置属性,这包括了一些自定义编辑器和资源加载器的设置。
2. FrameworkServlet.initServletBean()
  • 日志记录:记录初始化开始和完成的时间,这对于调试和监控是有帮助的。
  • WebApplicationContext初始化:通过调用initWebApplicationContext()来建立和配置Web应用上下文。这个上下文是Spring MVC框架中所有组件交互的基础。
  • 异常处理:处理在初始化过程中可能发生的异常。

WebApplicationContext的建立和配置

WebApplicationContext是Spring MVC中非常核心的部分,它负责管理Spring MVC应用中的所有Bean。

1. FrameworkServlet.initWebApplicationContext()
  • 获取根上下文:首先尝试从ServletContext中获取根上下文。
  • 创建新的上下文:如果没有现成的上下文,创建一个新的。
  • 上下文刷新:通过调用onRefresh()来完成上下文的初始化。

初始化Spring MVC的9大组件

DispatcherServlet在Spring MVC框架中起着中央调度器的作用。它负责初始化一系列关键组件,这些组件共同处理HTTP请求。

1. FrameworkServlet.onRefresh()
  • 调用initStrategies():这个方法负责初始化Spring MVC的九大组件。
2. DispatcherServlet.initStrategies()
  • 组件初始化:包括文件上传解析器、区域解析器、主题解析器、处理器映射器、处理器适配器、异常处理器解析器、视图名称翻译器、视图解析器和FlashMap管理器的初始化。如果应用上下文中没有相应的Bean定义,则使用默认的策略。

总结

这个初始化过程体现了Spring框架的灵活性和可扩展性。通过这种方式,DispatcherServlet加载和配置自己的环境,并准备好处理传入的HTTP请求。每个组件都在整个请求处理流程中扮演着特定的角色,确保Spring MVC应用能够以高度可配置和可扩展的方式运行。

在这里插入图片描述

处理请求(核⼼)

DispatcherServlet 接收到请求后, 执⾏doDispatch 调度⽅法, 再将请求转给Controller.

我们来看 doDispatch ⽅法的具体实现

    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpServletRequest processedRequest = request;
        HandlerExecutionChain mappedHandler = null;
        boolean multipartRequestParsed = false;
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

        try {
            try {
                ModelAndView mv = null;
                Exception dispatchException = null;

                try {
                    processedRequest = this.checkMultipart(request);
                    multipartRequestParsed = processedRequest != request;
                    //1. 获取执⾏链
                    //遍历所有的 HandlerMapping 找到与请求对应的Handler
                    mappedHandler = this.getHandler(processedRequest);
                    if (mappedHandler == null) {
                        this.noHandlerFound(processedRequest, response);
                        return;
                    }
					//2. 获取适配器
                    //遍历所有的 HandlerAdapter,找到可以处理该 Handler 的 HandlerAdapter
                    HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
                    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;
                        }
                    }
					//3. 执⾏拦截器preHandle⽅法
                    if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                        return;
                    }
					//4. 执⾏⽬标⽅法
                    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                    if (asyncManager.isConcurrentHandlingStarted()) {
                        return;
                    }

                    this.applyDefaultViewName(processedRequest, mv);
                    //5. 执⾏拦截器postHandle⽅法
                    mappedHandler.applyPostHandle(processedRequest, response, mv);
                } catch (Exception var20) {
                    dispatchException = var20;
                } catch (Throwable var21) {
                    dispatchException = new NestedServletException("Handler dispatch failed", var21);
                }
				//6. 处理视图, 处理之后执⾏拦截器afterCompletion⽅法
                this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
            } catch (Exception var22) {
                //7. 执⾏拦截器afterCompletion⽅法
                this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
            } catch (Throwable var23) {
                this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
            }

        } finally {
            if (asyncManager.isConcurrentHandlingStarted()) {
                if (mappedHandler != null) {
                    mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
                }
            } else if (multipartRequestParsed) {
                this.cleanupMultipart(processedRequest);
            }

        }
    }

HandlerAdapter 在 Spring MVC 中使⽤了适配器模式, 下⾯详细再介绍适配器模式, 也叫包装器模式. 简单来说就是⽬标类不能直接使⽤, 通过⼀个新类进⾏包装⼀下, 适配调⽤⽅使⽤.

把两个不兼容的接⼝通过⼀定的⽅式使之兼容.

HandlerAdapter 主要⽤于⽀持不同类型的处理器(如 Controller、HttpRequestHandler 或者Servlet 等),让它们能够适配统⼀的请求处理流程。这样,Spring MVC 可以通过⼀个统⼀的接⼝来处理来⾃各种处理器的请求.

从上述源码可以看出在开始执⾏ Controller 之前,会先调⽤ 预处理⽅法 applyPreHandle,⽽ applyPreHandle ⽅法的实现源码如下:

    boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
        for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
            // 获取项⽬中使⽤的拦截器 HandlerInterceptor
            HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
            if (!interceptor.preHandle(request, response, this.handler)) {
                this.triggerAfterCompletion(request, response, (Exception)null);
                return false;
            }
        }

        return true;
    }

在 applyPreHandle 中会获取所有的拦截器 HandlerInterceptor , 并执⾏拦截器中的 preHandle ⽅法,这样就会咱们前⾯定义的拦截器对应上了,如下图所⽰:

在这里插入图片描述

如果拦截器返回true, 整个发放就返回true, 继续执⾏后续逻辑处理

如果拦截器返回fasle, 则中断后续操作

在这里插入图片描述

DispatcherServlet.doDispatch() 方法的流程

  1. 处理多部分请求:
    • 检查并处理请求是否为多部分(如文件上传)。this.checkMultipart(request)会在请求是多部分时返回一个包装后的请求对象。
  2. 获取处理器(Handler):
    • 通过this.getHandler(processedRequest)获取与请求相匹配的HandlerExecutionChain(处理器执行链)。这个链包含了处理器(如Controller)和一系列拦截器。
  3. 获取处理器适配器(Handler Adapter):
    • 使用this.getHandlerAdapter(mappedHandler.getHandler())获取能够处理该请求的HandlerAdapterHandlerAdapter负责调用实际的处理器(Controller)方法。
  4. 执行拦截器的preHandle方法:
    • mappedHandler.applyPreHandle(processedRequest, response)会执行拦截器链中所有拦截器的preHandle方法。如果任何一个拦截器返回false,则中断处理流程。
  5. 执行目标方法:
    • mv = ha.handle(processedRequest, response, mappedHandler.getHandler())调用处理器(Controller)的方法,处理请求并返回ModelAndView对象。
  6. 执行拦截器的postHandle方法:
    • mappedHandler.applyPostHandle(processedRequest, response, mv)在处理器方法执行后,ModelAndView返回前执行。
  7. 处理视图和模型:
    • this.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException)处理ModelAndView对象,渲染视图。
  8. 执行拦截器的afterCompletion方法:
    • 在请求处理完毕后,无论成功还是发生异常,都会执行this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22),调用拦截器的afterCompletion方法。

拦截器(HandlerInterceptor)的作用

拦截器在Spring MVC中用于在处理器(Controller)执行前后执行一些操作。它们通常用于日志记录、权限检查、事务处理等。

  • preHandle:在处理器执行前调用。如果返回false,则中断执行链,后续的postHandle和处理器方法将不会被执行。
  • postHandle:在处理器执行后,但在视图渲染前调用。
  • afterCompletion:在请求完全结束后调用,用于清理资源。

总结

这个流程展示了Spring MVC如何处理一个HTTP请求:从确定处理器、适配器,到执行拦截器和处理器,再到渲染视图。这个过程中,拦截器的作用是在请求的前后提供了一个可插拔的方式来干预处理流程。这种架构提供了高度的灵活性和扩展性,允许开发者根据需要定制请求的处理过程。

适配器模式

HandlerAdapter 在 Spring MVC 中使⽤了适配器模式

适配器模式定义

适配器模式, 也叫包装器模式. 将⼀个类的接⼝,转换成客⼾期望的另⼀个接⼝, 适配器让原本接⼝不兼容的类可以合作⽆间.

简单来说就是⽬标类不能直接使⽤, 通过⼀个新类进⾏包装⼀下, 适配调⽤⽅使⽤. 把两个不兼容的接⼝通过⼀定的⽅式使之兼容.

⽐如下⾯两个接⼝, 本⾝是不兼容的(参数类型不⼀样, 参数个数不⼀样等等)

在这里插入图片描述

可以通过适配器的⽅式, 使之兼容

在这里插入图片描述

⽇常⽣活中, 适配器模式也是⾮常常⻅的

⽐如转换插头, ⽹络转接头等

出国旅⾏必备物品之⼀就是转换插头. 不同国家的插头标准是不⼀样的, 出国后我们⼿机/电脑充电器可能就没办法使⽤了. ⽐如美国电器 110V,中国 220V,就要有⼀个适配器将 110V 转化为 220V. 国内也经常使⽤转换插头把两头转为三头, 或者三头转两头

适配器模式⻆⾊

  • Target: ⽬标接⼝ (可以是抽象类或接⼝), 客⼾希望直接⽤的接⼝
  • Adaptee: 适配者, 但是与Target不兼容
  • Adapter: 适配器类, 此模式的核⼼. 通过继承或者引⽤适配者的对象, 把适配者转为⽬标接⼝
  • client: 需要使⽤适配器的对象

适配器模式的实现

场景: 前⾯学习的slf4j 就使⽤了适配器模式, slf4j提供了⼀系列打印⽇志的api, 底层调⽤的是log4j 或者logback来打⽇志, 我们作为调⽤者, 只需要调⽤slf4j的api就⾏了.

/**
 * slf4j接⼝
 */
public interface Slf4jApi {
    void log(String message);
}

/**
 * log4j 接⼝
 */
public class Log4j {
    public void log(String message){
        System.out.println("Log4j:"+message);
    }
}

/**
 * slf4j和log4j适配器
 */
public class Slf4jLog4jAdapter implements Slf4jApi{
    private Log4j log4j;

    public Slf4jLog4jAdapter(Log4j log4j){
        this.log4j=log4j;
    }

    @Override
    public void log(String message) {
        log4j.log(message);
    }
}

/**
 * 客⼾端调⽤
 */
public class Main {
    public static void main(String[] args) {
        Slf4jApi api=new Slf4jLog4jAdapter(new Log4j());
        api.log("我是通过Slf4j打印的");
    }
}
  • Target: ⽬标接⼝,Slf4jApi
  • Adaptee: 适配者,Log4j
  • Adapter: 适配器类,Slf4jLog4jAdapter
  • client: 需要使⽤适配器的对象,Main

可以看出, 我们不需要改变log4j的api,只需要通过适配器转换下, 就可以更换⽇志框架, 保障系统的平稳运⾏.

适配器模式的实现并不在slf4j-core中(只定义了Logger), 具体实现是在针对log4j的桥接器项⽬slf4jlog4j12中

设计模式的使⽤⾮常灵活, ⼀个项⽬中通常会含有多种设计模式.

适配器模式应⽤场景

⼀般来说,适配器模式可以看作⼀种"补偿模式",⽤来补救设计上的缺陷. 应⽤这种模式算是"⽆奈之举", 如果在设计初期,我们就能协调规避接⼝不兼容的问题, 就不需要使⽤适配器模式了所以适配器模式更多的应⽤场景主要是对正在运⾏的代码进⾏改造, 并且希望可以复⽤原有代码实现新的功能. ⽐如版本升级等.

统⼀数据返回格式

强制登录案例中, 我们共做了两部分⼯作

  1. 通过Session来判断⽤⼾是否登录
  2. 对后端返回数据进⾏封装, 告知前端处理的结果

回顾

后端统⼀返回结果

@Data
public class Result<T> {
 //业务状态码
 private ResultCode code;//0 成功   -1 失败   -2 未登录
 //错误信息
 private String errMsg;
 //数据
 private T data;
}

后端逻辑处理

 @RequestMapping("/getBookListByPage")
 //为了方便更好拓展,最好返回结果也是一个对象
 public Result getBookListByPage(PageRequest pageRequest, HttpSession session) {
     log.info("查询翻页信息,pageRequest:{}", pageRequest);
     用户登录校验
     //UserInfo userInfo= (UserInfo) session.getAttribute("session_user_key");
     //if(userInfo==null||userInfo.getId()<=0||"".equals(userInfo.getUserName())){
     //    //用户未登录
     //    return Result.unLogin();
     //}
     //校验成功
     if (pageRequest.getPageSize() < 0 || pageRequest.getCurrentPage() < 1) {
         //每页显示条数为负或者当前页数不为正数则错误
         return Result.fail("参数校验失败");
     }
     PageResult<BookInfo> bookInfoPageResult = null;
     try {
         bookInfoPageResult = bookService.selectBookInfoByPage(pageRequest);
         return Result.success(bookInfoPageResult);
     } catch (Exception e) {
         log.error("查询翻页信息错误,e:{}", e);
         return Result.fail(e.getMessage());
     }
 }

Result.success(pageResult) 就是对返回数据进⾏了封装

拦截器帮我们实现了第⼀个功能, 接下来看SpringBoot对第⼆个功能如何⽀持

快速⼊⻔

统⼀的数据返回格式使⽤ @ControllerAdviceResponseBodyAdvice接口 的⽅式实现 @ControllerAdvice 表⽰控制器通知类

添加类 ResponseAdvice , 实现 ResponseBodyAdvice 接⼝, 并在类上添加 @ControllerAdvice 注解

@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) {
        //返回之前,需要做的事情
        //body就是返回的结果
        return Result.success(body);
    }
}
  • supports⽅法: 判断是否要执⾏beforeBodyWrite⽅法. true为执⾏, false不执⾏. 通过该⽅法可以选择哪些类或哪些⽅法的response要进⾏处理, 其他的不进⾏处理.

从returnType获取类名和⽅法名


  • beforeBodyWrite⽅法: 对response⽅法进⾏具体操作处理

测试

测试接⼝: http://127.0.0.1:8080/book/queryBookInfoById?bookId=1

添加统⼀数据返回格式之前:

在这里插入图片描述

添加统⼀数据返回格式之后:

在这里插入图片描述

存在问题

问题现象:

我们继续测试修改图书的接⼝: http://127.0.0.1:8080/book/updateBook

在这里插入图片描述

结果显⽰, 发⽣内部错误

查看数据库, 发现数据操作成功

查看⽇志, ⽇志报错

在这里插入图片描述

多测试⼏种不同的返回结果, 发现只有返回结果为String类型时才有这种错误发⽣.

即请求返回类型是Result时就不需要再进行处理了

返回结果为String时不能正确处理

测试代码:

@RequestMapping("/test")
@RestController
public class TestController {
    @RequestMapping("t1")
    public Boolean t1(){
        return true;
    }

    @RequestMapping("t2")
    public Integer t2(){
        return 123;
    }

    @RequestMapping("t3")
    public String t3(){
        return "hello";
    }

    @RequestMapping("t4")
    public BookInfo t4(){
        return new BookInfo();
    }

    @RequestMapping("t5")
    public Result t5(){
        return Result.success("success");
    }
}

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;

    //包含一些不应该被拦截的的URL路径
    private static List<String> excludePath = Arrays.asList(
            "/user/login",//排除这个特定的路径
            //因为我们写的不是完全的前后端分离
            //下面是为了拦截前端部分的静态资源
            "/css/**",
            "/js/**",
            "/pic/**",
            "/**/*.html",
            "/test/**");

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)//添加了拦截器
                .addPathPatterns("/**")//  /**表示给所有方法添加拦截器,即匹配所有路径
                .excludePathPatterns(excludePath);
    }
}

解决⽅案:

@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        //返回之前,需要做的事情
        //body就是返回的结果
        if(body instanceof Result){
            return body;
        }
        if(body instanceof String){
            return objectMapper.writeValueAsString(Result.success(body));
        }
        return Result.success(body);
    }
}

重新测试, 结果返回正常:

在这里插入图片描述

原因分析:

SpringMVC默认会注册⼀些⾃带的 HttpMessageConverter (从先后顺序排列分别为ByteArrayHttpMessageConverter ,StringHttpMessageConverter , SourceHttpMessageConverter,SourceHttpMessageConverter , AllEncompassingFormHttpMessageConverter )

public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
        implements BeanFactoryAware, InitializingBean {

    //...
    public RequestMappingHandlerAdapter() {
        this.messageConverters = new ArrayList<>(4);
        this.messageConverters.add(new ByteArrayHttpMessageConverter());
        this.messageConverters.add(new StringHttpMessageConverter());
        if (!shouldIgnoreXml) {
            try {
                this.messageConverters.add(new SourceHttpMessageConverter<>());
            } catch (Error err) {
                // Ignore when no TransformerFactory implementation is available
            }
        }
        this.messageConverters.add(new AllEncompassingFormHttpMessageConverter())
    }
    //...
}

其中 AllEncompassingFormHttpMessageConverter 会根据项⽬依赖情况 添加对应的HttpMessageConverter

public  AllEncompassingFormHttpMessageConverter() {
    if(!shouldIgnoreXml){
        try {
            addPartConverter(new SourceHttpMessageConverter<>());
        } catch (Error err) {
            // Ignore when no TransformerFactory implementation is available
        }

        if (jaxb2Present && !jackson2XmlPresent) {
            addPartConverter(new Jaxb2RootElementHttpMessageConverter());
        }
    }

    if(kotlinSerializationJsonPresent){
        addPartConverter(new KotlinSerializationJsonHttpMessageConverter());
    }
    if(jackson2Present){
        addPartConverter(new MappingJackson2HttpMessageConverter());
    }
    else if(gsonPresent){
        addPartConverter(new GsonHttpMessageConverter());
    }
    else if(jsonbPresent){
        addPartConverter(new JsonbHttpMessageConverter());
    }

    if(jackson2XmlPresent&&!shouldIgnoreXml){
        addPartConverter(new MappingJackson2XmlHttpMessageConverter());
    }

    if(jackson2SmilePresent){
        addPartConverter(new MappingJackson2SmileHttpMessageConverter());
    }
}

在依赖中引⼊jackson包后,容器会把 MappingJackson2HttpMessageConverter ⾃动注册到messageConverters 链的末尾.

Spring会根据返回的数据类型, 从 messageConverters 链选择合适的HttpMessageConverter .

当返回的数据是⾮字符串时, 使⽤的 MappingJackson2HttpMessageConverter 写⼊返回对象.

当返回的数据是字符申时, StringHttpMessageConverter 会先被遍历到,这时会认为StringHttpMessageConverter 可以使⽤.

public abstract class AbstractMessageConverterMethodProcessor extends
        AbstractMessageConverterMethodArgumentResolver
        implements HandlerMethodReturnValueHandler {

    //...代码省略
    protected <T> void writeWithMessageConverters(@Nullable T value,
                                                  MethodParameter returnType,
                                                  ServletServerHttpRequest inputMessage, ServletServerHttpResponse
                                                          outputMessage)
            throws IOException, HttpMediaTypeNotAcceptableException,
            HttpMessageNotWritableException {

        //...代码省略
        if (selectedMediaType != null) {
            selectedMediaType = selectedMediaType.removeQualityValue();
            for (HttpMessageConverter<?> converter : this.messageConverters) {
                GenericHttpMessageConverter genericConverter = (converter
                        instanceof GenericHttpMessageConverter ?
                        (GenericHttpMessageConverter<?>) converter : null);
                if (genericConverter != null ?
                        ((GenericHttpMessageConverter)
                                converter).canWrite(targetType, valueType, selectedMediaType) :
                        converter.canWrite(valueType, selectedMediaType)) {
                    //getAdvice().beforeBodyWrite 执⾏之后, body转换成了Result类型的
                    结果
                            body = getAdvice().beforeBodyWrite(body, returnType,
                            selectedMediaType,
                            20 (Class<? extends HttpMessageConverter<?>>)
                            converter.getClass(),
                            inputMessage, outputMessage);
                    if (body != null) {
                        Object theBody = body;
                        LogFormatUtils.traceDebug(logger, traceOn ->
                                "Writing [" + LogFormatUtils.formatValue(theBody,
                                        !traceOn) + "]");
                        addContentDispositionHeader(inputMessage, outputMessage);
                        if (genericConverter != null) {
                            genericConverter.write(body, targetType,
                                    selectedMediaType, outputMessage);
                        } else {
                            //此时cover为StringHttpMessageConverter
                            ((HttpMessageConverter) converter).write(body,
                                    selectedMediaType, outputMessage);
                        }
                    } else {
                        if (logger.isDebugEnabled()) {
                            logger.debug("Nothing to write: null body");
                        }
                    }
                    return;
                }
            }
        }
        //...代码省略

    }
    //...代码省略
}

((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage) 的处理中, 调⽤⽗类的write⽅法

由于 StringHttpMessageConverter 重写了addDefaultHeaders⽅法, 所以会执⾏⼦类的⽅法

然⽽⼦类 StringHttpMessageConverter 的addDefaultHeaders⽅法定义接收参数为String, 此时t为Result类型, 所以出现类型不匹配"Result cannot be cast to java.lang.String"的异常

案例代码修改

如果⼀些⽅法返回的结果已经是Result类型了, 那就直接返回Result类型的结果即可

@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
    //返回之前,需要做的事情
    //body就是返回的结果
    if(body instanceof Result){
        return body;
    }
    if(body instanceof String){
        return objectMapper.writeValueAsString(Result.success(body));
    }
    return Result.success(body);
}

@SneakyThrows是lombok的一个注解,会自动帮我们加上trycatch

优点
  1. ⽅便前端程序员更好的接收和解析后端数据接⼝返回的数据

  2. 降低前端程序员和后端程序员的沟通成本, 按照某个格式实现就可以了, 因为所有接⼝都是这样返回的.

  3. 有利于项⽬统⼀数据的维护和修改.

  4. 有利于后端技术部⻔的统⼀规范的标准制定, 不会出现稀奇古怪的返回内容.

统⼀异常处理

统⼀异常处理使⽤的是 @ControllerAdvice + @ExceptionHandler 来实现的,@ControllerAdvice 表⽰控制器通知类, @ExceptionHandler 是异常处理器,两个结合表⽰当出现异常的时候执⾏某个通知,也就是执⾏某个⽅法事件

具体代码如下:

@Slf4j
@ResponseBody
@ControllerAdvice
public class ErrorHandler {

    @ExceptionHandler
    public Result exception(Exception e){
        log.error("发生异常,e:{}",e);
        return Result.fail("内部错误");
    }
}

类名, ⽅法名和返回值可以⾃定义, 重要的是注解

接⼝返回为数据时, 需要加 @ResponseBody 注解,如果不加这个注解就认为返回的是页面

类上面三个注解都要加,还有方法上的那个注解

以上代码表⽰,如果代码出现 Exception 异常(包括 Exception 的⼦类), 就返回⼀个 Result 的对象, Result 对象的设置参考 Result.fail(e.getMessage())

public static <T>Result<T> fail(String errMsg){
    Result result=new Result();
    result.setCode(ResultCode.FAIL);
    result.setErrMsg(errMsg);
    result.setData(null);
    return result;
}

我们可以针对不同的异常, 返回不同的结果.

@ResponseBody
@Slf4j
@ControllerAdvice
public class ErrorHandler {

    @ExceptionHandler
    public Result exception(Exception e){
        log.error("发生异常,e:{}",e);
        return Result.fail("内部错误");
    }

    @ExceptionHandler
    public Result exception(NullPointerException e){
        log.error("发生异常,e:{}",e);
        return Result.fail("NullPointerException 异常");
    }

    @ExceptionHandler
    public Result exception(ArithmeticException e){
        log.error("发生异常,e:{}",e);
        return Result.fail("ArithmeticException 异常");
    }
}

模拟制造异常:

@RequestMapping("/test")
@RestController
public class TestController {
    @RequestMapping("t1")
    public Boolean t1(){
        int a=1/0;
        return true;
    }

    @RequestMapping("t2")
    public Integer t2(){
        String a=null;
        System.out.println(a.length());
        return 123;
    }

    @RequestMapping("t3")
    public String t3(){
        int[] a={1,2,3};
        System.out.println(a[5]);
        return "hello";
    }
}

当有多个异常通知时,匹配顺序为当前类及其⼦类向上依次匹配

/test/t1 抛出ArithmeticException, 运⾏结果如下:

在这里插入图片描述

/test/t2 抛出NullPointerException, 运⾏结果如下:

在这里插入图片描述

/test/t3 抛出Exception, 运⾏结果如下:

在这里插入图片描述

log.error("发生异常,e:{}",e);以上代码最好都加上这句,不然比如这里调用/test/t3就不会在控制台出现这些错误日志了

在这里插入图片描述

@ControllerAdvice 源码分析

统⼀数据返回和统⼀异常都是基于 @ControllerAdvice 注解来实现的, 通过分析 @ControllerAdvice 的源码, 可以知道他们的执⾏流程.

点击 @ControllerAdvice 实现源码如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
    @AliasFor("basePackages")
    String[] value() default {};

    @AliasFor("value")
    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};

    Class<?>[] assignableTypes() default {};

    Class<? extends Annotation>[] annotations() default {};
}

从上述源码可以看出 @ControllerAdvice 派⽣于 @Component 组件, 这也就是为什么没有五⼤注解, ControllerAdvice 就⽣效的原因.

下⾯我们看看Spring是怎么实现的, 还是从 DispatcherServlet 的代码开始分析.DispatcherServlet 对象在创建时会初始化⼀系列的对象:

public class DispatcherServlet extends FrameworkServlet {
    //...
    @Override
    protected void onRefresh(ApplicationContext context) {
        initStrategies(context);
    }

    /**
     * Initialize the strategy objects that this servlet uses.
     * <p>May be overridden in subclasses in order to initialize further
     * strategy objects.
     */
    protected void initStrategies(ApplicationContext context) {
        initMultipartResolver(context);
        initLocaleResolver(context);
        initThemeResolver(context);
        initHandlerMappings(context);
        initHandlerAdapters(context);
        initHandlerExceptionResolvers(context);
        initRequestToViewNameTranslator(context);
        initViewResolvers(context);
        initFlashMapManager(context);
    }
    //...

}

对于 @ControllerAdvice 注解,我们重点关注 initHandlerAdapters(context)initHandlerExceptionResolvers(context) 这两个⽅法.

  1. initHandlerAdapters(context)

initHandlerAdapters(context) ⽅法会取得所有实现了 HandlerAdapter 接⼝的bean并保存起来,其中有⼀个类型为 RequestMappingHandlerAdapter 的bean,这个bean就是 @RequestMapping 注解能起作⽤的关键,这个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 ⽅法,⽐如返回数据前调⽤统⼀数据封装⾄于DispatcherServlet和RequestMappingHandlerAdapter是如何交互的这就是另⼀个复杂的话题了,此处不赘述, 源码部分难度⽐较⾼, 且枯燥, ⼤家以了解为主.

  1. initHandlerExceptionResolvers(context)

接下来看 DispatcherServletinitHandlerExceptionResolvers(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;
        }
    }
    //...
}

案例代码

通过上⾯统⼀功能的添加, 我们后端的接⼝已经发⽣了变化(后端返回的数据格式统⼀变成了Result类型), 所以我们需要对前端代码进⾏修改

实际开发中, 后端接⼝的设计需要经过多⽅评审检查(review). 在接⼝设计时就会考虑格式化的统⼀化,尽可能的避免返⼯

当前是学习阶段, 给⼤家讲了这个接⼝设计的演变过程

登录⻚⾯

登录界⾯没有拦截, 只是返回结果发⽣了变化, 所以只需要根据返回结果修改对应代码即可

登录结果代码修改

        function login() {
            $.ajax({
                url:"/user/login",
                type:"post",
                data:{
                    "userName":$("#userName").val(),
                    "password":$("#password").val()
                },
                success:function(result){
                    console.log(result);
                    if(result!=null&&result.code=="SUCCESS"&&result.data==true){
                        location.href = "book_list.html";
                    }else{
                        alert("用户名或密码错误");
                    }
                }
            });
        } 
图书列表

针对图书列表⻚有两处变化

  1. 拦截器进⾏了强制登录校验, 如果校验失败, 则http状态码返回401, 此时会⾛ajax的error逻辑处理
  2. 接⼝返回结果发⽣了变化

图书列表代码修改:

            function getBookList() {
                $.ajax({
                    type: "get",
                    url: "/book/getBookListByPage" + location.search,
                    success: function (result) {
                        //真实的前端处理逻辑比后端复杂
                        if (result.code == "UNLOGIN") {
                            location.href = "login.html";
                            return;
                        }

                        var finalHtml = "";
                        //加载列表
                        var pageResult = result.data;
                        for (var book of pageResult.records) {
                            //根据每一条记录拼接html,也就是一个<tr>
                            finalHtml += '<tr>';
                            finalHtml += '<td><input type="checkbox" name="selectBook" value="' + book.id + '" id="selectBook" class="book-select"></td>';
                            finalHtml += '<td>' + book.id + '</td>';
                            finalHtml += '<td>' + book.bookName + '</td>';
                            finalHtml += '<td>' + book.author + '</td>';
                            finalHtml += '<td>' + book.count + '</td>';
                            finalHtml += '<td>' + book.price + '</td>';
                            finalHtml += '<td>' + book.publish + '</td>';
                            finalHtml += '<td>' + book.statusCN + '</td>';
                            finalHtml += '<td>';
                            finalHtml += '<div class="op">';
                            finalHtml += '<a href="book_update.html?bookId=' + book.id + '">修改</a>';
                            finalHtml += '<a href="javascript:void(0)" οnclick="deleteBook(' + book.id + ')">删除</a>';
                            finalHtml += '</div>';
                            finalHtml += '</td>';
                            finalHtml += '</tr>';
                        }

                        $("tBody").html(finalHtml);

                        //翻页信息
                        $("#pageContainer").jqPaginator({
                            totalCounts: pageResult.total, //总记录数
                            pageSize: 10,    //每页的个数   
                            visiblePages: 5, //可视页数
                            currentPage: pageResult.pageRequest.currentPage,  //当前页码
                            first: '<li class="page-item"><a class="page-link">首页</a></li>',
                            prev: '<li class="page-item"><a class="page-link" href="javascript:void(0);">上一页<\/a><\/li>',
                            next: '<li class="page-item"><a class="page-link" href="javascript:void(0);">下一页<\/a><\/li>',
                            last: '<li class="page-item"><a class="page-link" href="javascript:void(0);">最后一页<\/a><\/li>',
                            page: '<li class="page-item"><a class="page-link" href="javascript:void(0);">{{page}}<\/a><\/li>',
                            //页面初始化和页码点击时都会执行
                            onPageChange: function (page, type) {
                                console.log("第" + page + "页, 类型:" + type);
                                if (type == "change") {
                                    location.href = "book_list.html?currentPage=" + page;
                                }
                            }
                        });

                    },
                    error: function (error) {
                        console.log(error);
                        if (error.status == 401) {
                            console.log("401");
                            location.href = "login.html";
                        }
                    }
                });
            }
其他

参考图书列表, 对删除图书, 批量删除图书,添加图书, 修改图书接⼝添加⽤⼾强制登录以及统⼀格式返回的逻辑处理

  1. 删除图书
            function deleteBook(bookId) {
                var isDelete = confirm("确认删除?");
                if (isDelete) {
                    //删除图书
                    $.ajax({
                        type: "post",
                        url: "/book/updateBook",
                        data: {
                            id: bookId,
                            status: 0
                        },
                        success: function (result) {
                            if (result != null && result.code == "SUCCESS" && result.data == "") {
                                //删除成功
                                location.href = "book_list.html";
                            } else {
                                alert(result);
                            }
                        },
                        error: function (error) {
                            console.log(error);
                            //用户未登录
                            if (error != null && error.status == 401) {
                                location.href = "login.html";
                            }
                        }
                    });
                }
            }
  1. 批量删除图书
            function batchDelete() {
                var isDelete = confirm("确认批量删除?");
                if (isDelete) {
                    //获取复选框的id
                    var ids = [];
                    $("input:checkbox[name='selectBook']:checked").each(function () {
                        ids.push($(this).val());
                    });
                    console.log(ids);

                    $.ajax({
                        type: "post",
                        url: "/book/batchDelete?ids=" + ids,
                        success: function (result) {
                            if (result != null && result.code == "SUCCESS" && result.data == "") {
                                //删除成功
                                location.href = "book_list.html";
                            } else {
                                alert(result);
                            }
                        },
                        error: function (error) {
                            console.log(error);
                            //用户未登录
                            if (error != null && error.status == 401) {
                                location.href = "login.html";
                            }
                        }
                    });
                }
            }
  1. 添加图书

如果后端返回的结果是String类型,当我们用统一结果返回时,返回的是JSON字符串,content-type 是 text/html,我们需要把它转为JSON

如果后端进行转换:

@RequestMapping(value = "/addBook",produces = "application/json")
public String addBook(BookInfo bookInfo)

如果前端进行转换:把字符串转为对象

JSON.parse(result)
        function add() {
            $.ajax({
                type: "post",
                url: "/book/addBook",
                data: $("#addBook").serialize(),//提交整个form表单
                success: function (result) {
                    console.log(result);
                    console.log(typeof result)
                    if (result != null && result.code == "SUCCESS" && result.data == "") {
                        //图书添加成功
                        location.href = "book_list.html";
                    } else {
                        alert(result);
                    }
                },
                error: function (error) {
                    console.log(error);
                    //用户未登录
                    if (error != null && error.status == 401) {
                        location.href = "login.html";
                    }
                }
            });
        }
  1. 获取图书详情
        $.ajax({
            type: "get",
            url: "/book/queryBookInfoById" + location.search,
            success: function (result) {
                if (result != null && result.code == "SUCCESS") {
                    var book = result.data;
                    if (book != null) {
                        //页面输入框的填充
                        $("#bookId").val(book.id);
                        $("#bookName").val(book.bookName);
                        $("#bookAuthor").val(book.author);
                        $("#bookStock").val(book.count);
                        $("#bookPrice").val(book.price);
                        $("#bookPublisher").val(book.publish);
                        $("#bookStatus").val(book.status);
                    } else {
                        alert("图书不存在");
                    }
                } else {
                    alert(result.errMsg);
                }
            },
            error: function (error) {
                console.log(error);
                //用户未登录
                if (error != null && error.status == 401) {
                    location.href = "login.html";
                }
            }
        });
  1. 修改图书
        function update() {
            $.ajax({
                type: "post",
                url: "/book/updateBook",
                data: $("#updateBook").serialize(),
                success: function (result) {
                    if (result != null && result.code == "SUCCESS" && result.data == "") {
                        location.href = "book_list.html";
                    } else {
                        alert(result);
                    }
                },
                error: function (error) {
                    console.log(error);
                    //用户未登录
                    if (error != null && error.status == 401) {
                        location.href = "login.html";
                    }
                }
            });
        }

总结

本章节主要介绍了SpringBoot 对⼀些统⼀功能的处理⽀持.

  1. 拦截器的实现主要分两部分: 1. 定义拦截器(实现HandlerInterceptor 接⼝) 2. 配置拦截器
  2. 统⼀数据返回格式通过@ControllerAdvice + ResponseBodyAdvice 来实现
  3. 统⼀异常处理使⽤@ControllerAdvice + @ExceptionHandler 来实现, 并且可以分异常来处理
  4. 学习了DispatcherServlet的⼀些源码.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2061471.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Leetcode JAVA刷刷站(75)颜色分类

一、题目概述 二、思路方向 在Java中&#xff0c;要实现这个排序算法&#xff0c;我们可以使用三指针的方法&#xff0c;也称作荷兰国旗问题&#xff08;Dutch National Flag Problem&#xff09;。这个方法使用三个指针来分别追踪数组中小于、等于和大于中间值的元素的边界。在…

未设置辅助手机的谷歌账号停用,申诉回来后登录需要手机验证,验证两次后成功恢复。

谷歌账号被停用后怎么办&#xff1f;果断申诉&#xff0c;申诉方法和模板等见我前面的文章或视频。 通常申诉以后1-2天会反馈结果&#xff0c;而且大部分&#xff08;80%以上&#xff09;会第一次就被审批通过&#xff0c;如下图所示&#xff1a; 当收到上面这样的邮件&#x…

OSError:[WinError -2146959355] 服务器运行失败

其实就是 找不到指定的程序&#xff0c;看报错&#xff0c;提示返回失败&#xff0c;最后通过设置wps由系统启动&#xff0c;解决报错 首先打开电脑&#xff0c;找到桌面上的【WPS文档】&#xff0c;右键&#xff0c;在打开的菜单中&#xff0c;选择【属性】选项 在弹出的属性…

GPT-4o: 引领多模态AI识别技术的革命【文末附GPT-4o教程】

GPT-4o简介 在人工智能技术的飞速发展中&#xff0c;图像识别已成为其核心能力之一。随着技术的不断演进&#xff0c;AI的图像处理能力已经从简单的内容识别&#xff0c;发展到能够将视觉信息转化为深入的文字描述。OpenAI最新发布的GPT-4o模型&#xff0c;正是这一进步的杰出…

AI + 3D 机器人视觉领域综合资源库

随着人工智能技术的不断发展,3D 机器人视觉领域已经成为了一个备受关注的研究方向。在这个领域中,研究者们致力于探索如何让机器人更好地理解三维空间,从而实现更加智能和灵活的操作。为了方便大家学习和研究,这里介绍一个全面的资源库——Awesome Robotics 3D,它汇集了最…

Bootstrap UI 编辑器

以下是 15 款最好的 Bootstrap 编辑器或者是在线编辑工具。 1. Bootstrap Magic 这是一个 Bootstrap 主题生成器&#xff0c;使用最新的 Bootstrap 3 版本和 Angular JS 版本&#xff0c;提供一个鲜活的用户修改预览。它包括了各种各样的导入&#xff0c;一个颜色选择器和智能…

写给大数据开发:在Databricks中自定义函数

你是否曾经在处理海量数据时感到力不从心?是否在重复编写相似代码时感到厌烦?如果是,那么Databricks中的自定义函数可能就是你一直在寻找的救星。在接下来的5分钟里,让我们一起探索如何利用这个强大的工具来revolutionize你的大数据开发工作流程。 目录 为什么要在Databrick…

kubernetes Pod 入门

一、Pod 的概念 kubernetes并不直接管理容器&#xff0c;它的最小管理单元是Pod。Pod是一个或多个容器的组合&#xff0c;这些容器贡献存储&#xff0c;网络&#xff0c;命名空间以及运行规范。在Pod中所有容器被统一安排和调度&#xff0c;在共享上下文中运行&#xff08;共享…

一款基于BS的美食网站的设计与实现

TOC springboot586一款基于BS的美食网站的设计与实现--论文 选题背景 由于互联网技术的快速发展&#xff0c;使得各部门都是以数字化、信息化、无纸化的发展趋势&#xff0c;随着趋势的发展&#xff0c;各种决策系统、辅助系统也应运而生&#xff0c;其中&#xff0c;美食网…

高性能Web服务器-- Nginx 的架构与安装详解

1.1 Nginx 概述 1.1.1 Nginx简介 Nginx&#xff1a;engine X &#xff0c;2002年开发&#xff0c;分为社区版和商业版(nginx plus ) 2019年3月11日 F5 Networks 6.7亿美元的价格收购 Nginx是免费的、开源的、高性能的HTTP和反向代理服务器、邮件代理服务器、以及TCP/UDP代理…

如何用CWE API 来减轻软件产品中的安全风险

本文分享自华为云开发者社区《用CWE API 减轻软件产品中的安全风险》作者&#xff1a; Uncle_Tom 1. CWE REST API 推出的目的 8 月 8 号&#xff0c;CWE™ 计划推出了“CWE REST API”。 CWE™计划由美国网络安全与基础设施安全局(Cybersecurity & Infrastructure Secur…

PyTorch——Dataloader使用

一、Dataloader是啥 前面我在写PyTorch的第一篇文章里讲过Dataset是啥&#xff0c;Dataset就是将数据集分类&#xff0c;并且分析出这些数据集它的位置哪、大小多少、这个数据集一共有多少数据......等等信息 那么把Dataset比作一副扑克牌&#xff0c;那么如果你就让这副牌放在…

《机器学习》 逻辑回归 大批量数据的下采样 <8>

一、案例文件 同样使用上节课的银行贷款案例&#xff0c;其文件内容大致如下&#xff1a;&#xff08;共28万多条&#xff0c;31列&#xff09; 现在要继续接着上节课的内容对模型进行优化 二、下采样流程 1、流程图示 2、具体流程介绍 1&#xff09;切分原数据集 大…

77、ansible及常见模块

ansible 一、ansible&#xff1a; 远程自动化运维 ansible是基于python开发的配置管理和应用部署工具。 也是自动化运维的重要工具。 可以批量配置&#xff0c;部署&#xff0c;管理上千台主机。 只需要在一台主机ansible就可以完成其他主机的操作。 1.1、操作模式&…

Dell 服务器 PowerEdge T440 通过BIOS配置RAID阵列

目录 1.清除当前RAID磁盘阵列配置 1.1开机按F2进入System Setup管理界面&#xff1b; 1.2点击Device Settings; 1.3选择RAID controller in Slot 4:DELL PERC Configuration Utility&#xff1b;卡型号> 1.4选择Configuration Management&#xff1b; 1.5选择View Dis…

Java 2.4 - JVM

一、Java 内存区域详解&#xff08;重点&#xff09; 本篇讨论的是 HotSpot 虚拟机 相比于 C 而言&#xff0c;程序员不需要对每个 new 操作都写对应的 delete / free 操作&#xff0c;这些操作我们会交给虚拟机去做。因此&#xff0c;如果不了解虚拟机的原理&#xff0c;一旦…

React 学习——React.memo

1、默认情况下&#xff1a;子跟着父一起渲染 2、memo 缓存,只有props发生变化的时候才会重新渲染 import { memo, useState } from react; // 默认情况下&#xff1a;子跟着父一起渲染 //memo 缓存,只有props发生变化的时候才会重新渲染 const MemoSon memo(function Son()…

Java使用Easy Excel对Excel进行操作

Easy Excel使用教程API&#xff1a; 读Excel | Easy Excel 官网 使用代码示例&#xff1a; 需要自行创建一个Maven项目&#xff0c;然后pom文件中需要的依赖如下&#xff1a; <dependencies><!-- easyExcel 表格依赖 --><dependency><groupId>com.a…

Qt实现tcp协议

void Widget::readyRead_slot() {//读取服务器发来的数据QByteArray msg socket->readAll();QString str QString::fromLocal8Bit(msg);QStringList list str.split(:);if(list.at(0) userName){QString str2;for (int i 1; i < list.count(); i) {str2 list.at(i);…

数据结构初阶(1)——算法的时间复杂度和空间复杂度

目录 1.算法效率 1.1 如何衡量一个算法的好坏 1.2算法的复杂度 2.时间复杂度 2.1时间复杂度的概念 2.2大O的渐进表示法 2.3常见时间复杂度计算举例 4. 常见复杂度对比 5.复杂度的oj练习 5.1消失的数字 5.2旋转数组 1.算法效率 1.1 如何衡量一个算法的好坏 代码不一…