JavaEE进阶(10)SpringBoot统一功能处理:拦截器入门及详解、DispatcherServlet源码、统一数据返回格式、统一异常处理、@ControllerAdvice源码、案例代码补充

接上次博客:JavaEE进阶(9)MyBatis 操作数据库(进阶):动态SQL、案例练习:表白墙、图书管理系统(用户登录、图书列表、修改图书、删除图书、批量删除、强制登录)-CSDN博客 

目录

拦截器

拦截器快速入门

什么是拦截器?

自定义拦截器

注册配置拦截器

拦截器详解

拦截路径

拦截器执行流程

强制登录校验

DispatcherServlet 源码分析(了解)

初始化(了解)  

处理请求(核心)

适配器模式

统一数据返回格式

快速入门

存在问题

原因分析

优点

统一异常处理

@ControllerAdvice 源码分析

initHandlerAdapters(context) 方法

initHandlerExceptionResolvers(context)

案例代码

登录页面

图书列表页

增加图书修改:

修改图书:

删除图书:


拦截器

在上一节,我们已经实现了强制登录的功能,但是使用 Session 来判断用户是否登录的方法比较繁琐:

  • 需要修改每个接口的处理逻辑;
  • 需要修改每个接口的返回结果;
  • 修改接口定义后,前端代码也需要做相应调整。

是否有更简单的方法来统一拦截所有请求,并进行 Session 校验呢?答案是肯定的,我们可以学习一种新的解决方案:拦截器。

Spring 拦截器(Interceptor)是一种用于拦截请求并执行预处理和后处理的机制。它们是基于 Java 的 AOP(Aspect-Oriented Programming)思想实现的,在 Spring MVC 中被广泛应用于处理请求、身份验证、日志记录、性能监控等方面。拦截器提供了一种强大的方式来增强应用程序的功能,而不需要修改现有的代码。

Spring 拦截器的主要功能和用法包括:

  1. 预处理和后处理逻辑:拦截器可以在请求被处理前执行预处理逻辑,比如身份验证、参数验证等操作;也可以在请求被处理后执行后处理逻辑,比如记录日志、添加跟踪标识等操作。

  2. 权限控制:拦截器可以用于实现权限控制逻辑,例如检查用户是否具有执行特定操作的权限,并在需要时拦截请求。

  3. 日志记录:拦截器可以用于记录请求和响应的详细信息,以便进行故障排查、性能监控等。

  4. 跨域请求处理:拦截器可以用于处理跨域请求,例如添加跨域响应头。

  5. 资源缓存:拦截器可以用于控制和管理静态资源的缓存,以提高应用程序的性能。

  6. 统一异常处理:拦截器可以用于捕获并处理请求处理过程中的异常,实现统一的异常处理逻辑。

AOP(Aspect-Oriented Programming,面向切面编程)是一种软件开发方法,它使开发者能够通过将横切关注点(cross-cutting concerns)从应用程序的核心业务逻辑中分离出来,以模块化的方式管理这些关注点。横切关注点指的是那些影响应用程序的多个部分、不属于核心业务逻辑但是在整个应用程序中广泛存在的功能,比如日志记录、事务管理、安全性、缓存、性能监控等。

AOP 的核心思想是将这些横切关注点抽象成一个称为“切面”的模块,然后通过一种称为“织入(weaving)”的机制将这些切面模块与应用程序的核心业务逻辑进行结合,从而实现对横切关注点的统一管理。这种方式使得横切关注点的变化不会影响应用程序的核心业务逻辑,同时也提高了代码的可维护性、可重用性和可测试性。

理解 AOP 可能需要花费一些时间,因为它是一种比较抽象的概念,但是一旦理解了它的核心思想,就会发现它在实际应用中的价值。

让我用一个更简单的方式来解释一下:

想象你在写一个程序,这个程序有很多功能,比如存储数据、验证用户权限、记录日志等等(就像我们还未完全完成的图书管理系统一样)。通常情况下,这些功能会被分散到不同的方法中,和核心业务逻辑混在一起。

AOP 的思想是把这些功能(比如存储数据、验证权限、记录日志等)抽象成一个个模块,叫做切面(Aspect)。然后,我们就可以通过一种机制,让这些切面模块和我们的核心业务逻辑关联起来,而不用直接在核心业务逻辑中写这些功能的代码。

再举个例子,假设你的程序有一个方法用来存储用户的订单。你可能会写一段代码来验证用户是否有权限执行这个操作、记录用户的操作日志等等。但是,如果你使用了 AOP,你可以把这些功能抽象成一个切面,然后把这个切面和存储订单的方法关联起来,这样就可以在不改变存储订单方法的情况下实现权限验证和日志记录等功能。

总之,AOP 的核心思想就是把程序中的一些通用功能抽象成一个个模块,然后通过一种机制把这些模块和程序的核心逻辑关联起来。这样可以提高代码的重用性、可维护性和可扩展性。

拦截器快速入门

什么是拦截器?

拦截器是Spring框架提供的核心功能之一,主要用于拦截用户的请求,在指定方法前后,根据业务需要执行预先设定的代码。简单来说,拦截器允许开发人员提前预定义一些逻辑,在用户的请求响应前后执行,也可以在用户请求前阻止其执行。

在拦截器中,开发人员可以在应用程序中执行一些通用性的操作。例如,通过拦截器来拦截前端发来的请求,然后判断 Session 中是否有登录用户的信息。如果存在登录信息,则可以放行请求;如果不存在登录信息,则可以进行拦截处理。

拦截器提供了一种灵活的机制,使得我们能够在请求的不同阶段进行处理,从而实现统一的业务逻辑,例如身份验证、日志记录、权限检查等。通过拦截器,我们可以将这些通用的功能逻辑从业务代码中分离出来,提高代码的复用性和可维护性。同时,拦截器还能够有效地控制请求的流程,增强应用程序的安全性和稳定性。

拦截器可以被形象地比喻为守卫大门的保安。就像一个建筑物的保安会在门口检查来访者的身份并根据需要执行特定的安全检查一样,拦截器在应用程序中充当着类似的角色。

当一个请求到达应用程序时,拦截器就像是一道安全门,会拦截并检查请求的内容、来源、权限等信息。如果请求满足了预设的条件,就像允许合法访客通过大门进入建筑物一样,拦截器会放行请求,使其继续流向后续的处理器或者控制器。反之,如果请求不符合条件,就像是不合法的访客被拦截在门外一样,拦截器可以阻止请求的继续执行,执行特定的拦截逻辑或者返回相应的错误信息。

通过这种保安的角色,拦截器能够确保应用程序的安全性和稳定性。它可以拦截恶意请求、未经授权的访问、非法操作等,从而保护系统的正常运行和用户数据的安全。同时,拦截器也能够执行一些通用的功能逻辑,比如日志记录、性能监控、异常处理等,从而提高应用程序的可维护性和可扩展性。

下面我们先来学习下拦截器的基本使用。

当使用拦截器时,一般会按照以下两个步骤进行:

  1. 定义拦截器: 拦截器的定义是指编写拦截器类并实现特定接口,如HandlerInterceptor接口。在定义拦截器时,我们可以编写一些通用的业务逻辑,例如登录验证、权限检查、日志记录等。拦截器的作用在于在请求进入处理器之前和离开处理器之后,对请求进行拦截和处理。因此,通过定义拦截器,我们可以在请求的不同阶段执行预先设定的代码逻辑,从而实现统一的业务处理。

  2. 注册配置拦截器: 注册配置拦截器是指将定义好的拦截器添加到Spring容器中,并配置拦截器的相关信息,例如指定拦截器对象、拦截的请求路径等。在Spring框架中,我们可以通过配置文件或者使用Java Config的方式来进行拦截器的注册。通过注册配置拦截器,我们告诉Spring框架在接收到请求时应该如何使用这个拦截器,并在何时调用其相应的拦截逻辑。

总的来说,拦截器的使用步骤包括定义拦截器和注册配置拦截器两个主要步骤。通过定义拦截器,我们可以编写通用的业务逻辑,并在请求的不同阶段执行相应的拦截处理;通过注册配置拦截器,我们告诉Spring框架在接收到请求时应该如何使用这个拦截器,从而实现统一的请求处理。

自定义拦截器

实现HandlerInterceptor接口,并重写其所有方法。

首先,我们需要创建一个拦截器类,该类通常需要实现Spring框架提供的HandlerInterceptor接口。在这个拦截器类中,我们就可以编写你所需要的拦截逻辑,例如登录验证、权限检查、日志记录等。具体的拦截逻辑根据我们自己的业务需求而定。

在定义拦截器时,我们需要实现preHandle、postHandle和afterCompletion三个方法,分别用于在请求处理前、请求处理后、视图渲染完成后执行相应的拦截逻辑。

源代码:

public interface HandlerInterceptor {
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return true;
    }

    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
    }

    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
    }
}

 

package com.example.librarysystem.book.interceptor;

import com.example.librarysystem.book.model.Constants;
import com.example.librarysystem.book.model.UserInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.Nullable;
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.preHandle.... ");
        return true;
    }
    
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
        log.info("目标方法执行后执行: LoginInterceptor.postHandle.... ");
    }

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
        log.info("视图渲染执行后执行: LoginInterceptor.afterCompletion.... ");
    }
}
  1. preHandle()方法: 在目标方法执行之前执行,该方法返回一个布尔值。如果返回true,则继续执行后续操作;如果返回false,则中断后续操作。在preHandle()方法中,通常可以进行一些预处理操作,例如权限检查、登录验证等。

  2. postHandle()方法: 在目标方法执行后、视图渲染前执行。postHandle()方法可以对请求的处理结果进行进一步处理或修改,但不能阻止请求的执行。例如,可以在postHandle()方法中添加一些日志记录或者对响应结果进行处理。

  3. afterCompletion()方法: 在视图渲染完成后执行,即请求处理完成并且视图渲染结束后执行。在这个方法中可以进行一些资源的清理工作,例如释放资源、记录请求处理的耗时等。需要注意的是,由于后端开发通常不涉及视图渲染,所以在实际应用中可能会较少用到这个方法。

这些方法的调用顺序是固定的:preHandle() -> 目标方法执行 -> postHandle() -> 视图渲染 -> afterCompletion()。通过这些方法,拦截器可以灵活地对请求进行拦截和处理,实现统一的业务逻辑,提高代码的可维护性和可扩展性。

注册配置拦截器

实现WebMvcConfigurer接口,并重写addInterceptors方法:

定义好拦截器后,需要将其注册配置到Spring容器中。通常情况下,可以通过配置文件或者使用Java Config的方式来进行拦截器的注册。在Spring Boot中,可以通过继承WebMvcConfigurerAdapter类或者实现WebMvcConfigurer接口来配置拦截器。然后,在addInterceptors方法中添加拦截器的配置信息,包括指定拦截器对象和拦截的请求路径等。这样,Spring框架在接收到请求时就会自动调用拦截器的相关方法,执行拦截逻辑。

源代码:

在这个接口中,定义了一系列默认方法(default methods),用于配置各种 Web MVC 相关的功能。

这些默认方法包括:

  • configurePathMatch(PathMatchConfigurer configurer):用于配置路径匹配器。
  • configureContentNegotiation(ContentNegotiationConfigurer configurer):用于配置内容协商策略。
  • configureAsyncSupport(AsyncSupportConfigurer configurer):用于配置异步支持。
  • configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer):用于配置默认的 Servlet 处理器。
  • addFormatters(FormatterRegistry registry):用于添加格式化器。
  • addInterceptors(InterceptorRegistry registry):用于添加拦截器。
  • addResourceHandlers(ResourceHandlerRegistry registry):用于添加资源处理器。
  • addCorsMappings(CorsRegistry registry):用于添加跨域配置。
  • addViewControllers(ViewControllerRegistry registry):用于添加视图控制器。
  • configureViewResolvers(ViewResolverRegistry registry):用于配置视图解析器。
  • addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers):用于添加方法参数解析器。
  • addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers):用于添加方法返回值处理器。
  • configureMessageConverters(List<HttpMessageConverter<?>> converters):用于配置消息转换器。
  • extendMessageConverters(List<HttpMessageConverter<?>> converters):用于扩展消息转换器。
  • configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers):用于配置异常处理器。
  • extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers):用于扩展异常处理器。
  • getValidator():获取验证器。
  • getMessageCodesResolver():获取消息代码解析器。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.web.servlet.config.annotation;

import java.util.List;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.HandlerExceptionResolver;

public interface WebMvcConfigurer {
    default void configurePathMatch(PathMatchConfigurer configurer) {
    }

    default void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    }

    default void configureAsyncSupport(AsyncSupportConfigurer configurer) {
    }

    default void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    }

    default void addFormatters(FormatterRegistry registry) {
    }

    default void addInterceptors(InterceptorRegistry registry) {
    }

    default void addResourceHandlers(ResourceHandlerRegistry registry) {
    }

    default void addCorsMappings(CorsRegistry registry) {
    }

    default void addViewControllers(ViewControllerRegistry registry) {
    }

    default void configureViewResolvers(ViewResolverRegistry registry) {
    }

    default void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
    }

    default void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
    }

    default void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    }

    default void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    }

    default void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    }

    default void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    }

    @Nullable
    default Validator getValidator() {
        return null;
    }

    @Nullable
    default MessageCodesResolver getMessageCodesResolver() {
        return null;
    }
}

package com.example.librarysystem.book.config;

import com.example.librarysystem.book.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("/**");
    }
}

现在运行一下:

拦截器拦截了请求,没有进行响应。

拦截器详解

拦截器的入门程序完成之后,我们来介绍拦截器的一些使用细节。

拦截器的使用主要涉及两个方面:

  1. 拦截器的拦截路径配置: 在使用拦截器时,我们需要配置拦截器拦截的路径。这些路径决定了哪些请求会被拦截器拦截并执行相应的拦截逻辑。通过配置拦截路径,我们可以精确地控制拦截器的作用范围,使其只对特定的请求进行拦截处理,而不影响其他请求。这样可以提高拦截器的效率,并避免对不需要拦截的请求产生影响。

  2. 拦截器实现原理: 拦截器的实现原理涉及到Spring MVC的执行流程。Spring MVC框架的核心是DispatcherServlet,它负责接收请求并将请求分发给合适的处理器(Controller)。在请求处理的过程中,DispatcherServlet会按照一定的执行流程调用拦截器的相关方法,例如preHandle()、postHandle()和afterCompletion()等。通过这些方法,拦截器可以在请求的不同阶段执行相应的拦截逻辑。拦截器的实现原理通常涉及到AOP(面向切面编程)和责任链模式等技术,通过这些技术可以实现对请求的拦截和处理。理解拦截器的实现原理有助于我们更好地理解拦截器的工作机制,并在需要时进行灵活的定制和扩展。

拦截路径

拦截路径指的是我们在定义拦截器时指定拦截器对哪些请求生效。在注册配置拦截器时,通常通过addPathPatterns()方法来指定要拦截哪些请求,同时也可以使用excludePathPatterns()方法来指定不拦截哪些请求。

在上述代码中,我们配置的拦截路径是 /**,表示拦截所有的请求。这意味着该拦截器会对所有的请求都生效,不管请求的路径是什么,都会被该拦截器拦截并执行相应的拦截逻辑。

通过指定拦截路径,我们可以灵活地控制拦截器的作用范围。例如,如果我们只想拦截某个特定路径下的请求,可以将拦截路径设置为该特定路径;如果我们想排除某些路径下的请求不被拦截,可以使用excludePathPatterns()方法来指定排除的路径。

拦截路径的设置需要根据具体的业务需求和场景来进行灵活配置。合理设置拦截路径可以提高拦截器的效率,减少不必要的拦截处理,同时也可以保证拦截器只对需要拦截的请求生效,不影响其他请求的正常处理。

在拦截器中,除了可以设置 /** 拦截所有资源外,还有一些常见的拦截路径设置,其含义及举例如下:

  • /*:表示一级路径。能够匹配一级路径下的所有资源,例如 /user、/book、/login,但不能匹配 /user/login。
  • /**:表示任意级路径。能够匹配任意级路径下的所有资源,例如 /user、/user/login、/user/reg。
  • /book/*:表示 /book 下的一级路径。能够匹配 /book 下的一级路径,例如 /book/addBook,但不能匹配 /book/addBook/1 和 /book。
  • /book/**:表示 /book 下的任意级路径。能够匹配 /book 下的任意级路径,例如 /book、/book/addBook、/book/addBook/2,但不能匹配 /user/login。

以上拦截规则可以拦截项目中的所有使用 URL,包括静态文件(如图片文件、JS 和 CSS 文件等)。通过合理设置拦截路径,可以确保拦截器只对需要拦截的请求生效,同时不影响对静态资源的访问。这样可以提高拦截器的效率,使其只拦截需要处理的请求,而不对其他请求产生影响。

拦截器执行流程

正常的调用顺序:

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

  1. 拦截器拦截请求: 在请求到达Controller之前,会先被拦截器拦截住。拦截器会执行preHandle()方法,这个方法需要返回一个布尔类型的值。如果返回true,表示放行本次操作,继续访问Controller中的方法;如果返回false,则不会放行,Controller中的方法也不会执行。
  2. Controller方法执行: 如果拦截器放行了请求,那么就会执行Controller中相应的方法。在Controller方法执行完毕后,会返回处理结果。
  3. 拦截器后处理: Controller方法执行完毕后,拦截器会回过头来执行postHandle()方法,这个方法会在Controller方法执行后、视图渲染前执行。在这个方法中,可以对请求的处理结果进行进一步处理或修改,但不能阻止请求的执行。
  4. 视图渲染及最终响应: 在postHandle()方法执行完毕后,请求的处理结果会被传递给视图进行渲染。渲染完成后,拦截器会执行afterCompletion()方法,这个方法会在视图渲染完成后执行,用于进行一些资源的清理工作。最终,拦截器会将处理结果响应给浏览器。

强制登录校验

学习例如拦截器的基本操作之后,我们就可以完成上次图书管理系统差着的最后⼀步操作了:

通过拦截器来完成图书管理系统中的强制登录校验功能。

定义拦截器

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

package com.example.librarysystem.book.interceptor;

import com.example.librarysystem.book.model.Constants;
import com.example.librarysystem.book.model.UserInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.Nullable;
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.preHandle.... ");
        //true- 放行, false-拦截
        //验证用户是否登录
        HttpSession session = request.getSession();
        UserInfo userInfo = (UserInfo) session.getAttribute(Constants.USER_SESSION_KEY);
        if (userInfo==null || userInfo.getId()<1){
            //http 状态码
            response.setStatus(401);
            return false;
        }
        return true;
    }

}

HTTP状态码401表示"未经过认证",指示身份验证是必需的,但要么没有提供身份验证,要么身份验证失败。如果请求已经包含授权凭据,那么401状态码表示服务器不接受这些凭据。 

注册配置拦截器 

package com.example.librarysystem.book.config;

import com.example.librarysystem.book.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("/**")
                //排除一些路径
                .excludePathPatterns("/user/login")
                .excludePathPatterns("/**/**.html")
                .excludePathPatterns("/css/**")
                .excludePathPatterns("/js/**")
                .excludePathPatterns("/pic/**");
    }
}

也可以写成这种格式:

@Configuration
public class WebConfig 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); // 设置拦截器排除拦截的路径
    }
}

第一种格式中,排除路径使用了多次excludePathPatterns()方法来逐个排除路径,代码比较冗长。而第二种格式中,使用了一个列表excludePaths来存储需要排除的路径,然后直接将整个列表传递给excludePathPatterns()方法,使得代码更加简洁和易读。总的来说,第二种格式更加简洁和易于维护,尤其是在排除路径较多时,可以更清晰地看出哪些路径被排除了。 

又或者这样写:

package com.example.librarysystem.book.config;

import com.example.librarysystem.book.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("/**").excludePathPatterns("/user/login","/**/**.html","/css/**","/js/**","/pic/**");
    }
}

在Java中,"..." 是一个语法糖,被称为可变参数(variable arguments)或者参数数量可变(varargs)。在方法的参数列表中,使用"..." 可以表示该参数可以接受不定数量的参数值。在这种情况下,(String... patterns) 表示该方法可以接受任意数量的字符串参数。

对于这个方法excludePathPatterns(String... patterns),我们可以像下面这样调用它:

excludePathPatterns("pattern1");
excludePathPatterns("pattern1", "pattern2");
excludePathPatterns("pattern1", "pattern2", "pattern3");
// 以此类推,可以传递任意数量的字符串参数

在方法内部,这些参数将被视为一个字符串数组,我们可以像操作普通数组一样对它们进行处理。 

相应的,这部分代码就可以删掉了: 

我现在直接进入列表页: 

按理来说它应该要跳转到登录页面强制登录的……

前端页面正常报错,但是没有跳转,说明问题出在前端代码上。

            function getBookList() {
                $.ajax({
                    type: "get",
                    url: "/book/getListByPage"+location.search,
                    success: function (result) {
                        if(result.code == -2){
                            confirm("用户未登录,请先登录");
                            location.href = "login.html";
                            return;
                        }
                        if(result.code == -1){
                            alert("发生内部错误,请联系管理员");
                            return;
                        }
                        var data = result.data;
                        var books = data.records;
                        console.log(books);
                        var finalHtml = "";
                        for(var book of books){
                            //拼接html
                            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.stateCN+'</td>';   
                            finalHtml +='<td>';   
                            finalHtml +='<div class="op">';   
                            finalHtml +='<a href="book_update.html?bookId='+book.id+'">修改</a>';   
                            finalHtml +='<a href="javascript:void(0)" onclick="deleteBook('+book.id+')">删除</a>';   
                            finalHtml +='</div>';   
                            finalHtml +='</td>';   
                            finalHtml +='</tr>';   
                        }

                        $("tbody").html(finalHtml);
                        //翻页信息
                        $("#pageContainer").jqPaginator({
                            totalCounts: data.count, //总记录数
                            pageSize: 10,    //每页的个数
                            visiblePages: 7, //可视页数
                            currentPage: data.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); 
                    }

                });
            }

我们添加一个ajax执行失败的error函数:

好了,根据报错信息再次修改前端代码:

            function getBookList() {
                $.ajax({
                    type: "get",
                    url: "/book/getListByPage"+location.search,
                    success: function (result) {

                        if(result.code == -1){
                            alert("发生内部错误,请联系管理员");
                            return;
                        }
                        var data = result.data;
                        var books = data.records;
                        console.log(books);
                        var finalHtml = "";
                        for(var book of books){
                            //拼接html
                            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.stateCN+'</td>';   
                            finalHtml +='<td>';   
                            finalHtml +='<div class="op">';   
                            finalHtml +='<a href="book_update.html?bookId='+book.id+'">修改</a>';   
                            finalHtml +='<a href="javascript:void(0)" onclick="deleteBook('+book.id+')">删除</a>';   
                            finalHtml +='</div>';   
                            finalHtml +='</td>';   
                            finalHtml +='</tr>';   
                        }

                        $("tbody").html(finalHtml);
                        //翻页信息
                        $("#pageContainer").jqPaginator({
                            totalCounts: data.count, //总记录数
                            pageSize: 10,    //每页的个数
                            visiblePages: 7, //可视页数
                            currentPage: data.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!=null && error.status==401){
                            location.href = "login.html";
                        }
                    }

                });
            }

成功,强制跳转到登录页面了。

也可以正常登录:

DispatcherServlet 源码分析(了解)

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

  1. 2024-03-03 00:57:10.640 INFO 36444 --- [ main] c.e.l.LibrarySystemApplication : Starting LibrarySystemApplication using Java 1.8.0_192 on LAPTOP-NHPKNI7C with PID 36444 (E:\IDEA\Project\LibrarySystem\target\classes started by ED in E:\IDEA\Project\LibrarySystem)

    • 这是应用程序启动的信息日志,指示应用程序正在启动。
    • 提供了启动的时间戳、日志级别(INFO)、线程ID(36444)和启动类的名称(LibrarySystemApplication)。
    • 提供了Java版本、计算机名称、进程ID以及启动的路径信息。
  2. 2024-03-03 00:57:10.642 INFO 36444 --- [ main] c.e.l.LibrarySystemApplication : No active profile set, falling back to 1 default profile: "default"

    • 这是应用程序的信息日志,指示没有活跃的配置文件,因此正在使用默认配置。
    • 提供了时间戳、日志级别(INFO)、线程ID和相关的消息。
  3. 2024-03-03 00:57:11.193 INFO 36444 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 9090 (http)

    • 这是Tomcat服务器初始化的信息日志,指示Tomcat服务器已经使用9090端口初始化。
    • 提供了时间戳、日志级别(INFO)、线程ID和相关的消息。
  4. 2024-03-03 00:57:11.197 INFO 36444 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]

    • 这是Tomcat服务启动的信息日志,指示Tomcat服务正在启动。
    • 提供了时间戳、日志级别(INFO)、线程ID和相关的消息。
  5. 2024-03-03 00:57:11.197 INFO 36444 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.82]

    • 这是Servlet引擎启动的信息日志,指示Servlet引擎正在启动。
    • 提供了时间戳、日志级别(INFO)、线程ID和相关的消息。
  6. 2024-03-03 00:57:11.273 INFO 36444 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext

    • 这是Spring嵌入式Web应用程序上下文初始化的信息日志,指示Spring应用程序上下文正在初始化。
    • 提供了时间戳、日志级别(INFO)、线程ID和相关的消息。
  7. 2024-03-03 00:57:11.273 INFO 36444 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 606 ms

    • 这是Spring Servlet Web服务器应用程序上下文初始化完成的信息日志。
    • 提供了时间戳、日志级别(INFO)、线程ID和相关的消息,表示根Web应用程序上下文初始化完成,并且耗时606毫秒。
  8. Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.

    • 这是关于日志初始化的信息日志,指示使用org.apache.ibatis.logging.stdout.StdOutImpl适配器来初始化日志。
    • 该消息表示日志系统已经初始化,并指定了用于输出日志的适配器。
  9. Parsed mapper file: 'file [E:\IDEA\Project\LibrarySystem\target\classes\mapper\BookInfoMapper.xml]'

    • 这是关于解析映射器文件的信息日志,指示已经解析了BookInfoMapper.xml文件。
    • 提供了文件路径以及解析的结果。
  10. 2024-03-03 00:57:11.513 INFO 36444 --- [ main] o.s.b.a.w.s.WelcomePageHandlerMapping : Adding welcome page: class path resource [static/index.html]

    • 这是关于添加欢迎页的信息日志,指示已经添加了欢迎页static/index.html。
    • 提供了时间戳、日志级别(INFO)、线程ID和相关的消息。
  11. 2024-03-03 00:57:11.664 INFO 36444 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 9090 (http) with context path ''

    • 这是关于Tomcat服务器启动的信息日志,指示Tomcat服务器已经在9090端口启动,并且上下文路径为空。
    • 提供了时间戳、日志级别(INFO)、线程ID和相关的消息。
  12. 2024-03-03 00:57:11.671 INFO 36444 --- [ main] c.e.l.LibrarySystemApplication : Started LibrarySystemApplication in 1.295 seconds (JVM running for 2.069)

    • 这是关于应用程序启动完成的信息日志,指示应用程序在1.295秒内启动完成,并且JVM已经运行了2.069秒。
    • 提供了时间戳、日志级别(INFO)、线程ID和相关的消息。

现在,我们首先第一次启动程序,访问接口,然后观察日志:

然后清空日志,再次启动,观察日志:

你会发现缺少了一开始的 Initializing Spring ‘dispatcherServlet’……

我们继续观察,先访问第一个接口。

我们以为它已经初始化完毕,但是它又做了一些初始化:

我们再次请求一遍:

不会再显示这段话。

从这里我们可以得出结论,它只执行一次,而且是在服务启动之后、处理请求之前之前执行。

我们的服务为什么知道要走拦截器?怎么知道要走具体的哪个Contrlllor?入口就在这个类——DispatcherServlet。

那么我们现在就来一点点了解DispatcherServlet类。

DispatcherServlet从名字上看就可以知道它是一个Servlet。

Servlet的生命周期一般包括以下三个阶段:

  1. 初始化(Initialization):在Servlet被第一次加载到内存中时调用。在这个阶段,Servlet容器调用Servlet的init()方法,用于执行一些初始化操作,比如加载配置、建立数据库连接等。init()方法只会在Servlet第一次加载时调用,之后的请求不会再触发init()方法。

  2. 服务(Service):在Servlet被初始化之后,它会等待接收来自客户端的请求。当客户端发送请求时,Servlet容器会调用Servlet的service()方法,并将请求传递给该方法进行处理。在service()方法中,Servlet通常会根据请求的类型(GET、POST等)来执行相应的逻辑处理,并生成响应返回给客户端。

  3. 销毁(Destruction):当Servlet容器需要释放Servlet占用的资源或者关闭应用程序时,会调用Servlet的destroy()方法。在这个阶段,Servlet可以执行一些清理工作,比如关闭数据库连接、释放资源等。destroy()方法只会在Servlet被销毁之前被调用一次。

这三个阶段构成了Servlet的生命周期,它们决定了Servlet在应用程序中的行为和状态。

当Tomcat启动之后,核心类DispatcherServlet,它来控制程序的执行顺序:

  1. DispatcherServlet的作用

    • DispatcherServlet是Spring MVC中的一个核心类,用于控制程序的执行顺序。
    • 所有请求都会先经过DispatcherServlet。
  2. 请求处理流程

    • 请求到达DispatcherServlet后,会执行其中的doDispatch方法来进行调度。
    • 如果存在拦截器,会首先执行拦截器中的preHandle()方法。如果preHandle()返回true,则继续访问Controller中的方法;如果返回false,则请求处理流程停止,不再继续执行。
    • Controller中的方法执行完毕后,会回到DispatcherServlet,然后执行拦截器中的postHandle()和afterCompletion()方法。
    • 最终,DispatcherServlet将响应数据返回给浏览器。

初始化(了解)  

DispatcherServlet的初始化方法init()是在其父类HttpServletBean中实现的,它是在Servlet被实例化并且将要开始服务请求之前调用的。它的主要作用是加载web.xml中DispatcherServlet的配置,并调用子类的初始化方法。

web.xml是一个关键的配置文件,通常用于配置Web项目的各种组件,比如Listener、Filter、Servlet等。但从Spring框架3.1版本开始,它开始支持Servlet 3.0,而从3.2版本开始,可以通过配置DispatcherServlet来实现,从而不再需要使用web.xml配置文件。这种方式使得配置更加简洁灵活,也更符合现代化的Java Web开发的趋势。

init() 具体代码如下: 

public final void init() throws ServletException {
    // 创建一个ServletConfigPropertyValues对象,用于处理Servlet配置中的属性值
    PropertyValues pvs = new ServletConfigPropertyValues(this.getServletConfig(), this.requiredProperties);
    // 检查是否有属性需要被设置
    if (!pvs.isEmpty()) {
        try {
            // 创建一个BeanWrapper对象,用于设置Servlet的属性值
            BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
            // 创建一个资源加载器,用于加载资源
            ResourceLoader resourceLoader = new ServletContextResourceLoader(this.getServletContext());
            // 注册Resource类型的自定义编辑器,用于处理资源类型的属性
            bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.getEnvironment()));
            // 初始化BeanWrapper,准备设置属性值
            this.initBeanWrapper(bw);
            // 使用BeanWrapper设置属性值
            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;
        }
    }

    // 调用initServletBean方法,进一步初始化Servlet的Bean
    this.initServletBean();
}

大致流程和思路:

  1. 创建ServletConfigPropertyValues对象,用于处理Servlet配置中的属性值。
  2. 检查是否有需要设置的属性值。
  3. 如果有需要设置的属性值,创建一个BeanWrapper对象,用于设置Servlet的属性。
  4. 创建一个资源加载器,用于加载资源。
  5. 注册Resource类型的自定义编辑器,以便正确处理资源类型的属性。
  6. 初始化BeanWrapper,准备设置属性值。
  7. 使用BeanWrapper设置属性值。
  8. 如果设置属性值时出现异常,记录错误日志并抛出异常。
  9. 调用initServletBean()方法,进一步初始化Servlet的Bean。

所以这样看下来,这个方法的主要目的是根据Servlet的配置,设置Servlet的属性值。通过这个方法,Servlet可以在初始化时对自身进行配置,以便后续的请求处理。并且在这之后调用initServletBean()方法,将进行Servlet的进一步初始化工作。

我们来看看initServletBean()方法:

因为这是在父类,只是简单的定义,具体的实现还要看子类——FrameworkServlet 类。

下面是 initServletBean() 的具

你有没有发现第一句话很眼熟?不就是我们刚刚看过的只执行一次的那段话吗?

大致流程和思路:

  1. 记录初始化日志到ServletContext,表示正在初始化Spring Servlet。
  2. 如果日志级别为INFO,则记录Servlet的初始化信息。
  3. 记录初始化开始时间。
  4. 尝试初始化Web应用上下文。
  5. 尝试初始化FrameworkServlet。
  6. 如果初始化过程中出现异常,记录错误日志并抛出异常。
  7. 如果日志级别为DEBUG,则记录请求参数和请求头是否被日志记录的信息。
  8. 如果日志级别为INFO,则记录初始化完成信息和耗时。

总结一下,initServletBean() 方法的执行步骤如下:

  1. 建立 WebApplicationContext 容器:首先,方法会建立一个 WebApplicationContext 容器,这个容器负责管理 Servlet 的 Bean 以及其他与 Web 应用相关的 Spring 组件。WebApplicationContext 是 ApplicationContext 的子接口,专门用于 Web 应用中,能够感知 Servlet 生命周期,能够让 Servlet 与 Spring 环境整合起来。

  2. 加载 Spring MVC 配置文件中定义的 Bean:接下来,initServletBean() 方法会加载 Spring MVC 配置文件中定义的 Bean 到该 WebApplicationContext 容器中。这些 Bean 可能包括控制器、拦截器、视图解析器等等,它们定义了 Spring MVC 框架的配置信息,通过加载这些 Bean,Spring 能够为 Servlet 提供所需的依赖项和功能。

  3. 将容器添加到 ServletContext 中:最后,initServletBean() 方法会将创建好的 WebApplicationContext 容器添加到 ServletContext 中,这样整个应用程序都能够访问到这个容器,从而实现 Servlet 与 Spring 环境的无缝集成。

在这个过程中,框架会执行各种初始化操作,例如实例化 Servlet,解析并加载配置文件,创建和初始化依赖关系等等,以确保 Servlet 在运行时能够按照预期的方式工作。同时,方法也会记录初始化过程的日志,以便开发人员在需要时查看日志,排查问题或者了解应用程序的启动过程。

总的来说,initServletBean() 方法在 Spring MVC 中扮演着重要的角色,它负责初始化 Servlet 的 Bean 并确保 Spring Servlet 上下文和框架的正确初始化,从而使得 Servlet 能够在 Web 应用中正常运行。

我们把initServletBean() 方法中涉及到的重要方法分析一下:

首先:

protected WebApplicationContext initWebApplicationContext() {
    // 获取ServletContext中的根WebApplicationContext
    WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext());
    WebApplicationContext wac = null;
    // 如果已经有了WebApplicationContext,则直接使用
    if (this.webApplicationContext != null) {
        wac = this.webApplicationContext;
        // 如果webApplicationContext是ConfigurableWebApplicationContext的实例
        if (wac instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)wac;
            // 如果上下文尚未激活,则进行配置和刷新操作
            if (!cwac.isActive()) {
                if (cwac.getParent() == null) {
                    // 设置父上下文为ServletContext中的根WebApplicationContext
                    cwac.setParent(rootContext);
                }
                // 配置并刷新WebApplicationContext
                this.configureAndRefreshWebApplicationContext(cwac);
            }
        }
    }

    // 如果没有已经存在的WebApplicationContext,则尝试从ServletContext中查找
    if (wac == null) {
        wac = this.findWebApplicationContext();
    }

    // 如果仍然没有找到WebApplicationContext,则创建一个新的
    if (wac == null) {
        wac = this.createWebApplicationContext(rootContext);
    }

    // 如果未收到刷新事件,则调用onRefresh方法
    if (!this.refreshEventReceived) {
        synchronized(this.onRefreshMonitor) {
            this.onRefresh(wac);
        }
    }

    // 如果需要发布上下文,则将其存储在ServletContext中
    if (this.publishContext) {
        String attrName = this.getServletContextAttributeName();
        this.getServletContext().setAttribute(attrName, wac);
    }

    return wac;
}

这段代码是用于初始化WebApplicationContext的方法(initWebApplicationContext())。它负责创建或者获取Spring Web应用程序上下文,并进行一系列的初始化和配置工作。

大致流程和思路:

  1. 获取ServletContext中的根WebApplicationContext,如果已经存在则直接使用。
  2. 如果已经存在的WebApplicationContext是ConfigurableWebApplicationContext类型的,并且尚未激活,则进行配置和刷新操作。
  3. 尝试从ServletContext中查找已经存在的WebApplicationContext。
  4. 如果在ServletContext中未找到WebApplicationContext,则尝试创建一个新的。
  5. 如果当前Servlet尚未收到刷新事件,则调用onRefresh方法进行相应处理。
  6. 如果需要发布上下文,则将其存储在ServletContext中。
  7. 返回WebApplicationContext。

这个方法的主要目的是初始化和获取Spring Web应用程序上下文,并根据需要对其进行配置、刷新和存储操作。

里面提到调用onRefresh方法。在初始化web容器的过程中,会通过onRefresh来初始化SpringMVC的容器:

它在FrameworkServlet 类没有实现,我们去子类看看: 

又是一个初始化!但是这个初始化非常非常重要!我们的Spring之所以能够运行,全靠这里:
它是用于初始化Spring MVC中的各种策略(strategies)的方法(initStrategies())。它调用了一系列的初始化方法来配置和准备Spring MVC框架中的关键组件。

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);
    // 初始化FlashMap管理器
    this.initFlashMapManager(context);
}

大致流程和思路:

  1. 调用initMultipartResolver方法,初始化处理文件上传的解析器。
  2. 调用initLocaleResolver方法,初始化区域解析器,用于处理请求的语言区域。
  3. 调用initThemeResolver方法,初始化主题解析器,用于处理请求的主题。
  4. 调用initHandlerMappings方法,初始化处理器映射器,用于确定请求应该由哪个处理器处理。
  5. 调用initHandlerAdapters方法,初始化处理器适配器,用于执行请求处理器。
  6. 调用initHandlerExceptionResolvers方法,初始化处理器异常解析器,用于处理请求处理器抛出的异常。
  7. 调用initRequestToViewNameTranslator方法,初始化请求到视图名称的转换器,用于将请求映射到视图名称。
  8. 调用initViewResolvers方法,初始化视图解析器,用于将视图名称解析为实际的视图对象。
  9. 调用initFlashMapManager方法,初始化FlashMap管理器,用于管理FlashMap,用于在重定向之间传递数据。

这个方法的主要目的是初始化Spring MVC框架中的各种策略,以便后续的请求处理能够顺利进行。每个初始化方法都负责配置和准备一个特定的组件,以确保Spring MVC框架的正常运行。

我们这里只是简单介绍一下,一会儿碰到了会仔细讲解。

方法initMultipartResolver、initLocaleResolver、initThemeResolver、initRequestToViewNameTranslator、initFlashMapManager的处理方式几乎都⼀样(1.2.3.7.8,9),

这些方法都是通过尝试从应用上下文中获取特定名称的 Bean,如果找不到相应的 Bean,则使用默认的策略。如果找到了 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。

// 初始化文件上传解析器 MultipartResolver
private void initMultipartResolver(ApplicationContext context) {
    try {
        // 尝试从应用上下文中获取名为 "multipartResolver" 的 MultipartResolver 类型的 Bean
        this.multipartResolver = (MultipartResolver)context.getBean("multipartResolver", MultipartResolver.class);
        // 如果日志级别是 TRACE,则记录检测到的 MultipartResolver
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("Detected " + this.multipartResolver);
        } 
        // 如果日志级别是 DEBUG,则记录检测到的 MultipartResolver 类名
        else if (this.logger.isDebugEnabled()) {
            this.logger.debug("Detected " + this.multipartResolver.getClass().getSimpleName());
        }
    } 
    // 捕获 NoSuchBeanDefinitionException 异常,表示应用上下文中没有名为 "multipartResolver" 的 Bean
    catch (NoSuchBeanDefinitionException var3) {
        // 将 multipartResolver 设置为 null
        this.multipartResolver = null;
        // 如果日志级别是 TRACE,则记录未找到 MultipartResolver 的信息
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("No MultipartResolver 'multipartResolver' declared");
        }
    }
}

// 初始化区域解析器 LocaleResolver
private void initLocaleResolver(ApplicationContext context) {
    try {
        // 尝试从应用上下文中获取名为 "localeResolver" 的 LocaleResolver 类型的 Bean
        this.localeResolver = (LocaleResolver)context.getBean("localeResolver", LocaleResolver.class);
        // 如果日志级别是 TRACE,则记录检测到的 LocaleResolver
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("Detected " + this.localeResolver);
        } 
        // 如果日志级别是 DEBUG,则记录检测到的 LocaleResolver 类名
        else if (this.logger.isDebugEnabled()) {
            this.logger.debug("Detected " + this.localeResolver.getClass().getSimpleName());
        }
    } 
    // 捕获 NoSuchBeanDefinitionException 异常,表示应用上下文中没有名为 "localeResolver" 的 Bean
    catch (NoSuchBeanDefinitionException var3) {
        // 将 localeResolver 设置为使用默认策略获取的 LocaleResolver
        this.localeResolver = (LocaleResolver)this.getDefaultStrategy(context, LocaleResolver.class);
        // 如果日志级别是 TRACE,则记录未找到 LocaleResolver 的信息
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("No LocaleResolver 'localeResolver': using default [" + this.localeResolver.getClass().getSimpleName() + "]");
        }
    }
}

// 初始化主题解析器 ThemeResolver
private void initThemeResolver(ApplicationContext context) {
    try {
        // 尝试从应用上下文中获取名为 "themeResolver" 的 ThemeResolver 类型的 Bean
        this.themeResolver = (ThemeResolver)context.getBean("themeResolver", ThemeResolver.class);
        // 如果日志级别是 TRACE,则记录检测到的 ThemeResolver
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("Detected " + this.themeResolver);
        } 
        // 如果日志级别是 DEBUG,则记录检测到的 ThemeResolver 类名
        else if (this.logger.isDebugEnabled()) {
            this.logger.debug("Detected " + this.themeResolver.getClass().getSimpleName());
        }
    } 
    // 捕获 NoSuchBeanDefinitionException 异常,表示应用上下文中没有名为 "themeResolver" 的 Bean
    catch (NoSuchBeanDefinitionException var3) {
        // 将 themeResolver 设置为使用默认策略获取的 ThemeResolver
        this.themeResolver = (ThemeResolver)this.getDefaultStrategy(context, ThemeResolver.class);
        // 如果日志级别是 TRACE,则记录未找到 ThemeResolver 的信息
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("No ThemeResolver 'themeResolver': using default [" + this.themeResolver.getClass().getSimpleName() + "]");
        }
    }
}

// 初始化处理器映射器 HandlerMappings
private void initHandlerMappings(ApplicationContext context) {
    // 将 handlerMappings 初始化为 null
    this.handlerMappings = null;
    // 如果需要检测所有的处理器映射器
    if (this.detectAllHandlerMappings) {
        // 从 ApplicationContext 中获取所有的 HandlerMapping 类型的 Bean,并排序
        Map<String, HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
        if (!matchingBeans.isEmpty()) {
            this.handlerMappings = new ArrayList(matchingBeans.values());
            AnnotationAwareOrderComparator.sort(this.handlerMappings);
        }
    } else {
        try {
            // 尝试从 ApplicationContext 中获取名为 "handlerMapping" 的 HandlerMapping 类型的 Bean
            HandlerMapping hm = (HandlerMapping)context.getBean("handlerMapping", HandlerMapping.class);
            // 将 handlerMappings 初始化为包含单个 HandlerMapping 的列表
            this.handlerMappings = Collections.singletonList(hm);
        } catch (NoSuchBeanDefinitionException var4) {
        }
    }

    // 如果 handlerMappings 为 null
    if (this.handlerMappings == null) {
        // 使用默认策略获取处理器映射器,并记录日志
        this.handlerMappings = this.getDefaultStrategies(context, HandlerMapping.class);
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("No HandlerMappings declared for servlet '" + this.getServletName() + "': using default strategies from DispatcherServlet.properties");
        }
    }

    // 遍历处理器映射器列表,如果有映射器使用了路径模式,则将 parseRequestPath 标记为 true
    Iterator var6 = this.handlerMappings.iterator();
    while(var6.hasNext()) {
        HandlerMapping mapping = (HandlerMapping)var6.next();
        if (mapping.usesPathPatterns()) {
            this.parseRequestPath = true;
            break;
        }
    }
}

// 初始化处理器适配器 HandlerAdapters
private void initHandlerAdapters(ApplicationContext context) {
    // 将 handlerAdapters 初始化为 null
    this.handlerAdapters = null;
    // 如果需要检测所有的处理器适配器
    if (this.detectAllHandlerAdapters) {
        // 从 ApplicationContext 中获取所有的 HandlerAdapter 类型的 Bean,并排序
        Map<String, HandlerAdapter> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);
        if (!matchingBeans.isEmpty()) {
            this.handlerAdapters = new ArrayList(matchingBeans.values());
            AnnotationAwareOrderComparator.sort(this.handlerAdapters);
        }
    } else {
        try {
            // 尝试从 ApplicationContext 中获取名为 "handlerAdapter" 的 HandlerAdapter 类型的 Bean
            HandlerAdapter ha = (HandlerAdapter)context.getBean("handlerAdapter", HandlerAdapter.class);
            // 将 handlerAdapters 初始化为包含单个 HandlerAdapter 的列表
            this.handlerAdapters = Collections.singletonList(ha);
        } catch (NoSuchBeanDefinitionException var3) {
        }
    }

    // 如果 handlerAdapters 为 null
    if (this.handlerAdapters == null) {
        // 使用默认策略获取处理器适配器,并记录日志
        this.handlerAdapters = this.getDefaultStrategies(context, HandlerAdapter.class);
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("No HandlerAdapters declared for servlet '" + this.getServletName() + "': using default strategies from DispatcherServlet.properties");
        }
    }
}

// 初始化异常处理器解析器 HandlerExceptionResolvers
private void initHandlerExceptionResolvers(ApplicationContext context) {
    // 将 handlerExceptionResolvers 初始化为 null
    this.handlerExceptionResolvers = null;
    // 如果需要检测所有的异常处理器解析器
    if (this.detectAllHandlerExceptionResolvers) {
        // 从 ApplicationContext 中获取所有的 HandlerExceptionResolver 类型的 Bean,并排序
        Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
        if (!matchingBeans.isEmpty()) {
            this.handlerExceptionResolvers = new ArrayList(matchingBeans.values());
            AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
        }
    } else {
        try {
            // 尝试从 ApplicationContext 中获取名为 "handlerExceptionResolver" 的 HandlerExceptionResolver 类型的 Bean
            HandlerExceptionResolver her = (HandlerExceptionResolver)context.getBean("handlerExceptionResolver", HandlerExceptionResolver.class);
            // 将 handlerExceptionResolvers 初始化为包含单个 HandlerExceptionResolver 的列表
            this.handlerExceptionResolvers = Collections.singletonList(her);
        } catch (NoSuchBeanDefinitionException var3) {
        }
    }

    // 如果 handlerExceptionResolvers 为 null
    if (this.handlerExceptionResolvers == null) {
        // 使用默认策略获取异常处理器解析器,并记录日志
        this.handlerExceptionResolvers = this.getDefaultStrategies(context, HandlerExceptionResolver.class);
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("No HandlerExceptionResolvers declared in servlet '" + this.getServletName() + "': using default strategies from DispatcherServlet.properties");
        }
    }
}

// 初始化请求到视图名称转换器 RequestToViewNameTranslator
private void initRequestToViewNameTranslator(ApplicationContext context) {
    try {
        // 尝试从应用上下文中获取名为 "viewNameTranslator" 的 RequestToViewNameTranslator 类型的 Bean
        this.viewNameTranslator = (RequestToViewNameTranslator)context.getBean("viewNameTranslator", RequestToViewNameTranslator.class);
        // 如果日志级别是 TRACE,则记录检测到的 RequestToViewNameTranslator 类名
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("Detected " + this.viewNameTranslator.getClass().getSimpleName());
        } 
        // 如果日志级别是 DEBUG,则记录检测到的 RequestToViewNameTranslator
        else if (this.logger.isDebugEnabled()) {
            this.logger.debug("Detected " + this.viewNameTranslator);
        }
    } 
    // 捕获 NoSuchBeanDefinitionException 异常,表示应用上下文中没有名为 "viewNameTranslator" 的 Bean
    catch (NoSuchBeanDefinitionException var3) {
        // 将 viewNameTranslator 设置为使用默认策略获取的 RequestToViewNameTranslator
        this.viewNameTranslator = (RequestToViewNameTranslator)this.getDefaultStrategy(context, RequestToViewNameTranslator.class);
        // 如果日志级别是 TRACE,则记录未找到 RequestToViewNameTranslator 的信息
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("No RequestToViewNameTranslator 'viewNameTranslator': using default [" + this.viewNameTranslator.getClass().getSimpleName() + "]");
        }
    }
}

// 初始化视图解析器 ViewResolvers
private void initViewResolvers(ApplicationContext context) {
    this.viewResolvers = null;
    // 如果需要检测所有的视图解析器
    if (this.detectAllViewResolvers) {
        // 从应用上下文中获取所有的 ViewResolver 类型的 Bean
        Map<String, ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false);
        // 如果找到了视图解析器
        if (!matchingBeans.isEmpty()) {
            // 将找到的视图解析器放入视图解析器列表中,并进行排序
            this.viewResolvers = new ArrayList(matchingBeans.values());
            AnnotationAwareOrderComparator.sort(this.viewResolvers);
        }
    } else {
        try {
            // 尝试从应用上下文中获取名为 "viewResolver" 的 ViewResolver 类型的 Bean
            ViewResolver vr = (ViewResolver)context.getBean("viewResolver", ViewResolver.class);
            // 将找到的视图解析器添加到视图解析器列表中
            this.viewResolvers = Collections.singletonList(vr);
        } catch (NoSuchBeanDefinitionException var3) {
        }
    }

    // 如果视图解析器列表为空
    if (this.viewResolvers == null) {
        // 使用默认策略获取视图解析器,并将其记录到日志中
        this.viewResolvers = this.getDefaultStrategies(context, ViewResolver.class);
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("No ViewResolvers declared for servlet '" + this.getServletName() + "': using default strategies from DispatcherServlet.properties");
        }
    }
}

// 初始化 FlashMapManager
private void initFlashMapManager(ApplicationContext context) {
    try {
        // 尝试从应用上下文中获取名为 "flashMapManager" 的 FlashMapManager 类型的 Bean
        this.flashMapManager = (FlashMapManager)context.getBean("flashMapManager", FlashMapManager.class);
        // 如果日志级别是 TRACE,则记录检测到的 FlashMapManager 类名
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("Detected " + this.flashMapManager.getClass().getSimpleName());
        } 
        // 如果日志级别是 DEBUG,则记录检测到的 FlashMapManager
        else if (this.logger.isDebugEnabled()) {
            this.logger.debug("Detected " + this.flashMapManager);
        }
    } 
    // 捕获 NoSuchBeanDefinitionException 异常,表示应用上下文中没有名为 "flashMapManager" 的 Bean
    catch (NoSuchBeanDefinitionException var3) {
        // 将 flashMapManager 设置为使用默认策略获取的 FlashMapManager
        this.flashMapManager = (FlashMapManager)this.getDefaultStrategy(context, FlashMapManager.class);
        // 如果日志级别是 TRACE,则记录未找到 FlashMapManager 的信息
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("No FlashMapManager 'flashMapManager': using default [" + this.flashMapManager.getClass().getSimpleName() + "]");
        }
    }
}


处理请求(核心)

init()结束之后就到了调用service()的时候,但是DispatcherServlet的service方法藏得很深。

FrameWorkServlet里面:

DispatcherServlet 中具体实现:

protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
    // 记录请求日志
    this.logRequest(request);
    // 如果是内部包含请求,则创建一个包含请求的属性快照
    Map<String, Object> attributesSnapshot = null;
    if (WebUtils.isIncludeRequest(request)) {
        attributesSnapshot = new HashMap();
        Enumeration<?> attrNames = request.getAttributeNames();
        // 遍历请求属性
        label116: while (true) {
            String attrName;
            do {
                if (!attrNames.hasMoreElements()) {
                    break label116;
                }
                attrName = (String) attrNames.nextElement();
            } while (!this.cleanupAfterInclude && !attrName.startsWith("org.springframework.web.servlet"));

            attributesSnapshot.put(attrName, request.getAttribute(attrName));
        }
    }

    // 设置请求的一些重要属性
    request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.getWebApplicationContext());
    request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
    request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
    request.setAttribute(THEME_SOURCE_ATTRIBUTE, this.getThemeSource());
    // 如果有FlashMapManager,则处理FlashMap
    if (this.flashMapManager != null) {
        FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
        if (inputFlashMap != null) {
            request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
        }
        request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
        request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
    }

    // 如果需要解析请求路径,则进行解析并缓存
    RequestPath previousRequestPath = null;
    if (this.parseRequestPath) {
        previousRequestPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE);
        ServletRequestPathUtils.parseAndCache(request);
    }

    try {
        // 执行请求的分派
        this.doDispatch(request, response);
    } finally {
        // 在处理完成后,恢复请求的属性快照
        if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted() && attributesSnapshot != null) {
            this.restoreAttributesAfterInclude(request, attributesSnapshot);
        }
        // 如果需要解析请求路径,则设置已解析的请求路径
        if (this.parseRequestPath) {
            ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request);
        }
    }
}

大致流程和思路:

  1. 记录请求的日志。
  2. 如果是内部包含请求,则创建一个包含请求属性的快照,以便稍后恢复请求属性。
  3. 设置请求的一些重要属性,如WebApplicationContext、LocaleResolver、ThemeResolver等。
  4. 如果存在FlashMapManager,则处理FlashMap,用于在请求间传递数据。
  5. 如果需要解析请求路径,则进行解析并缓存。
  6. 执行请求的分派,即将请求分派给相应的处理器进行处理。
  7. 在处理完成后,恢复请求的属性快照,如果需要解析请求路径,则设置已解析的请求路径。

这里面有很重要的一部分——执行请求的分派,我们也称为调度。

在Web开发中,调度(Dispatch)是指根据客户端请求的特征(通常是URL或URL中的路径)将请求传递到相应的处理器(例如Controller)的过程。这个过程通常由Web服务器(例如Tomcat)和Web应用程序框架(例如Spring MVC)协同完成。

当客户端发送请求时,服务器会根据请求的URL以及其他相关的信息来确定应该将请求交给哪个处理器来处理。在Spring MVC中,这个过程就是请求的分派(Dispatch)。在DispatcherServlet的上下文中,调度器会根据配置和规则,将请求分发给合适的Controller来处理。

所以毫无疑问,里面的 doDispatch 方法是一个极其重要的方法,它负责处理请求的分派过程,包括获取处理器、处理器适配、拦截器的应用、视图解析等。

我们来看 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 {
                // 检查请求是否为multipart类型
                processedRequest = this.checkMultipart(request);
                multipartRequestParsed = processedRequest != request;
                // 获取请求对应的处理器
                mappedHandler = this.getHandler(processedRequest);
                if (mappedHandler == null) {
                    // 如果未找到对应的处理器,则处理请求
                    this.noHandlerFound(processedRequest, response);
                    return;
                }

                // 获取处理器适配器
                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;
                    }
                }

                // 应用处理器前置处理
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }

                // 处理请求
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                // 如果是异步处理,直接返回
                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }

                // 应用默认视图名称
                this.applyDefaultViewName(processedRequest, mv);
                // 应用处理器后置处理
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            } catch (Exception var20) {
                dispatchException = var20;
            } catch (Throwable var21) {
                dispatchException = new NestedServletException("Handler dispatch failed", var21);
            }

            // 处理请求分派结果
            this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception) dispatchException);
        } catch (Exception var22) {
            this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
        } catch (Throwable var23) {
            this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
        }

    } finally {
        // 如果是并发处理,应用并发处理后置处理;否则,清理multipart请求
        if (asyncManager.isConcurrentHandlingStarted()) {
            if (mappedHandler != null) {
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
            }
        } else if (multipartRequestParsed) {
            this.cleanupMultipart(processedRequest);
        }
    }
}

适配器模式(Adapter Pattern)是一种结构型设计模式,用于解决两个不兼容接口之间的兼容性问题。它通过引入一个适配器来使得原本由于接口不兼容而不能一起工作的类能够协同工作。

适配器模式的核心思想是通过一个中间类(适配器)来包装原有的类,将原有类的接口转换成目标接口,从而让原有类能够被客户端调用,达到兼容不同接口的目的。适配器模式主要包含对象适配器模式和类适配器模式两种形式。

在 Spring MVC 中,HandlerAdapter 就是一个适配器,它的主要作用是支持不同类型的处理器(如 Controller、HttpRequestHandler 或者 Servlet 等),让它们能够适配统一的请求处理流程。这样,Spring MVC 可以通过一个统一的接口来处理来自各种处理器的请求。

具体来说,HandlerAdapter 接口定义了统一的处理器适配器接口,其实现类负责将特定类型的处理器(如 Controller)适配到 Spring MVC 的处理流程中。通过适配器模式,Spring MVC 可以更加灵活地支持不同类型的处理器,而不需要修改核心的处理流程代码。

总之,适配器模式在 Spring MVC 中的应用使得不同类型的处理器能够与统一的请求处理流程进行适配,提高了框架的灵活性和扩展性,使得开发人员可以更加方便地集成和使用各种类型的处理器。

大致流程和思路:

  1. 初始化变量,准备处理请求过程中需要使用的一些对象。
  2. 尝试执行请求分派过程,包括获取处理器、处理器适配、拦截器的应用、处理请求等。
  3. 处理请求分派过程中可能出现的异常,包括处理器执行异常和处理过程中的其他异常。
  4. 在处理完成后,处理可能存在的并发处理和multipart请求。
  5. 处理完毕,结束请求分派过程。

这个方法的主要目的是执行请求分派过程,其中涉及到获取处理器、处理器适配、拦截器的应用、处理请求、处理结果等一系列操作,最终返回响应结果。

从上述源码可以看出在开始执行 Controller 之前,会先调用预处理方法applyPreHandle,它用于应用处理器前置拦截器的方法(applyPreHandle(HttpServletRequest request, HttpServletResponse response))。它会遍历拦截器列表,并依次调用每个拦截器的preHandle方法来进行前置拦截器的处理。

applyPreHandle 方法的实现源码如下:

boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
    // 遍历拦截器列表
    for (int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
        // 获取当前拦截器
        HandlerInterceptor interceptor = (HandlerInterceptor) this.interceptorList.get(i);
        // 调用当前拦截器的preHandle方法进行前置拦截器处理
        if (!interceptor.preHandle(request, response, this.handler)) {
            // 如果preHandle返回false,则触发afterCompletion方法,并返回false
            this.triggerAfterCompletion(request, response, (Exception) null);
            return false;
        }
    }
    // 如果所有拦截器的preHandle方法均返回true,则返回true
    return true;
}

大致流程和思路:

  1. 遍历拦截器列表,逐个获取拦截器对象。
  2. 调用当前拦截器的preHandle方法进行前置拦截器处理。
  3. 如果有任何一个拦截器的preHandle方法返回false,则触发afterCompletion方法,并返回false。
  4. 如果所有拦截器的preHandle方法均返回true,则返回true,表示前置拦截器处理通过。

这个方法的主要目的是应用处理器的前置拦截器,即在处理请求之前对请求进行一些预处理操作。它会依次调用每个拦截器的preHandle方法,并根据返回值决定是否继续处理请求。

其实你会发现这个方法applyPreHandle和拦截器的三大方法中的第一个preHandle名字很类似,而且你顺着代码继续看,就会发现在 doDispatch 方法中,后续又调用了 applyPostHandle方法和triggerAfterCompletion,名字也是和拦截器的方法一一对应的。

void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) throws Exception {
    // 从拦截器列表的末尾开始遍历
    for (int i = this.interceptorList.size() - 1; i >= 0; --i) {
        // 获取当前拦截器
        HandlerInterceptor interceptor = (HandlerInterceptor) this.interceptorList.get(i);
        // 调用当前拦截器的postHandle方法进行后置拦截器处理
        interceptor.postHandle(request, response, this.handler, mv);
    }
}

大致流程和思路:

  1. 从拦截器列表的末尾开始遍历拦截器列表。
  2. 获取当前拦截器,并调用其postHandle方法进行后置拦截器处理。
  3. 对于每个拦截器,都会调用其postHandle方法,无论前面的拦截器是否返回了false。

这个方法的主要目的是应用处理器的后置拦截器,即在处理请求之后对响应进行一些处理。它会依次调用每个拦截器的postHandle方法。

private void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, Exception ex) throws Exception {
    // 如果存在处理器执行链,则触发处理器执行链的afterCompletion方法
    if (mappedHandler != null) {
        mappedHandler.triggerAfterCompletion(request, response, ex);
    }
    // 将异常继续抛出
    throw ex;
}

大致流程和思路:

  1. 检查是否存在处理器执行链(HandlerExecutionChain)。
  2. 如果存在处理器执行链,则调用其triggerAfterCompletion方法,触发拦截器的afterCompletion方法,进行清理工作。
  3. 将异常继续抛出,以便在上层调用栈中处理。

这个方法的主要目的是在请求处理完成后触发清理工作,并将处理过程中出现的异常继续抛出,以便上层代码能够捕获并处理异常。

适配器模式

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

适配器模式是一种结构型设计模式,也称为包装器模式。它的主要目的是将一个类的接口转换成客户端期望的另一个接口,从而使原本接口不兼容的类能够合作无间。简而言之,适配器模式通过引入一个新的类(适配器),将原本不兼容的接口包装起来,使得客户端能够正常调用目标类。

比如下面两个接口,本身是不兼容的:

// 第一个接口
public interface TargetInterface {
    void targetMethod();
}

// 第二个接口
public interface AdapteeInterface {
    void adapteeMethod();
}

这两个接口在参数类型、方法名、方法数量等方面都不一样,导致它们之间不能直接合作。 

为了使 TargetInterface 和 AdapteeInterface 兼容,我们可以创建一个适配器类,让该适配器类实现 TargetInterface 接口,并在内部调用 AdapteeInterface 的方法。这样,就可以通过适配器间接地调用 AdapteeInterface 的方法,从而实现了接口的兼容性。 

// TargetInterface 接口
public interface TargetInterface {
    void targetMethod();
}

// AdapteeInterface 接口
public interface AdapteeInterface {
    void adapteeMethod();
}

// 适配器类,实现 TargetInterface 接口
public class Adapter implements TargetInterface {
    // 持有一个 AdapteeInterface 的引用
    private AdapteeInterface adaptee;

    // 构造方法,接受一个 AdapteeInterface 对象作为参数
    public Adapter(AdapteeInterface adaptee) {
        this.adaptee = adaptee;
    }

    // 实现 TargetInterface 接口的方法,内部调用 AdapteeInterface 的方法
    @Override
    public void targetMethod() {
        // 在此方法中调用 AdapteeInterface 的 adapteeMethod 方法
        adaptee.adapteeMethod();
    }
}

// 具体的 Adaptee 类,实现 AdapteeInterface 接口
public class Adaptee implements AdapteeInterface {
    // 实现 AdapteeInterface 接口的方法
    @Override
    public void adapteeMethod() {
        System.out.println("Adaptee's adapteeMethod is called");
    }
}

// 测试类
public class Main {
    public static void main(String[] args) {
        // 创建 Adaptee 对象
        Adaptee adaptee = new Adaptee();
        // 创建 Adapter 对象,将 Adaptee 对象传入适配器中
        TargetInterface adapter = new Adapter(adaptee);
        // 调用 TargetInterface 中的方法,实际上内部会调用 AdapteeInterface 的方法
        adapter.targetMethod();
    }
}

通过这样的适配器模式,我们可以使 TargetInterface 和 AdapteeInterface 两个接口兼容,实现了类之间的互操作。 

适配器模式的应用场景包括:

  1. 当需要使用一个已经存在的类,但是其接口与需求不匹配时,可以使用适配器模式进行适配。
  2. 当需要创建一个可以复用的类,该类与其他不相关或不可预见的类进行协作时,可以使用适配器模式来将其接口转换成客户端期望的接口。

在 Spring MVC 中,HandlerAdapter 就是一个适配器,它将不同类型的处理器(如 Controller、HttpRequestHandler 或者 Servlet 等)适配到统一的请求处理流程中。这样,不同类型的处理器就能够与 Spring MVC 的处理流程无缝协作,而不需要修改核心的处理流程代码。

在适配器模式中,有以下几个角色:

  1. Target(目标): 目标接口或抽象类,是客户端希望直接使用的接口。客户端通过目标接口与适配器进行交互。

  2. Adaptee(适配者): 适配者是一个已经存在的、但与目标接口不兼容的类。它需要被适配以便与客户端进行交互。

  3. Adapter(适配器): 适配器是适配器模式的核心。适配器类通过继承或引用适配者的对象,将适配者转换为目标接口,使得适配者能够被客户端使用。

  4. Client(客户端): 客户端是使用适配器的对象。客户端通过目标接口与适配器交互,从而间接地与适配者进行交互。

在这些角色中,适配器起到了连接客户端和适配者的桥梁作用,使得客户端能够使用适配者的功能,而不需要修改客户端的代码。适配器模式能够有效地将不兼容的接口转换为兼容的接口,提高了代码的复用性和灵活性。

适配器模式的实现

场景:前面我们学习的Slf4j 就使用了适配器模式,slf4j提供了⼀系列打印日志的API,底层调用的是log4j 或者 logback来打印日志,我们作为调用者,只需要调用slf4j的API就行了。

我们就以Slf4j为例来学习一下适配器:

package com.example.librarysystem.book.adapt;

public class Log4j {
    public void log(String message) {
        System.out.println("Log4j打印日志, message:" + message);
    }
}
package com.example.librarysystem.book.adapt;

public class logBack {
    public void info(String message) {
        System.out.println("LogBack打印日志, message:" + message);
    }
}
package com.example.librarysystem.book.adapt;


public interface Slf4jApi {
    void log(String message);
}
package com.example.librarysystem.book.adapt;

public class Slf4jLog4jAdapt implements Slf4jApi {
    private Log4j log4j;

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

    @Override
    public void log(String message) {
        log4j.log("Slf4j适配log4j, 日志打印: " + message);
    }
}
package com.example.librarysystem.book.adapt;

public class Slf4jLogBackAdapt implements Slf4jApi {
    private LogBack logBack;

    public Slf4jLogBackAdapt(LogBack logBack) {
        this.logBack = logBack;
    }

    @Override
    public void log(String message) {
        logBack.info("Slf4jLogBackAdapt打印日志:" + message);
    }
}

上述代码用于将不同的日志框架(如 Log4j 和 LogBack)适配为统一的 Slf4jApi 接口,从而使得客户端可以统一调用 Slf4jApi 接口来打印日志。

逐个分析每个类的作用:

  1. Log4j 类: 这个类代表了 Log4j 日志框架,其中有一个 log 方法用于打印日志。

  2. LogBack 类: 这个类代表了 LogBack 日志框架,其中有一个 info 方法用于打印日志。

  3. Slf4jApi 接口: 这个接口定义了一个 log 方法,用于打印日志。这是客户端期望使用的统一接口。

  4. Slf4jLog4jAdapt 类: 这个类是一个适配器,实现了 Slf4jApi 接口,并在内部持有一个 Log4j 对象。通过调用 Log4j 的 log 方法来实现 Slf4jApi 接口的 log 方法。

  5. Slf4jLogBackAdapt 类: 这个类也是一个适配器,同样实现了 Slf4jApi 接口,但是内部持有的是一个 LogBack 对象。通过调用 LogBack 的 info 方法来实现 Slf4jApi 接口的 log 方法。

通过这些适配器类,我们可以将 Log4j 和 LogBack 两个不同的日志框架适配为 Slf4jApi 接口,从而实现了日志框架之间的兼容性,使得客户端可以使用统一的接口来进行日志打印,而无需关心具体使用的是哪个日志框架。

package com.example.librarysystem.book.adapt;

public class Main {
    public static void main(String[] args) {
        Slf4jApi slf4jApi = new Slf4jLog4jAdapt(new Log4j());
        slf4jApi.log("打印日志");

        Slf4jApi slf4jApi2 = new Slf4jLogBackAdapt(new LogBack());
        slf4jApi2.log("打印日志"); // 注意这里是 slf4jApi2

    }
}

补充:

LF4J 本身只提供了日志接口,而具体的实现是由不同的桥接器(如 slf4j-log4j12、slf4j-logback 等)来实现的。这些桥接器充当了适配器的角色,将 SLF4J 的接口适配到不同的日志框架(例如 Log4j、Logback 等)上。

这种设计使得用户可以在不更改代码的情况下,轻松地切换日志实现,只需要替换相应的桥接器即可。这符合适配器模式的思想,提高了代码的灵活性和可维护性。

适配器模式应用场景

你有没有想过,我们为啥一定要用适配器模式?它也不简便啊!为啥我们不让两个类直接实现Slf4jAPI接口呢?

因为早定义过的改不了了。

适配器模式在软件开发中通常被视为一种“补偿模式”,用于弥补设计上的缺陷或解决接口不兼容的问题。

其应用场景主要包括以下几个方面:

  1. 接口不兼容的问题: 当不同系统之间存在接口不兼容的情况时,适配器模式可以提供一个解决方案。例如,两个系统使用了不同的接口规范或协议,但需要进行交互通信,此时可以使用适配器模式将两者之间的接口进行适配。

  2. 设计初期未考虑到的问题: 在软件设计初期,可能无法完全预见所有的需求和变化。如果在设计初期就能协调规避接口不兼容的问题,那么可能不需要使用适配器模式。但是当出现设计上的缺陷或未预料到的情况时,适配器模式可以帮助我们在不修改已有代码的情况下进行补救。

  3. 对现有代码的改造和扩展: 适配器模式常用于对现有代码进行改造或扩展,以实现新的功能或满足新的需求。通过引入适配器,我们可以在不影响现有代码结构的情况下,为系统添加新的特性或功能。

  4. 版本升级等需求: 当系统需要进行版本升级或与新的技术进行集成时,可能需要使用适配器模式。适配器可以帮助系统与新技术或新版本进行适配,以确保系统的平稳升级和运行。

总的来说,适配器模式适用于需要在运行时对现有代码进行改造或补救设计缺陷的情况。虽然它常被视为一种“无奈之举”,但在软件开发中,适配器模式是一种常见且有效的设计模式,能够帮助我们应对复杂的接口和系统间的兼容性问题。

统一数据返回格式

回顾:强制登录案例中,我们共做了两部分工作:

1、通过Session来判断用户是否登录 ;

2、对后端返回数据进行封装,告知前端处理的结果。

后端统一返回结果: 

后端逻辑处理:

拦截器已经帮我们实现了第一个功能, 接下来看SpringBoot对第二个功能如何支持。

快速入门

统⼀的数据返回格式需要到 @ControllerAdvice 和 ResponseBodyAdvice。@ControllerAdvice 表示控制器通知类。响应体增强器(ResponseBodyAdvice),它允许我们在将响应体写回客户端之前修改响应体的内容。具体来说,它的作用是在每次控制器方法执行完毕并返回响应体之前,将响应体包装成一个统一的格式,通常用于统一格式化 API 响应。

首先,我们添加类 ResponseAdvice , 实现 ResponseBodyAdvice 接口,并在类上添加 @ControllerAdvice 注解。

package com.example.librarysystem.book.config;
import com.example.librarysystem.book.model.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

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

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        //对哪些请求进行处理  true-进行处理, false-不进行处理
        return true;
    }
    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        return Result.success(body);
    }
}

在我们的代码中,它的作用是将响应体包装成一个名为 Result 的类的实例,并调用 Result.success(body) 方法将原始的响应体作为成功响应的数据部分返回。这样做的好处是可以统一接口的响应格式,使得前端处理响应更加方便和统一。

当我们在一个应用中有多个控制器方法返回响应给客户端时,通常希望这些响应保持一致的格式,以便前端或其他客户端可以更轻松地处理它们。然而,如果我们不使用统一的方式来处理响应体,那么每个控制器方法可能都会返回不同的格式,这会增加前端处理响应的复杂性。

为了解决这个问题,我们就可以使用 Spring MVC 中的 ResponseBodyAdvice 接口。这个接口允许我们在将响应体返回给客户端之前对其进行修改。通过实现 ResponseBodyAdvice 接口,我们就可以定义一个全局的增强器,该增强器将应用于每个控制器方法的响应体。

我们实现了一个名为 ResponseAdvice 的 ResponseBodyAdvice 类。在这个类中,我们重写了 beforeBodyWrite 方法,该方法在每个控制器方法返回响应体之前被调用。在这个方法中,可以对原始的响应体进行处理,并将其包装成统一的格式。

我们还将原始的响应体作为参数传递给 beforeBodyWrite 方法,并使用 Result.success(body) 方法将其包装成一个名为 Result 的类的实例。这个 Result 类通常包含了响应的状态码、消息和数据等信息,可以根据需要进行定制。

把我们的拦截器给注释掉,然后运行:

统一结果返回:

我们稍作总结得出结论:

ResponseBodyAdvice 是 Spring 框架中的一个接口,用于对控制器方法返回的响应体进行增强处理。其主要作用是在将响应体返回给客户端之前,允许开发者对响应体进行修改或包装,以实现统一的响应格式化。这一功能对于构建 API 应用特别有用。

通过实现 ResponseBodyAdvice 接口,开发者可以创建一个全局的增强器,该增强器将应用于所有控制器方法的响应体。这样一来,无论哪个控制器方法返回响应,都会经过这个增强器的处理,保证了响应的一致性和统一性。

在 ResponseBodyAdvice 接口中,需要实现 beforeBodyWrite 方法,该方法在每个控制器方法返回响应体之前被调用。在这个方法中,开发者可以获取原始的响应体,并对其进行处理。通常情况下,开发者会将原始的响应体包装成一个统一的格式,例如将其放入一个名为 Result 的类的实例中。

通过这种方式,开发者可以确保所有控制器方法返回的响应体都遵循统一的格式,包括状态码、消息和数据等信息。这简化了前端或其他客户端对响应的处理流程,提高了代码的可维护性和一致性。

ResponseBodyAdvice 的使用使得我们可以在不修改每个控制器方法的情况下,统一控制所有响应的格式,从而降低了重复代码的量,并使代码更加清晰易懂。

存在问题

我们现在再来看看别的方法:

我们可以对updateBook再进行修改:

    @RequestMapping("/updateBook")
    public Result updateBook(BookInfo bookInfo){
        log.info("更新图书, updateBook:{}",bookInfo);
        if (bookInfo.getId()<0){
            return Result.fail(false,"ID 不合法");
        }
        try {
            Integer result = bookService.updateBook(bookInfo);
            if (result<=0){
                return Result.fail(false,"更新失败");
            }
            return Result.success(true);
        }catch (Exception e){
            log.error("更新图书失败, e:{}", e);
            return Result.fail(false,e.getMessage());
        }
    }

 运行:

虽然运行成功,但是你会发现就像快递一样,这个响应结果被层层包裹。

我们当然不希望过度包装,所以再加一些处理:

package com.example.librarysystem.book.config;
import com.example.librarysystem.book.model.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

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

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        //对哪些请求进行处理  true-进行处理, false-不进行处理
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        //如果body是Result 类型, 不需要再包装
        if (body instanceof Result){
            return body;
        }
        return Result.success(body);
    }
}

再测试一下queryBook,也是统一的:

同理,测试addBook:

发现报错,回去看看日志:

这个错误表明代码尝试将响应体转换为字符串,但实际上响应体是一个类型为 com.example.librarysystem.book.model.Result 的对象。因此,发生了 java.lang.ClassCastException,无法将 Result 对象强制转换为字符串。

我们发现addBook方法和其他几个方法唯一不同的地方就是它的返回值是String。

那么我们来验证一下猜想:

package com.example.librarysystem.book.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

    @RequestMapping("/t2")
    public boolean t2() {
        return true;
    }

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

我们先注释掉统一接口返回:

现在统一结果处理: 

解决方案:

package com.example.librarysystem.book.config;
import com.example.librarysystem.book.model.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

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

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        //对哪些请求进行处理  true-进行处理, false-不进行处理
        return true;
    }
    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        //如果body是Result 类型, 不需要再包装
        if (body instanceof Result){
            return body;
        }
        //如果返回结果为String类型, 使⽤SpringBoot内置提供的Jackson来实现信息的序列化
        if (body instanceof String){
            return objectMapper.writeValueAsString(Result.success(body));
        }
        return Result.success(body);
    }
}

原因分析

Spring MVC 默认会注册一系列自带的 HttpMessageConverter,它们按照一定的顺序排列,其中包括 ByteArrayHttpMessageConverter、StringHttpMessageConverter、SourceHttpMessageConverter、AllEncompassingFormHttpMessageConverter 等。特别地,AllEncompassingFormHttpMessageConverter 会根据项目依赖情况添加对应的 HttpMessageConverter。

在引入 Jackson 包后,容器会自动将 MappingJackson2HttpMessageConverter 注册到 messageConverters 链的末尾。Spring 会根据返回的数据类型,从 messageConverters 链中选择合适的 HttpMessageConverter。当返回的数据不是字符串时,会使用 MappingJackson2HttpMessageConverter 写入返回对象。而当返回的数据是字符串时,StringHttpMessageConverter 会被优先选择。

在处理 ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage) 这一步时,会调用父类的 write 方法。但是由于 StringHttpMessageConverter 重写了 addDefaultHeaders 方法,因此会执行子类的方法。然而,子类 StringHttpMessageConverter 的 addDefaultHeaders 方法接收的参数类型为 String,而此时的 body 类型是 Result,导致类型不匹配的异常 "Result cannot be cast to java.lang.String" 的发生。

也就是说,默认情况下,Spring MVC会注册一系列内置的HttpMessageConverter,这些转换器负责将Java对象转换为HTTP响应体的各种格式,比如JSON、XML、字符串等。这些转换器被组织成一个链,按照优先级顺序依次尝试转换控制器方法的返回值。

其中,StringHttpMessageConverter是用于将Java字符串转换为HTTP响应体的转换器之一。因为字符串是一种常见的返回类型,所以它通常出现在排较前面,此时它会在转换器链中优先被选择。但是,StringHttpMessageConverter内部的addDefaultHeaders方法只接受字符串类型的参数,因此如果尝试将非字符串类型的对象传递给它,就会导致类型不匹配的异常。

在我们的代码中,异常发生的原因就是StringHttpMessageConverter尝试将Result类型的对象传递给addDefaultHeaders方法,而Result对象无法转换为字符串,导致类型转换异常。我们的ResponseAdvice类将所有的响应体都包装在Result对象中,而StringHttpMessageConverter不支持处理这种类型的对象。

优点

  1. 方便前端程序员更好的接收和解析后端数据接口返回的数据: 统一数据返回格式使前端程序员可以预期到后端接口返回的数据结构,从而更轻松地编写前端代码来处理这些数据。不需要每次都去查看文档或者请求示例,因为他们知道可以期待的数据结构。

  2. 降低前端程序员和后端程序员的沟通成本: 通过定义统一的数据返回格式,前端和后端的开发人员之间的沟通变得更加简单。前端开发人员不需要询问后端每个接口的返回格式,而是可以依赖于已经定义好的格式。这降低了沟通的需求,减少了可能出现的误解和不必要的沟通成本。

  3. 有利于项目统一数据的维护和修改: 通过统一数据返回格式,项目中所有的接口都会遵循相同的格式返回数据。这使得在项目中对数据格式的修改和维护变得更加容易。如果需要对返回数据进行修改,只需要在统一的数据格式中进行修改,而不需要分别修改每个接口的返回格式。

  4. 有利于后端技术部门的统一规范的标准制定: 统一数据返回格式有助于后端技术团队制定统一的规范和标准。定义统一的数据格式可以防止出现各种不一致或者奇怪的返回内容,从而使整个项目的代码更加规范和易于维护。

统一异常处理

统一异常处理是一个良好的实践,它有以下几个重要原因:

  1. 提高代码的可维护性和可读性: 统一异常处理可以将异常处理逻辑集中在一处,使代码更加清晰和易于理解。开发人员不需要在每个地方都编写相同的异常处理代码,而是在统一的地方处理所有异常。

  2. 提高系统的稳定性和可靠性: 统一异常处理可以捕获和处理系统中的各种异常情况,从而保证系统在面对异常情况时能够正确地处理并继续运行,而不至于因为未处理的异常而导致系统崩溃或异常退出。

  3. 提供更友好的用户体验: 通过统一异常处理,可以向用户返回统一格式的错误信息,告知用户发生了什么问题以及如何解决。这样可以提高用户体验,让用户更容易理解和处理错误。

  4. 便于排查和调试: 统一异常处理可以集中记录系统中发生的异常情况,包括异常类型、发生时间、异常堆栈信息等,这些信息对于排查和调试问题非常有帮助。通过统一异常处理,可以更方便地定位和解决系统中的问题。

总之,统一异常处理能够提高代码的可维护性、系统的稳定性和可靠性,同时提供更友好的用户体验和便于排查和调试的功能。因此,建议在开发过程中实现统一异常处理机制。

你刚刚可能已经注意到,总有一些我们捕获不到的异常。

我们回过头看看刚刚测试的时候显示的响应:

明明给我们报了一个500的错误,但是因为业务状态码显示的是200,程序还是对它进行了正常的处理,按照我们定义的BookInfo进行解析,把它包装了一下返回在data里面了。

这并不是我们想要的结果。我们希望的是当发生异常的时候,业务状态码应该返回-1,data至少应该返回null。

统一异常处理是通过使用 @ControllerAdvice 和 @ExceptionHandler 注解来实现的。

@ControllerAdvice 注解用于标识一个类,表示控制器通知类。这意味着该类中的方法可以用于全局控制器的异常处理。通常,我们会在这个类中定义一些方法,用于捕获和处理在控制器中抛出的异常。

@ExceptionHandler 注解用于标识方法,表示异常处理器。当控制器中抛出了异常,并且该异常与 @ExceptionHandler 注解所标识的方法中定义的异常类型匹配时,就会执行该方法,进行异常处理。在这个方法中,我们可以根据具体情况进行异常处理,比如记录日志、返回特定的错误信息等。

这两个注解结合使用,可以让我们在整个应用程序中统一处理异常,提高代码的可维护性和可读性。

在进行接口返回数据时,我们通常还需要在方法上添加 @ResponseBody 注解,以指示该方法返回的是数据,而不是视图。这样,返回的数据会被序列化为 JSON 或 XML 格式,方便客户端进行解析和处理。当然,如果你就是希望返回一个页面,那么就不需要加这个注解。这里我们可以直接在类上添加 @ResponseBody,因为我们所有的返回都希望是数据。

package com.example.librarysystem.book.config;

import com.example.librarysystem.book.model.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

@Slf4j
@ResponseBody
@ControllerAdvice
public class ErrorHandler {
    /**
     * 捕获异常, 返回统一的结果
     */
    @ExceptionHandler
    public Result handler(Exception e){
        log.error("捕获异常, e:", e);
        return Result.fail("内部发生错误"+e.getMessage());
    }

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

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

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

通过使用 @ControllerAdvice 和 @ExceptionHandler 注解,我们可以实现统一的异常处理机制,使得异常处理逻辑更加集中和统一,提高了代码的可维护性和可读性。

我们现在利用TestController进行测试:

package com.example.librarysystem.book.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

    //空指针异常
    @RequestMapping("/t2")
    public boolean t2() {
        String a = null;
        System.out.println(a.length());
        return true;
    }

    @RequestMapping("/t3")
    public String t3() {
        int a = 10 / 0;
        return "t3";
    }
}

异常捕获的顺序通常应该按照特定性从低到高的顺序进行,以确保最具体的异常类型首先被捕获和处理,而较一般的异常类型则在后续进行处理。一般来说,异常捕获的顺序应该遵循以下原则:

  1. 从具体到一般: 先捕获特定的异常类型,然后再捕获更一般的异常类型。这样可以确保更具体的异常能够被专门处理,而不会被更一般的异常处理器所忽略。

  2. 从低层到高层: 如果在多个异常处理器中存在嵌套关系,通常应该从内部到外部的顺序进行异常捕获。这样可以确保异常在适当的层次被处理,同时避免异常处理器的覆盖或者重复处理。

  3. 按照业务逻辑进行排列: 根据业务逻辑的复杂程度和异常的可能性,合理地安排异常处理器的顺序。通常来说,先处理那些可能性较高或者对系统影响较大的异常。

比如我们的程序, 首先会捕获特定的异常类型,例如NullPointerException、ArithmeticException等。这些异常通常是由于编程错误或者特定的业务逻辑导致的,因此需要特别处理。 如果某个异常没有被上面的特定异常处理器捕获,那么它将由后续的一般异常处理器来处理,例如RuntimeException。这些异常可能是由于程序运行时环境、配置问题或其他不可预测的情况导致的。通过按照特定性顺序捕获异常,可以确保异常被适当地处理,并且能够根据具体的异常类型返回相应的错误信息,从而提高系统的可靠性和稳定性。

@ControllerAdvice 源码分析

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

点击 @ControllerAdvice 实现源码如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

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;

@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 组件,因此它具有了组件的特性,可以被Spring容器扫描到并生效。这也解释了为什么在使用 @ControllerAdvice 注解时不需要再添加其他五大注解(@Component、@Service、@Repository、@Configuration、@RestController),因为@ControllerAdvice 本身已经具有了组件的功能。

通过将 @ControllerAdvice 注解添加到类上,Spring会将其识别为全局控制器通知,用于统一处理控制器层面的异常。这种设计使得异常处理器的配置更加简洁,同时也符合了Spring框架中的约定大于配置的理念。

下面我们看看Spring是怎么实现的,还是从 DispatcherServlet 的代码开始分析。

DispatcherServlet 对象在创建时会初始化一系列的对象:

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

initHandlerAdapters(context) 方法

initHandlerAdapters(context) 方法是Spring框架中的一个关键方法,它负责初始化处理器适配器(Handler Adapter)。处理器适配器是Spring MVC中的一个核心组件,负责将请求映射到合适的处理器(Handler)上,并且负责处理请求的参数绑定、数据转换等工作。

private void initHandlerAdapters(ApplicationContext context) {
    // 初始化处理器适配器列表
    this.handlerAdapters = null;
    // 如果设置了detectAllHandlerAdapters为true,则从ApplicationContext中获取所有的HandlerAdapter
    if (this.detectAllHandlerAdapters) {
        Map<String, HandlerAdapter> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);
        // 如果找到了匹配的HandlerAdapter,则进行排序并保存到handlerAdapters列表中
        if (!matchingBeans.isEmpty()) {
            this.handlerAdapters = new ArrayList(matchingBeans.values());
            AnnotationAwareOrderComparator.sort(this.handlerAdapters);
        }
    } else {
        // 如果detectAllHandlerAdapters为false,则尝试从ApplicationContext中获取名为"handlerAdapter"的HandlerAdapter
        try {
            HandlerAdapter ha = (HandlerAdapter)context.getBean("handlerAdapter", HandlerAdapter.class);
            this.handlerAdapters = Collections.singletonList(ha);
        } catch (NoSuchBeanDefinitionException var3) {
            // 如果未找到名为"handlerAdapter"的HandlerAdapter,则不做任何操作
        }
    }

    // 如果handlerAdapters为空,则使用默认的处理器适配器策略
    if (this.handlerAdapters == null) {
        this.handlerAdapters = this.getDefaultStrategies(context, HandlerAdapter.class);
        // 如果日志级别为TRACE,则记录使用默认策略的信息
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("No HandlerAdapters declared for servlet '" + this.getServletName() + "': using default strategies from DispatcherServlet.properties");
        }
    }
}

大致流程和思路:

  1. 首先初始化处理器适配器列表为null。
  2. 如果detectAllHandlerAdapters为true,则从ApplicationContext中获取所有的HandlerAdapter,并进行排序后保存到handlerAdapters列表中。
  3. 如果detectAllHandlerAdapters为false,则尝试从ApplicationContext中获取名为"handlerAdapter"的HandlerAdapter,如果找到则保存到handlerAdapters列表中。
  4. 如果handlerAdapters仍然为空,则使用默认的处理器适配器策略,这些策略通常在DispatcherServlet.properties文件中配置。
  5. 如果日志级别为TRACE,则记录使用默认策略的信息。

在 initHandlerAdapters(context) 方法中,首先会获取所有实现了 HandlerAdapter 接口的Bean,并保存起来。这些Bean包括各种类型的处理器适配器,如RequestMappingHandlerAdapter的Bean,这个Bean就是
@RequestMapping 注解能起作用的关键。RequestMappingHandlerAdapter 是一个特殊的处理器适配器,它是Spring MVC中用于处理使用 @RequestMapping 注解的控制器方法的关键组件。

 关键代码是里面的一个方法:

private void initControllerAdviceCache() {
    // 检查应用上下文是否为空
    if (this.getApplicationContext() != null) {
        // 查找标注了@ControllerAdvice注解的Bean,并将其存储在adviceBeans列表中
        List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(this.getApplicationContext());
        // 创建一个列表,用于存储RequestBodyAdvice和ResponseBodyAdvice的Bean
        List<Object> requestResponseBodyAdviceBeans = new ArrayList();
        // 遍历ControllerAdviceBean列表
        Iterator var3 = adviceBeans.iterator();
        while (var3.hasNext()) {
            ControllerAdviceBean adviceBean = (ControllerAdviceBean) var3.next();
            Class<?> beanType = adviceBean.getBeanType();
            // 检查Bean类型是否为空
            if (beanType == null) {
                throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
            }
            // 从Bean类型中选择标注了@ModelAttribute注解的方法,并将其缓存到modelAttributeAdviceCache中
            Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);
            if (!attrMethods.isEmpty()) {
                this.modelAttributeAdviceCache.put(adviceBean, attrMethods);
            }
            // 从Bean类型中选择标注了@InitBinder注解的方法,并将其缓存到initBinderAdviceCache中
            Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);
            if (!binderMethods.isEmpty()) {
                this.initBinderAdviceCache.put(adviceBean, binderMethods);
            }
            // 如果Bean类型是RequestBodyAdvice或者ResponseBodyAdvice的子类,则将其加入requestResponseBodyAdviceBeans列表中
            if (RequestBodyAdvice.class.isAssignableFrom(beanType) || ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
                requestResponseBodyAdviceBeans.add(adviceBean);
            }
        }
        // 将requestResponseBodyAdviceBeans列表中的Bean添加到requestResponseBodyAdvice列表中
        if (!requestResponseBodyAdviceBeans.isEmpty()) {
            this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans);
        }
        // 输出调试日志
        if (this.logger.isDebugEnabled()) {
            int modelSize = this.modelAttributeAdviceCache.size();
            int binderSize = this.initBinderAdviceCache.size();
            int reqCount = this.getBodyAdviceCount(RequestBodyAdvice.class);
            int resCount = this.getBodyAdviceCount(ResponseBodyAdvice.class);
            if (modelSize == 0 && binderSize == 0 && reqCount == 0 && resCount == 0) {
                this.logger.debug("ControllerAdvice beans: none");
            } else {
                this.logger.debug("ControllerAdvice beans: " + modelSize + " @ModelAttribute, " + binderSize + " @InitBinder, " + reqCount + " RequestBodyAdvice, " + resCount + " ResponseBodyAdvice");
            }
        }
    }
}

大致流程和思路:

  1. 获取应用上下文(ApplicationContext)。
  2. 查找标注了@ControllerAdvice注解的Bean,并将其存储在adviceBeans列表中。
  3. 遍历adviceBeans列表,对每个Bean执行以下操作:
    • 从Bean类型中选择标注了@ModelAttribute注解的方法,并将其缓存到modelAttributeAdviceCache中。
    • 从Bean类型中选择标注了@InitBinder注解的方法,并将其缓存到initBinderAdviceCache中。
    • 如果Bean类型是RequestBodyAdvice或者ResponseBodyAdvice的子类,则将其加入requestResponseBodyAdviceBeans列表中。
  4. 将requestResponseBodyAdviceBeans列表中的Bean添加到requestResponseBodyAdvice列表中。
  5. 输出调试日志,记录@ControllerAdvice注解的Bean中包含的Advice的数量。

这个方法的主要目的是初始化控制器通知缓存,以便后续的请求处理过程中能够正确地选择和使用控制器通知。

总之,在应用启动过程中,initHandlerAdapters(context) 方法会获取所有被 @ControllerAdvice 注解标注的Bean对象,并对它们做进一步处理。@ControllerAdvice 注解用于定义全局控制器通知,通常用于统一处理控制器层面的异常、数据绑定、数据转换等操作。因此,在初始化处理器适配器时,Spring会识别并处理所有的 @ControllerAdvice 注解标注的Bean,以确保它们能够在应用运行时起到作用。 

综上,在 Spring MVC 中,@ControllerAdvice 注解用于定义全局控制器通知,它可以包含多种类型的通知,如异常处理、数据绑定、数据转换等。其中,ResponseBodyAdvice 类用于在控制器方法返回结果之前对结果进行处理,通常用于统一数据封装或者对返回结果进行额外的处理。

当应用启动时,Spring 框架会扫描并识别所有使用了 @ControllerAdvice 注解的类,并将它们纳入管理。对于实现了 ResponseBodyAdvice 接口的类,Spring 将其识别为统一响应数据的处理器。这些类会被放置在容器中,以便在特定事件发生时调用相应的方法。

在请求处理过程中,DispatcherServlet 负责拦截所有请求并将它们分发给合适的控制器进行处理。而 RequestMappingHandlerAdapter 是 Spring MVC 中用于处理 @RequestMapping 注解的控制器方法的关键组件之一。它负责解析控制器方法的参数、执行方法、处理返回值等工作。在处理控制器方法返回结果时,RequestMappingHandlerAdapter 会检查是否存在实现了 ResponseBodyAdvice 接口的类,并在需要的时候调用它们的方法对返回结果进行处理。

因此,DispatcherServlet 和 RequestMappingHandlerAdapter 之间通过调用 ResponseBodyAdvice 类的方法来实现对控制器方法返回结果的统一处理。这样的设计使得在应用中可以轻松地实现统一的数据封装和处理逻辑。

initHandlerExceptionResolvers(context)

在 Spring MVC 中,DispatcherServlet 负责拦截并处理所有的 HTTP 请求,它通过一系列的组件来实现请求的处理过程。其中,initHandlerExceptionResolvers(context) 方法是 DispatcherServlet 初始化过程中的关键步骤之一。

用于初始化处理器异常解析器(HandlerExceptionResolver)的方法,initHandlerExceptionResolvers(ApplicationContext context,其中处理器异常解析器负责处理在请求处理过程中产生的异常,将其转换为合适的响应。

private void initHandlerExceptionResolvers(ApplicationContext context) {
    // 初始化处理器异常解析器列表为null
    this.handlerExceptionResolvers = null;
    // 如果设置了detectAllHandlerExceptionResolvers为true,则从ApplicationContext中获取所有的HandlerExceptionResolver
    if (this.detectAllHandlerExceptionResolvers) {
        Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
        // 如果找到了匹配的HandlerExceptionResolver,则进行排序并保存到handlerExceptionResolvers列表中
        if (!matchingBeans.isEmpty()) {
            this.handlerExceptionResolvers = new ArrayList(matchingBeans.values());
            AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
        }
    } else {
        // 如果detectAllHandlerExceptionResolvers为false,则尝试从ApplicationContext中获取名为"handlerExceptionResolver"的HandlerExceptionResolver
        try {
            HandlerExceptionResolver her = (HandlerExceptionResolver)context.getBean("handlerExceptionResolver", HandlerExceptionResolver.class);
            this.handlerExceptionResolvers = Collections.singletonList(her);
        } catch (NoSuchBeanDefinitionException var3) {
            // 如果未找到名为"handlerExceptionResolver"的HandlerExceptionResolver,则不做任何操作
        }
    }

    // 如果handlerExceptionResolvers为空,则使用默认的处理器异常解析器策略
    if (this.handlerExceptionResolvers == null) {
        this.handlerExceptionResolvers = this.getDefaultStrategies(context, HandlerExceptionResolver.class);
        // 如果日志级别为TRACE,则记录使用默认策略的信息
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("No HandlerExceptionResolvers declared in servlet '" + this.getServletName() + "': using default strategies from DispatcherServlet.properties");
        }
    }
}

大致流程和思路:

  1. 首先初始化处理器异常解析器列表为null。
  2. 如果detectAllHandlerExceptionResolvers为true,则从ApplicationContext中获取所有的HandlerExceptionResolver,并进行排序后保存到handlerExceptionResolvers列表中。
  3. 如果detectAllHandlerExceptionResolvers为false,则尝试从ApplicationContext中获取名为"handlerExceptionResolver"的HandlerExceptionResolver,如果找到则保存到handlerExceptionResolvers列表中。
  4. 如果handlerExceptionResolvers仍然为空,则使用默认的处理器异常解析器策略,这些策略通常在DispatcherServlet.properties文件中配置。
  5. 如果日志级别为TRACE,则记录使用默认策略的信息。

这个方法的主要目的是初始化处理器异常解析器列表,以便后续的请求处理过程中能够正确地选择和使用处理器异常解析器。

在这个方法中,DispatcherServlet 会获取所有实现了 HandlerExceptionResolver 接口的 bean,并将它们保存起来以备后续使用。HandlerExceptionResolver 接口定义了处理控制器方法抛出的异常的策略。通过实现该接口,开发人员可以自定义异常处理逻辑,例如将异常信息封装为特定格式的响应数据、跳转到指定的错误页面等。

其中,ExceptionHandlerExceptionResolver 是 Spring MVC 中用于处理 @ExceptionHandler 注解的关键组件之一。在应用启动过程中,它会扫描并获取所有被 @ControllerAdvice 注解标注的 bean 对象,并进一步处理它们。

通过结合 @ControllerAdvice 注解和 @ExceptionHandler 注解,开发人员可以在全局范围内定义异常处理器,并将它们应用于所有的控制器方法。这样,当控制器方法抛出异常时,ExceptionHandlerExceptionResolver 就会根据异常类型寻找匹配的异常处理器,并调用相应的方法进行异常处理。

ExceptionHandlerExceptionResolver 通过 ExceptionHandlerMethodResolver 来解析异常,并最终找到适用的 @ExceptionHandler 标注的方法。在 ExceptionHandlerMethodResolver 中,getMappedMethod(Class<? extends Throwable> exceptionType) 方法起着关键作用。

getMappedMethod(Class<? extends Throwable> exceptionType) 方法接收一个异常类型作为参数,然后会在已注册的异常处理类中查找对应的 @ExceptionHandler 注解标注的方法。它会根据给定的异常类型(exceptionType)从已映射的异常处理方法(mappedMethods)中找到最匹配的方法。

private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
    // 用于存储与给定异常类型匹配的异常类列表
    List<Class<? extends Throwable>> matches = new ArrayList<>();
    // 遍历已映射的异常处理方法集合
    Iterator var3 = this.mappedMethods.keySet().iterator();
    while (var3.hasNext()) {
        // 获取已映射的异常类
        Class<? extends Throwable> mappedException = (Class) var3.next();
        // 判断当前异常类型是否是映射的异常类或其子类
        if (mappedException.isAssignableFrom(exceptionType)) {
            // 如果是,则将该异常类加入匹配列表中
            matches.add(mappedException);
        }
    }
    // 如果匹配列表不为空,则从中选择最匹配的异常类
    if (!matches.isEmpty()) {
        // 如果匹配到多个异常类,则按照异常类的继承深度进行排序
        if (matches.size() > 1) {
            matches.sort(new ExceptionDepthComparator(exceptionType));
        }
        // 返回最匹配异常类对应的异常处理方法
        return (Method) this.mappedMethods.get(matches.get(0));
    } else {
        // 如果未匹配到任何异常类,则返回一个标识没有匹配异常处理方法的常量
        return NO_MATCHING_EXCEPTION_HANDLER_METHOD;
    }
}

大致流程和思路:

  1. 初始化一个空的列表用于存储与给定异常类型匹配的异常类。
  2. 遍历已映射的异常处理方法集合,对于每一个已映射的异常类,判断它是否是给定异常类型的父类或者相同类。
  3. 如果是,则将该异常类加入匹配列表中。
  4. 如果匹配列表不为空,则选择最匹配的异常类,如果匹配到多个异常类,则按照异常类的继承深度进行排序。
  5. 返回最匹配异常类对应的异常处理方法,如果未匹配到任何异常类,则返回一个标识没有匹配异常处理方法的常量。

按照异常类的继承深度进行排序的规则是根据抛出异常相对于声明异常的深度来进行的。具体来说,如果有多个异常处理方法能够处理同一类型的异常,那么会根据异常的继承关系来判断哪个方法更适合处理这个异常,继承深度越浅的方法会排在前面。

举例说明:

假设有两个异常处理方法:

  1. 方法handler(NullPointerException e),声明处理的异常类型是NullPointerException。
  2. 方法handler(Exception e),声明处理的异常类型是Exception。

假设抛出的异常是NullPointerException,它继承于RuntimeException,而RuntimeException又继承于Exception。根据继承关系,NullPointerException相对于handler(NullPointerException e)声明的深度为0,相对于handler(Exception e)声明的深度为2。因此,handler(NullPointerException e)标注的方法会排在前面,因为它更适合处理NullPointerException类型的异常。

这个排序规则确保了在处理多个匹配的异常处理方法时,优先选择最具体的方法来处理异常,这样能够确保异常能够被最合适的方法捕获和处理,提高了异常处理的精确性和准确性。

案例代码

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

登录页面

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

登录结果代码修改

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="css/bootstrap.min.css">
    <link rel="stylesheet" href="css/login.css">
    <script type="text/javascript" src="js/jquery.min.js"></script>
</head>

<body>
    <div class="container-login">
        <div class="container-pic">
            <img src="pic/computer.png" width="350px">
        </div>
        <div class="login-dialog">
            <h3>登录</h3>
            <div class="row">
                <span>用户名</span>
                <input type="text" name="userName" id="userName" class="form-control">
            </div>
            <div class="row">
                <span>密码</span>
                <input type="password" name="password" id="password" class="form-control">
            </div>
            <div class="row">
                <button type="button" class="btn btn-info btn-lg" onclick="login()">登录</button>
            </div>
        </div>
    </div>
    <script src="js/jquery.min.js"></script>
    <script>
        function login() {
        $.ajax({
            type:"post",
            url:"/user/login",
            data:{
                userName:$("#userName").val(),
                password:$("#password").val()
            } ,
            success:function(result){
                if(result.code == 200 && result.data==true){
                    //验证成功
                    location.href = "book_list.html";
                }else{
                    alert("用户名或密码错误!");
                }
            }
        });
        }
    </script>
</body>

</html>

图书列表页

增加图书修改:

弹出来的小框就是result。

这又是哪里出了问题?

前端后端都没有报错,并且后端一直在正常添加图书,说明后端代码没有问题。

这个时候我们只能回到前端代码添加一些日志,再次运行后通过前端的日志来判断问题。

实在不行就抓包。

问题其实出在返回类型。你还记得吗?我们之前看到过的,addBook这个方法返回的类型是字符串,也就是说result是字符串,通 “ . ” 的方式是不行的。

现在解决方法有两种:

1、前端处理,把字符串转为对象;

2、后端处理:设置content_type。

这里我们选择第二种处理方式,因为这样统一接口更规范。


    @RequestMapping(value = "/addBook",produces = "application/json")
    public String addBook(BookInfo bookInfo){
        log.info("添加图书, bookInfo:{}",bookInfo);
        //参数校验
        if (!StringUtils.hasLength(bookInfo.getBookName())
                || !StringUtils.hasLength(bookInfo.getAuthor())
                || bookInfo.getCount()<=0
                || bookInfo.getPrice()==null
                || !StringUtils.hasLength(bookInfo.getPublish())){
            return "参数错误";
        }
        //添加图书
        try {
            bookService.insertBook(bookInfo);
        }catch (Exception e){
            return "内部发生错误, 请联系管理员";
        }
        return "";

    }

所以记住,接口返回类型为String类型,注意“统一结果处理”和“接口设置返回类型”。

修改图书:

哇,一点进去我就惊呆了啊,啥都没有了……而且前端后端都没有任何提示……

回到前端代码看看,继续修改这个部分:

 

 

继续更改代码: 

修改完成:

删除图书:

都有问题,直接去修改前端代码:

 

 

首先,单次删除成功:

 

批量删除也成功了。

 如此这般,我们的图书管理系统编程终于告一段落。

  • 25
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值