RequestMappingHandlerMapping 与 RequestMappingHandlerAdapter

DispatcherServlet 的初始化

选择支持内嵌Tomcat 服务器的 Spring 容器作为 ApplicationContext 的实现:

AnnotationConfigServletWebServerApplicationContext context =
        new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);

WebConfig 作为配置类,向 Spring 容器中添加内嵌 Web 容器工厂、DispatcherServlet 和 DispatcherServlet 注册对象。

@Configuration
@ComponentScan
public class WebConfig {
    /**
     * 内嵌 Web 容器工厂
     */
    @Bean
    public TomcatServletWebServerFactory tomcatServletWebServerFactory() {
        return new TomcatServletWebServerFactory();
    }

    /**
     * 创建 DispatcherServlet
     */
    @Bean
    public DispatcherServlet dispatcherServlet() {
        return new DispatcherServlet();
    }

    /**
     * 注册 DispatcherServlet,Spring MVC 的入口
     */
    @Bean
    public DispatcherServletRegistrationBean dispatcherServletRegistrationBean(DispatcherServlet dispatcherServlet) {
        return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
    }
}

控制台输出:

Tomcat initialized with port(s): 8080 (http)
Root WebApplicationContext: initialization completed in 2132 ms

从输出中可以发现,此时DispatcherServlet 还未被加载。
当Tomcat 服务器 首次 使用到 DispatcherServlet 时,才会由Tomcat 服务器初始化 DispatcherServlet。
断点 DispatcherServlet 的 onRefresh() 方法中 this.initStrategies(context); 的所在行:

protected void onRefresh(ApplicationContext context) {
    this.initStrategies(context);
}

查看调用栈可知,是从 GenericServlet 的 init() 方法执行到 onRefresh() 方法的。
因此 DispatcherServlet 的初始化流程走的是 Servlet 的初始化流程。

使 DispatcherServlet 在Tomcat 服务器启动时被初始化

DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");
    registrationBean.setLoadOnStartup(1);

当存在多个 DispatcherServlet 需要被注册时,设置的 loadOnStartup 越大,优先级越小,初始化顺序越靠后。
再次重启程序,根据控制台输出的内容可知,不仅完成 Tomcat 和 Spring 容器的初始化,DispatcherServlet 也初始化成功。

抽取配置信息到配置文件中

使用 @PropertySource 注解设置配置类需要读取的配置文件,以便后续读取配置文件中的内容。
要读取配置文件中的内容,可以使用 @Value 注解,但该注解一次仅仅能够读取一个值,现实是往往需要从配置文件中读取多个值。
可以使用 @EnableConfigurationProperties 注解完成配置文件信息与对象的绑定,后续使用时作为 @Bean 注解标记的方法的参数直接在方法中使用即可:

@Configuration
@ComponentScan
@PropertySource("classpath:application.properties")
@EnableConfigurationProperties({WebMvcProperties.class, ServerProperties.class})
public class WebConfig {
   ......
    /**
     * 注册 DispatcherServlet,Spring MVC 的入口
     */
    @Bean
    public DispatcherServletRegistrationBean dispatcherServletRegistrationBean(DispatcherServlet dispatcherServlet,
                                                                               WebMvcProperties webMvcProperties) {
        DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");
        registrationBean.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
        return registrationBean;
    }
}
image.png
DispatcherServlet 初始化时执行的操作

回到 DispatcherServlet 的 onRefresh() 方法,它又调用了 initStrategies() 方法:

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

在这个方法中初始化了一系列组件,见名识意即可,重点介绍:

  • initHandlerMappings():初始化处理器映射器
  • initHandlerAdapters():初始化处理器适配器
  • initHandlerExceptionResolvers():初始化异常处理器

在所有的初始化方法中都有一个相似的逻辑,首先使用一个布尔值判断是否检测 所有 目标组件。
Spring 支持父子容器嵌套,如果判断的布尔值为 true,那么 Spring 不仅会在当前容器中获取目标组件,还会在其所有父级容器中寻找(这个特性仅在 Spring MVC 中有效,Boot 中已被合并)。
以 initHandlerMappings() 为例:

private void initHandlerMappings(ApplicationContext context) {
    this.handlerMappings = null;
    if (this.detectAllHandlerMappings) { // 是否需要检测所有处理器映射器
        // --snip--
    } else {
        // 无需检测所有处理器映射器时,获取当前容器中的处理器映射器
        // --snip--
    }

    if (this.handlerMappings == null) {
        // 当前容器中没有处理器映射器时,设置默认的处理器映射器
        this.handlerMappings = this.getDefaultStrategies(context, HandlerMapping.class);
        // --snip--
    }

    // --snip--
}

RequestMappingHandlerMapping

HandlerMapping,即处理器映射器,用于建立请求路径与控制器方法的映射关系。
RequestMappingHandlerMapping 是 HandlerMapping 的一种实现,根据类名可知,它是通过 @RequestMapping 注解来实现路径映射。
当 Spring 容器中没有 HandlerMapping 的实现时,尽管 DispatcherServlet 在初始化时会添加一些默认的实现,但这些实现不会交由 Spring 管理,而是作为 DispatcherServlet 的成员变量。

信息: Initializing Spring DispatcherServlet 'dispatcherServlet'
[INFO ] Initializing Servlet 'dispatcherServlet' 
[TRACE] No MultipartResolver 'multipartResolver' declared 
[TRACE] No LocaleResolver 'localeResolver': using default [AcceptHeaderLocaleResolver] 
[TRACE] No ThemeResolver 'themeResolver': using default [FixedThemeResolver] 
[TRACE] No HandlerMappings declared for servlet 'dispatcherServlet': using default strategies from DispatcherServlet.properties 
[TRACE] No HandlerAdapters declared for servlet 'dispatcherServlet': using default strategies from DispatcherServlet.properties 
[TRACE] No HandlerExceptionResolvers declared in servlet 'dispatcherServlet': using default strategies from DispatcherServlet.properties 
[TRACE] No RequestToViewNameTranslator 'viewNameTranslator': using default [DefaultRequestToViewNameTranslator] 
[TRACE] No ViewResolvers declared for servlet 'dispatcherServlet': using default strategies from DispatcherServlet.properties 
[TRACE] No FlashMapManager 'flashMapManager': using default [SessionFlashMapManager] 
[INFO] Completed initialization in 482 ms 

在配置类中将 RequestMappingHandlerMapping 添加到 Spring 容器:

@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
    return new RequestMappingHandlerMapping();
}

接下来用 Spring 提供的测试类来测试:

HandlerExecutionChain chain = handlerMapping.getHandler(new MockHttpServletRequest("GET", "/test1"));
[DEBUG] 16:51:35.349 [main] com.itheima.a20.Controller1         - test1() 

getHandler() 方法返回的对象时处理器执行链,不仅包含映射器方法,还包含需要执行的拦截器信息。

:::info
MockHttpServletRequest 的使用需要导入以下依赖:
:::

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
</dependency>

RequestMappingHandlerAdapter

RequestMappingHandlerAdapter 实现了 HandlerAdapter 接口,HandlerAdapter 用于执行控制器方法,而 RequestMapping 表明 RequestMappingHandlerAdapter 用于执行被 @RequestMapping 注解标记的控制器方法。

public class MyRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {
    @Override
    public ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
        return super.invokeHandlerMethod(request, response, handlerMethod);
    }
}
MyRequestMappingHandlerAdapter handlerAdapter = context.getBean(MyRequestMappingHandlerAdapter.class);
handlerAdapter.invokeHandlerMethod(request, response, ((HandlerMethod) handlerMapping.getHandler(request).getHandler()));

自定义参数解析器

假如经常需要使用到请求头中的 Token 信息,自定义 @Token 注解,使用该注解标记控制器方法的哪个参数来获取 Token 信息:

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Token {
}

自定义参数解析器:

public class TokenArgumentResolver implements HandlerMethodArgumentResolver {
    @Override//判断是否包含注解
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterAnnotation(Token.class) != null;
    }

    @Override//解析内容
    public Object resolveArgument(MethodParameter parameter,
                                  ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest,
                                  WebDataBinderFactory binderFactory) throws Exception {
        return webRequest.getHeader("token");
    }
}

将参数解析器添加到 HandlerAdapter 中:

 adapter.setCustomArgumentResolvers(Collections.singletonList(tokenArgumentResolver));

参数解析器

测试数据:

static class Controller {
    public void test(
            @RequestParam("name1") String name1, // name1=张三
            String name2, // name2=李四
            @RequestParam("age") int age, // age=18
            @RequestParam(name = "home", defaultValue = "${JAVA_HOME}") String home1, // spring 获取数据
            @RequestParam("file") MultipartFile file, // 上传文件
            @PathVariable("id") int id, //  /test/124   /test/{id}
            @RequestHeader("Content-Type") String header,
            @CookieValue("token") String token,
            @Value("${JAVA_HOME}") String home2, // spring 获取数据  ${} #{}
            HttpServletRequest request, // request, response, session ...
            @ModelAttribute("abc") User user1, // name=zhang&age=18
            User user2, // name=zhang&age=18
            @RequestBody User user3 // json
    ) {
    }
}

@Getter
@Setter
@ToString
static class User {
    private String name;
    private int age;
}

@RequestParam

@RequestParam 注解的解析需要使用到 RequestParamMethodArgumentResolver 参数解析器。构造时需要两个参数:

  • beanFactory:Bean 工厂对象。需要解析 ${} 时,就需要指定 Bean 工厂对象
  • useDefaultResolution:布尔类型参数。为 false 表示只解析添加了 @RequestParam 注解的参数,为 true 针对未添加 @RequestParam 注解的参数也使用该参数解析器进行解析。

RequestParamMethodArgumentResolver 利用 resolveArgument() 方法完成参数的解析,该方法需要传递四个参数:

  • parameter:参数对象
  • mavContainer:ModelAndView 容器,用来存储中间的 Model 结果
  • webRequest:由 ServletWebRequest 封装后的请求对象
  • binderFactory:数据绑定工厂,用于完成对象绑定和类型转换,比如将字符串类型的 18 转换成整型
public static void main(String[] args) throws Exception {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
    ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
    HttpServletRequest request = mockRequest();

    // 控制器方法封装成 HandlerMethod
    Method method = Controller.class.getMethod("test", String.class, String.class,
            int.class, String.class, MultipartFile.class,
            int.class, String.class, String.class,
            String.class, HttpServletRequest.class, User.class,
            User.class, User.class);
    HandlerMethod handlerMethod = new HandlerMethod(new Controller(), method);

    // 准备对象绑定与类型转换
    ServletRequestDataBinderFactory binderFactory = new ServletRequestDataBinderFactory(null, null);

    // 准备 ModelAndViewContainer 用来存储中间的 Model 结果
    ModelAndViewContainer container = new ModelAndViewContainer();

    // 解析每个参数值
    for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
        // useDefaultResolution 为 false 表示必须添加 @RequestParam 注解
        RequestParamMethodArgumentResolver resolver = new RequestParamMethodArgumentResolver(beanFactory, true);

        String annotations = Arrays.stream(parameter.getParameterAnnotations())
                .map(i -> i.annotationType().getSimpleName()).collect(Collectors.joining());
        String appendAt = annotations.length() > 0 ? "@" + annotations + " " : "";
        // 设置参数名解析器
        parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
        String paramInfo = "[" + parameter.getParameterIndex() + "] " + appendAt +
                parameter.getParameterType().getSimpleName() + " " + parameter.getParameterName();

        if (resolver.supportsParameter(parameter)) {
            Object v = resolver.resolveArgument(parameter, container, new ServletWebRequest(request), binderFactory);
            System.out.println(Objects.requireNonNull(v).getClass());
            System.out.println(paramInfo + " -> " + v);
        } else {
            System.out.println(paramInfo);
        }
    }
}

多个参数解析器的组合 - 组合模式

不同种类的参数需要不同的参数解析器,当前使用的参数解析器不支持当前参数的解析时,就应该换一个参数解析器进行解析。
可以将所有参数解析器添加到一个集合中,然后遍历这个集合,实现上述需求。
Spring 提供了名为 HandlerMethodArgumentResolverComposite 的类,对上述逻辑进行封装。

public static void main(String[] args) throws Exception {
    // --snip--

    // 解析每个参数值
    for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
        // 多个参数解析器的组合
        HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
        composite.addResolvers(
                // useDefaultResolution 为 false 表示必须添加 @RequestParam 注解
                new RequestParamMethodArgumentResolver(beanFactory, true)
        );

        // --snip--

        if (composite.supportsParameter(parameter)) {
            Object v = composite.resolveArgument(parameter, container, new ServletWebRequest(request), binderFactory);
            System.out.println(paramInfo + " -> " + v);
        } else {
            System.out.println(paramInfo);
        }
    }
}

@PathVariable

@PathVariable 注解的解析需要使用到 PathVariableMethodArgumentResolver 参数解析器。构造时无需传入任何参数。
使用该解析器需要一个 Map 集合,该 Map 集合是 @RequestMapping 注解上指定的路径和实际 URL 路径进行匹配后,得到的路径上的参数与实际路径上的值的关系(获取这个 Map 并将其设置给 request 作用域由 HandlerMapping 完成)。

@RequestHeader

@RequestHeader 注解的解析需要使用到 RequestHeaderMethodArgumentResolver 参数解析器。构造时需要传入一个Bean 工厂对象。

@CookieValue

@CookieValue 注解的解析需要使用到 ServletCookieValueMethodArgumentResolver 参数解析器。构造时需要传入一个Bean 工厂对象。

@Value

@Value 注解的解析需要使用到 ExpressionValueMethodArgumentResolver 参数解析器。构造时需要传入一个Bean 工厂对象。

HttpServletRequest

HttpServletRequest 类型的参数的解析需要使用到 ServletRequestMethodArgumentResolver 参数解析器。构造时无需传入任何参数。
ServletRequestMethodArgumentResolver 参数解析器不仅可以解析 HttpServletRequest 类型的参数,还支持许多其他类型的参数,其支持的参数类型可在 supportsParameter() 方法中看到:

public boolean supportsParameter(MethodParameter parameter) {
    Class<?> paramType = parameter.getParameterType();
    return (WebRequest.class.isAssignableFrom(paramType) ||
            ServletRequest.class.isAssignableFrom(paramType) ||
            MultipartRequest.class.isAssignableFrom(paramType) ||
            HttpSession.class.isAssignableFrom(paramType) ||
            (pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
            (Principal.class.isAssignableFrom(paramType) && !parameter.hasParameterAnnotations()) ||
            InputStream.class.isAssignableFrom(paramType) ||
            Reader.class.isAssignableFrom(paramType) ||
            HttpMethod.class == paramType ||
            Locale.class == paramType ||
            TimeZone.class == paramType ||
            ZoneId.class == paramType);
}

@ModelAttribute

@ModelAttribute 注解的解析需要使用到 ServletModelAttributeMethodProcessor 参数解析器。构造时需要传入一个布尔类型的值。为 false 时,表示 @ModelAttribute 不是不必须的,即是必须的。
针对 @ModelAttribute("abc") User user1 和 User user2 两种参数来说,尽管后者没有使用 @ModelAttribute 注解,但它们使用的是同一种解析器。
添加两个 ServletModelAttributeMethodProcessor 参数解析器,先解析带 @ModelAttribute 注解的参数,再解析不带 @ModelAttribute 注解的参数。
通过 ServletModelAttributeMethodProcessor 解析得到的数据还会被存入 ModelAndViewContainer 中。存储的数据结构是一个 Map,其 key 为 @ModelAttribute 注解指定的 value 值,在未显式指定的情况下,默认为对象类型的首字母小写对应的字符串。

[0] @RequestParam String name1 -> zhangsan
[1] String name2
[2] @RequestParam int age -> 18
[3] @RequestParam String home1 -> D:\environment\JDK1.8
[4] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@2beee7ff
[5] @PathVariable int id -> 123
[6] @RequestHeader String header -> application/json
[7] @CookieValue String token -> 123456
[8] @Value String home2 -> D:\environment\JDK1.8
[9] HttpServletRequest request -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@5fa07e12
[10] @ModelAttribute User user1 -> A21.User(name=张三, age=18)
模型数据: {abc=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.abc=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
[11] User user2 -> A21.User(name=张三, age=18)
模型数据: {abc=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.abc=org.springframework.validation.BeanPropertyBindingResult: 0 errors, user=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.user=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
[12] @RequestBody User user3 -> A21.User(name=李四, age=20)
模型数据: {abc=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.abc=org.springframework.validation.BeanPropertyBindingResult: 0 errors, user=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.user=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

@RequestBody User user3 参数也被 ServletModelAttributeMethodProcessor 解析了,如果想使其数据通过 JSON 数据转换而来,则需要使用另一个参数解析器。

@RequestBody

@RequestBody 注解的解析需要使用到 RequestResponseBodyMethodProcessor 参数解析器。构造时需要传入一个消息转换器列表。
@RequestBody User user3 参数数据通过 JSON 数据得到,与上一节的解析进行区分。
除此之外,添加的参数解析器顺序也影响着解析结果:

new ServletModelAttributeMethodProcessor(false),
new RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())),
new ServletModelAttributeMethodProcessor(true)

先添加解析 @ModelAttribute 注解的解析器,再添加解析 @RequestBody 注解的解析器,最后添加解析省略了 @ModelAttribute 注解的解析器。如果更换最后两个解析器的顺序,那么 @RequestBody User user3 将会被 ServletModelAttributeMethodProcessor 解析,而不是 RequestResponseBodyMethodProcessor。
因此 String name2 参数也能通过添加同种参数但不同构造参数的解析器进行解析,注意添加的解析器的顺序,先处理对象,再处理单个参数:

[0] @RequestParam String name1 -> zhangsan
[1] String name2 -> lisi
[2] @RequestParam int age -> 18
[3] @RequestParam String home1 -> D:\environment\JDK1.8
[4] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@5e17553a
[5] @PathVariable int id -> 123
[6] @RequestHeader String header -> application/json
[7] @CookieValue String token -> 123456
[8] @Value String home2 -> D:\environment\JDK1.8
[9] HttpServletRequest request -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@13bc8645
[10] @ModelAttribute User user1 -> A21.User(name=张三, age=18)
[11] User user2 -> A21.User(name=张三, age=18)
[12] @RequestBody User user3 -> A21.User(name=李四, age=20)

获取参数名

在项目的 src 目录外创建一个 Bean2.java 文件,使其不会被 IDEA 自动编译:

package indi.mofan.a22;

public class Bean2 {
    public void foo(String name, int age) {

    }
}

将命令行切换到 Bean2.java 文件所在目录的位置,执行 javac .\Bean2.java 命令手动编译 Bean2.java。查看 Bean2.class 文件的内容:

package indi.mofan.a22;

public class Bean2 {
    public Bean2() {
    }

    public void foo(String var1, int var2) {
    }
}

编译生成的 class 文件中的 foo() 方法的参数名称不再是 nameage,也就是说直接使用 javac 命令进行编译得到的字节码文件不会保存方法的参数名称。
执行 javac -parameters .\Bean2.java 再次编译 Bean2.java,并查看得到的 Bean2.class 文件内容:

package indi.mofan.a22;

public class Bean2 {
    public Bean2() {
    }

    public void foo(String name, int age) {
    }
}

foo() 方法的参数名称得以保留。
还可以使用 javap -c -v .\Bean2.class 命令反编译 Bean2.classfoo() 方法的反编译结果如下:

public void foo(java.lang.String, int);
  descriptor: (Ljava/lang/String;I)V
  flags: ACC_PUBLIC
  Code:
    stack=0, locals=3, args_size=3
       0: return
    LineNumberTable:
      line 6: 0
  MethodParameters:
    Name                           Flags
    name
    age

foo() 方法的参数信息被保存在 MethodParameters 中,可以使用 反射 获取.
使用 javac -g .\Bean2.java 命令进行编译也会保留方法的参数信息。再次使用 javap 反编译 Bean2.classfoo() 方法的反编译结果如下:

public void foo(java.lang.String, int);
  descriptor: (Ljava/lang/String;I)V
  flags: ACC_PUBLIC
  Code:
    stack=0, locals=3, args_size=3
       0: return
    LineNumberTable:
      line 6: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       1     0  this   Lindi/mofan/a22/Bean2;
          0       1     1  name   Ljava/lang/String;
          0       1     2   age   I

foo() 方法的参数信息被保存在 LocalVariableTable 中,不能使用反射获取,但可以使用 ASM 获取,使用 Spring 封装的解析工具:

public static void main(String[] args) throws Exception {
    // 反射获取参数名
    Method foo = Bean2.class.getMethod("foo", String.class, int.class);

    // 基于 LocalVariableTable 本地变量表获取
    LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
    String[] parameterNames = discoverer.getParameterNames(foo);
    System.out.println(Arrays.toString(parameterNames));
}

DefaultParameterNameDiscoverer 将两种实现进行了统一:

public class DefaultParameterNameDiscoverer extends PrioritizedParameterNameDiscoverer {

   public DefaultParameterNameDiscoverer() {
      if (KotlinDetector.isKotlinReflectPresent() && !NativeDetector.inNativeImage()) {
         addDiscoverer(new KotlinReflectionParameterNameDiscoverer());
      }
      addDiscoverer(new StandardReflectionParameterNameDiscoverer());
      addDiscoverer(new LocalVariableTableParameterNameDiscoverer());
   }

}

:::info
javac -g 的局限性:只限类使用,接口无法使用
:::

对象绑定与类型转换

底层第一套转换接口与实现(Spring)

image.png
  • Printer 把其它类型转为 String
  • ParserString 转为其它类型
  • Formatter 综合 PrinterParser 的功能
  • Converter 把类型 S 转为类型 T
  • PrinterParserConverter 经过适配转换成 GenericConverter 放入 Converters 集合
  • FormattingConversionService 利用其它接口实现转换

底层第二套转换接口(JDK)

image.png
  • PropertyEditorString 与其它类型相互转换
  • PropertyEditorRegistry 可以注册多个 PropertyEditor 对象
  • 可以通过 FormatterPropertyEditorAdapter 与第一套接口进行适配

高层转换接口与实现

image.png
  • 它们都实现了 TypeConverter 高层转换接口,在转换时会用到 TypeConverterDelegate 委派ConversionServicePropertyEditorRegistry 真正执行转换(使用 Facade 门面模式)
  • 首先查看是否存在实现了 PropertyEditorRegistry 的自定义转换器,@InitBinder 注解实现的就是自定义转换器(用了适配器模式把 Formatter 转为需要的 PropertyEditor
  • 再查看是否存在 ConversionService 实现
  • 再利用默认的 PropertyEditor 实现
  • 最后有一些特殊处理
  • SimpleTypeConverter 仅做类型转换
  • BeanWrapperImpl 利用 Property,即 Getter/Setter,为 Bean 的属性赋值,,必要时进行类型转换
  • DirectFieldAccessor 利用 Field,即字段,为 Bean 的字段赋值,必要时进行类型转换
  • ServletRequestDataBinder 为 Bean 的属性执行绑定,必要时进行类型转换,根据布尔类型成员变量 directFieldAccess 选择利用 Property 还是 Field,还具备校验与获取校验结果功能

ControllerAdvice 之 @InitBinder

image.png
@InitBinder 注解只能作用在方法上,通常搭配 @ControllerAdvice@Controller 以及他们的衍生注解使用。比如:

@Configuration
public class WebConfig {
    @ControllerAdvice
    static class MyControllerAdvice {
        @InitBinder
        public void binder3(WebDataBinder webDataBinder) {
            webDataBinder.addCustomFormatter(new MyDateFormatter("binder3 转换器"));
        }
    }

    @Controller
    static class Controller1 {
        @InitBinder
        public void binder1(WebDataBinder webDataBinder) {
            webDataBinder.addCustomFormatter(new MyDateFormatter("binder1 转换器"));
        }

        public void foo() {
        }
    }

    @Controller
    static class Controller2 {
        @InitBinder
        public void binder21(WebDataBinder webDataBinder) {
            webDataBinder.addCustomFormatter(new MyDateFormatter("binder21 转换器"));
        }

        @InitBinder
        public void binder22(WebDataBinder webDataBinder) {
            webDataBinder.addCustomFormatter(new MyDateFormatter("binder22 转换器"));
        }

        public void foo() {
        }
    }
}

@InitBinder 作用的方法存在于被 @ControllerAdvice 标记的类里面时,是对 所有 控制器都生效的自定义类型转换器。当 @InitBinder 作用的方法存在于被 @Controller 标记的类里面时,是 只对当前 控制器生效的自定义类型转换器。
@InitBinder 的来源有两个:

  1. @ControllerAdvice 标记的类中 @InitBinder 标记的方法,由 RequestMappingHandlerAdapter 在初始化时解析并记录
  2. @Controller 标记的类中 @InitBinder 标记的方法,由 RequestMappingHandlerAdapter 在控制器方法首次执行时解析并记录

控制器方法执行流程

ServletInvocableHandlerMethod 的组成:
image.png
HandlerMethod 需要:

  • bean,即哪个 Controller
  • method,即 Controller 中的哪个方法

ServletInvocableHandlerMethod 需要:

  • WebDataBinderFactory,用于对象绑定、类型转换
  • ParameterNameDiscoverer,用于参数名解析
  • HandlerMethodArgumentResolverComposite,用于解析参数
  • HandlerMethodReturnValueHandlerComposite,用于处理返回值

控制器方法执行流程:
RequestMappingHandlerAdapter 为起点,创建 WebDataBinderFactory,添加自定义类型转换器,再创建 ModelFactory,添加 Model 数据。
image.png
接下来调用 ServletInvocableHandlerMethod,主要完成三件事:

  1. 准备参数
  2. 反射调用控制器方法
  3. 处理返回值
image.png

ControllerAdvice 之 @ModelAttribute

准备 @ModelAttribute 在整个 HandlerAdapter 调用过程中所处的位置:
image.png
@ModelAttribute 可以作用在参数上和方法上。
当其作用在参数上时,会将请求中的参数信息 按名称 注入到指定对象中,并将这个对象信息自动添加到 ModelMap 中。当未指定 @ModelAttributevalue 时,添加到 ModelMap 中的 key 是对象类型首字母小写对应的字符串。此时的 @ModelAttribute 注解由 ServletModelAttributeMethodProcessor 解析。
当其作用在方法上时:

  • 如果该方法在被 @Controller 注解标记的类中,会在当前控制器中每个控制器方法执行前执行被 @ModelAttribute 标记的方法,如果该方法有返回值,自动将返回值添加到 ModelMap 中。当未指定 @ModelAttributevalue 时,添加到 ModelMap 中的 key 是返回值类型首字母小写对应的字符串。
  • 如果该方法在被 @ControllerAdvice 注解标记的类中,会在所有控制器方法执行前执行该方法。

作用在方法上的 @ModelAttribute 注解由 RequestMappingHandlerAdapter 解析。

返回值处理器

测试类:

@Slf4j
static class Controller {
    public ModelAndView test1() {
        log.debug("test1()");
        ModelAndView mav = new ModelAndView("view1");
        mav.addObject("name", "张三");
        return mav;
    }

    public String test2() {
        log.debug("test2()");
        return "view2";
    }

    @ModelAttribute
    public User test3() {
        log.debug("test3()");
        return new User("李四", 20);
    }

    public User test4() {
        log.debug("test4()");
        return new User("王五", 30);
    }

    public HttpEntity<User> test5() {
        log.debug("test5()");
        return new HttpEntity<>(new User("赵六", 40));
    }

    public HttpHeaders test6() {
        log.debug("test6()");
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-Type", "text/html");
        return headers;
    }

    @ResponseBody
    public User test7() {
        log.debug("test7()");
        return new User("钱七", 50);
    }
}

@Getter
@Setter
@ToString
@AllArgsConstructor
public static class User {
    private String name;
    private int age;
}

为测试渲染视图(MVC 早期使用)需要额外引入依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

相关配置:

@Configuration
public class WebConfig {
    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setDefaultEncoding("utf-8");
        configurer.setTemplateLoaderPath("classpath:templates");
        return configurer;
    }

    /**
     * FreeMarkerView 在借助 Spring 初始化时,会要求在 web 环境才会走 setConfiguration, 这里想办法去掉了 web 环境的约束
     */
    @Bean
    public FreeMarkerViewResolver viewResolver(FreeMarkerConfigurer configurer) {
        FreeMarkerViewResolver resolver = new FreeMarkerViewResolver() {
            @Override
            protected AbstractUrlBasedView instantiateView() {
                FreeMarkerView view = new FreeMarkerView() {
                    @Override
                    protected boolean isContextRequired() {
                        return false;
                    }
                };
                view.setConfiguration(configurer.getConfiguration());
                return view;
            }
        };
        resolver.setContentType("text/html;charset=utf-8");
        resolver.setPrefix("/");
        resolver.setSuffix(".ftl");
        resolver.setExposeSpringMacroHelpers(false);
        return resolver;
    }
}

FreeMaker 使用方法:

@SuppressWarnings("all")
private static void renderView(ApplicationContext context, ModelAndViewContainer container,
                               ServletWebRequest webRequest) throws Exception {
    log.debug(">>>>>> 渲染视图");
    FreeMarkerViewResolver resolver = context.getBean(FreeMarkerViewResolver.class);
    String viewName = container.getViewName() != null ? container.getViewName() : new DefaultRequestToViewNameTranslator().getViewName(webRequest.getRequest());
    log.debug("没有获取到视图名, 采用默认视图名: {}", viewName);
    // 每次渲染时, 会产生新的视图对象, 它并非被 Spring 所管理, 但确实借助了 Spring 容器来执行初始化
    View view = resolver.resolveViewName(viewName, Locale.getDefault());
    view.render(container.getModel(), webRequest.getRequest(), webRequest.getResponse());
    System.out.println(new String(((MockHttpServletResponse) webRequest.getResponse()).getContentAsByteArray(), StandardCharsets.UTF_8));
}

ModelAndView

ModelAndView 类型的返回值由 ModelAndViewMethodReturnValueHandler 处理,构造时无需传入任何参数。
解析 ModelAndView 时,将其中的视图和模型数据分别提取出来,放入 ModelAndViewContainer 中,之后根据视图信息找到对应的模板页面,再将模型数据填充到模板页面中,完成视图的渲染。

字符串类型

控制器方法的返回值是字符串类型时,返回的字符串即为视图的名称。与 ModelAndView 类型的返回值相比,不包含模型数据。
此种类型的返回值由 ViewNameMethodReturnValueHandler 处理,构造时无需传入任何参数。

@ModelAttribute

@ModelAttribute 注解作用在方法上时,会将方法的返回值作为模型数据添加到 ModelAndViewContainer 中。
@ModelAttribute 标记的方法的返回值由 ServletModelAttributeMethodProcessor 解析,构造时需要传入一个布尔类型数据 annotationNotRequired,表示 @ModelAttribute 注解是否不是必须的。
模型数据已经有了,但视图名称又是什么呢?
在实际开发场景中,控制器方法需要被 @RequestMapping 标记,并指定请求地址,比如:

@ModelAttribute
@RequestMapping("/test3")
public User test3() {
    log.debug("test3()");
    return new User("李四", 20);
}

当未找到视图名称时,默认以请求路径作为视图名称。
但在本节测试中省略了路径映射这一步,因此需要通过编程的方式将请求路径解析后的结果放入 request 作用域中。

private static Consumer<MockHttpServletRequest> mockHttpServletRequestConsumer(String methodName) {
    return req -> {
        req.setRequestURI("/" + methodName);
        UrlPathHelper.defaultInstance.resolveAndCacheLookupPath(req);
    };
}

private static void test3(ApplicationContext context) {
    String methodName = "test3";
    testReturnValueProcessor(context, methodName, 
                             mockHttpServletRequestConsumer(methodName), null);
}

HttpEntity

HttpEntity 类型的返回值由 HttpEntityMethodProcessor 处理,构造时需要传入一个消息转换器列表。
这种类型的返回值表示响应完成,无需经过视图的解析、渲染流程再生成响应。可在处理器的 handleReturnValue() 方法中得以论证:

public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
                              ModelAndViewContainer mavContainer, 
                              NativeWebRequest webRequest) throws Exception {
    // 一进入方法就设置请求处理完毕
    mavContainer.setRequestHandled(true);

    // --snip--
}

HttpEntity 中包含了状态码、响应体信息和响应头信息。

HttpHeaders

HttpEntity 相比,HttpHeaders 只包含响应头信息,HttpHeaders 类型的返回值由 HttpHeadersReturnValueHandler 处理,构造时无需传入任何参数。
HttpEntity 一样,这种类型的返回值也表示响应完成,无需经过视图的解析、渲染流程再生成响应,也可在处理器的 handleReturnValue() 方法中得以论证(省略源码)。

@ResponseBody

@ResponseBody 标记的方法的返回值由 RequestResponseBodyMethodProcessor 处理,构造时需要传入一个消息转换器列表。
这样的返回值也表示响应完成,无需经过视图的解析、渲染流程再生成响应,也可在处理器的 handleReturnValue() 方法中得以论证(省略源码)。

ControllerAdvice 之 ResponseBodyAdvice

ResponseBodyAdvice 增强 在整个 HandlerAdapter 调用过程中所处的位置:
image.png
ResponseBodyAdvice 是一个接口,对于实现了这个接口并被 @ControllerAdvice 标记的类来说,能够在调用每个控制器方法返回结果前,调用重写的 ResponseBodyAdvice 接口中的 beforeBodyWrite() 方法对返回值进行增强。
现有一个控制器类与内部使用到的 User 类:

@Controller
public static class MyController {
    @ResponseBody
    public User user() {
        return new User("王五", 18);
    }
}

@Getter
@Setter
@ToString
@AllArgsConstructor
public static class User {
    private String name;
    private int age;
}

调用控制器方法,并输出响应数据:

public static void main(String[] args) throws Exception {
    AnnotationConfigApplicationContext context =
        new AnnotationConfigApplicationContext(WebConfig.class);

    ServletInvocableHandlerMethod handlerMethod = new ServletInvocableHandlerMethod(
        context.getBean(WebConfig.MyController.class),
        WebConfig.MyController.class.getMethod("user")
    );
    handlerMethod.setDataBinderFactory(new ServletRequestDataBinderFactory(Collections.emptyList(), null));
    handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
    // 设置参数解析器(省略源码)
    handlerMethod.setHandlerMethodArgumentResolvers(getArgumentResolvers(context));
    // 设置返回值处理器(省略源码)
    handlerMethod.setHandlerMethodReturnValueHandlers(getReturnValueHandlers(context));

    MockHttpServletRequest request = new MockHttpServletRequest();
    MockHttpServletResponse response = new MockHttpServletResponse();
    ModelAndViewContainer container = new ModelAndViewContainer();
    handlerMethod.invokeAndHandle(new ServletWebRequest(request, response), container);

    System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
    context.close();
}

输出:

{"name":"王五","age":18}

在实际开发场景中常常需要对返回的数据类型进行统一,比如都返回 Result 类型:

@Getter
@Setter
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Result {
    private int code;
    private String msg;
    private Object data;

    @JsonCreator
    private Result(@JsonProperty("code") int code, @JsonProperty("data") Object data) {
        this.code = code;
        this.data = data;
    }

    private Result(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public static Result ok() {
        return new Result(200, null);
    }

    public static Result ok(Object data) {
        return new Result(200, data);
    }

    public static Result error(String msg) {
        return new Result(500, "服务器内部错误:" + msg);
    }
}

除了直接让控制器方法返回 Result 外,还可以使用 ResponseBodyAdvice 进行增强:

@ControllerAdvice
static class MyControllerAdvice implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType,
                            Class<? extends HttpMessageConverter<?>> converterType) {
        /*
         * 满足条件才转换
         * 1. 控制器方法被 @ResponseBody 注解标记
         * 2. 控制器方法所在类被 @ResponseBody 注解或包含 @ResponseBody 注解的注解标记
         */
        return returnType.getMethodAnnotation(ResponseBody.class) != null
            || AnnotationUtils.findAnnotation(returnType.getContainingClass(), ResponseBody.class) != null;
    }

    @Override
    public Object beforeBodyWrite(Object body,
                                  MethodParameter returnType,
                                  MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request,
                                  ServerHttpResponse response) {
        if (body instanceof Result) {
            return body;
        }
        return Result.ok(body);
    }
}
{"code":200,"data":{"name":"王五","age":18}}

异常处理

DispatcherServlet 中对异常处理的核心方法是 processHandlerException(),在这个方法中会对所有异常解析器进行遍历,然后使用每个异常解析器对异常信息进行处理。
存放异常解析器的是 DispatcherServlet 中泛型为 HandlerExceptionResolver、名为 handlerExceptionResolvers 的列表成员变量。
HandlerExceptionResolver 是一个接口,本节讲解解析 @ExceptionHandler 注解的异常解析器 ExceptionHandlerExceptionResolver
四个控制器类,测试异常处理方法被 @ResponseBody 注解标记、异常处理方法返回 ModelAndView、嵌套异常和对异常处理方法的参数处理:

static class Controller1 {
    public void foo() {
    }

    @ResponseBody
    @ExceptionHandler
    public Map<String, Object> handle(ArithmeticException e) {
        return Collections.singletonMap("error", e.getMessage());
    }
}

static class Controller2 {
    public void foo() {
    }

    @ExceptionHandler
    public ModelAndView handler(ArithmeticException e) {
        return new ModelAndView("test2", Collections.singletonMap("error", e.getMessage()));
    }
}

static class Controller3 {
    public void foo() {
    }

    @ResponseBody
    @ExceptionHandler
    public Map<String, Object> handle(IOException e) {
        return Collections.singletonMap("error", e.getMessage());
    }
}

static class Controller4 {
    public void foo() {}

    @ExceptionHandler
    @ResponseBody
    public Map<String, Object> handle(Exception e, HttpServletRequest request) {
        System.out.println(request);
        return Collections.singletonMap("error", e.getMessage());
    }
}
@SneakyThrows
public static void main(String[] args) {
    ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver();
    resolver.setMessageConverters(Collections.singletonList(new MappingJackson2HttpMessageConverter()));
    // 调用该方法,添加默认的参数解析器和返回值处理器
    resolver.afterPropertiesSet();

    MockHttpServletRequest request = new MockHttpServletRequest();
    MockHttpServletResponse response = new MockHttpServletResponse();
    HandlerMethod handlerMethod = new HandlerMethod(new Controller1(), Controller1.class.getMethod("foo"));
    Exception e = new ArithmeticException("除以零");
    resolver.resolveException(request, response, handlerMethod, e);
    System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));

    handlerMethod = new HandlerMethod(new Controller2(), Controller2.class.getMethod("foo"));
    ModelAndView modelAndView = resolver.resolveException(request, response, handlerMethod, e);
    System.out.println(modelAndView.getModel());
    System.out.println(modelAndView.getViewName());

    // 嵌套异常
    handlerMethod = new HandlerMethod(new Controller3(), Controller3.class.getMethod("foo"));
    e = new Exception("e1", new RuntimeException("e2", new IOException("e3")));
    resolver.resolveException(request, response, handlerMethod, e);
    System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));

    // 异常处理方法参数处理
    handlerMethod = new HandlerMethod(new Controller4(), Controller4.class.getMethod("foo"));
    e = new Exception("e4");
    resolver.resolveException(request, response, handlerMethod, e);
    System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
}

TomCat 异常处理

可以利用 @ExceptionHandler@ControllerAdvice 注解全局对控制器方法中抛出的异常进行处理,但针对诸如 filter 中不在控制器方法中的异常就变得无能为力了。
因此需要一个更上层的“异常处理者”,这个“异常处理者”就是 Tomcat 服务器。

错误页处理

首先将“老三样”利用配置类添加到 Spring 容器中,还要将 RequestMappingHandlerMappingRequestMappingHandlerAdapter 也添加到 Spring 容器中。
必要的控制器也不能少,控制器方法手动制造异常,但不提供使用 @ExceptionHandler 实现的异常处理方法,将产生的异常交由 Tomcat 处理:

@Configuration
public class WebConfig {
    @Bean
    public TomcatServletWebServerFactory servletWebServerFactory() {
        return new TomcatServletWebServerFactory();
    }

    @Bean
    public DispatcherServlet dispatcherServlet() {
        return new DispatcherServlet();
    }

    @Bean
    public DispatcherServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
        DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");
        registrationBean.setLoadOnStartup(1);
        return registrationBean;
    }

    @Bean
    public RequestMappingHandlerMapping requestMappingHandlerMapping() {
        // 解析 @RequestMapping
        return new RequestMappingHandlerMapping();
    }

    @Bean
    public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
        RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter();
        // 注意默认的 RequestMappingHandlerAdapter 不会带 jackson 转换器
        handlerAdapter.setMessageConverters(Collections.singletonList(new MappingJackson2HttpMessageConverter()));
        return handlerAdapter;
    }

    @Controller
    public static class MyController {
        @RequestMapping("test")
        public ModelAndView test() {
            int i = 1 / 0;
            return null;
        }
    }
}

在浏览器中访问 http://localhost:8080/test 地址:
Tomcat错误处理页.png显示 Tomcat 的错误处理页,并在页面中输出了错误信息。
Tomcat 默认提供的错误处理方式返回的是 HTML 格式的数据,但需要返回 JSON 格式的数据又该怎么自定义呢?
修改 Tomcat 默认的错误处理路径,并添加后置处理器进行注册:

/**
 * 修改了 Tomcat 服务器默认错误地址
 */
@Bean
public ErrorPageRegistrar errorPageRegistrar() {
    /*
     * ErrorPageRegistrar 由 SpringBoot 提供,TomcatServletWebServerFactory 也实现了该接口
     * 出现错误,会使用请求转发 forward 跳转到 error 地址
     */
    return webServerFactory -> webServerFactory.addErrorPages(new ErrorPage("/error"));
}

@Bean
public ErrorPageRegistrarBeanPostProcessor errorPageRegistrarBeanPostProcessor() {
    /*
     * 在 TomcatServletWebServerFactory 初始化完成前,获取容器中所有的 ErrorPageRegistrar
     * 并将这些 ErrorPageRegistrar 进行注册
     */
    return new ErrorPageRegistrarBeanPostProcessor();
}

重启程序,再次在浏览器中访问 http://localhost:8080/test,此时页面上不再显示 Tomcat 的默认错误处理页,而是产生了 404 错误。
这是因为整个程序中并没有名称为 error 的页面,或者为 /error 的请求路径。在控制器中添加请求路径为 /error 的控制器方法,该方法被 @ResponseBody 标记,最终返回 JSON 格式的数据:

@RequestMapping("/error")
@ResponseBody
public Map<String, Object> error(HttpServletRequest request) {
    // tomcat 会将异常对象存储到 request 作用域中,可以直接获取
    Throwable e = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
    return Collections.singletonMap("error", e.getMessage());
}

再次重启程序,控制台输出的路径映射信息多了一条:

映射路径: { [/error]}    方法信息: indi.mofan.a32.WebConfig$MyController#error(HttpServletRequest)
映射路径: { [/test]}    方法信息: indi.mofan.a32.WebConfig$MyController#test()

在浏览器中访问 http://localhost:8080/test
自定义Tomcat错误处理页.png

BasicErrorController

BasicErrorController 是由 SpringBoot 提供的类,它也是一个控制器:

@Controller
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {
    // --snip--
}

它的映射路径会先从配置文件中读取,在未进行任何配置的情况下,默认路径是 /error
向容器中添加 BasicErrorController,构造 BasicErrorController 时需要传递两个参数:

  • errorAttributes:错误属性,可以理解成封装的错误信息对象
  • errorProperties:也可以翻译成错误属性,用于对输出的错误信息进行配置

移除前文添加的 error() 控制器方法。
再次重启程序,控制台输出的路径映射信息为:

映射路径: { [/error]}    方法信息: org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
映射路径: { [/test]}    方法信息: indi.mofan.a32.WebConfig$MyController#test()
映射路径: { [/error], produces [text/html]}    方法信息: org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml(HttpServletRequest, HttpServletResponse)

路径映射信息多了两条,它们的请求路径一样,但根据不同的请求来源返回不同格式的数据。

使用接口测试工具访问

如果采用 Postman 等接口测试工具访问 http://localhost:8080/test 路径时,将返回 JSON 格式的数据,比如:

{
  "timestamp": 1674736682248,
  "status": 500,
  "error": "Internal Server Error",
  "path": "/test"
}

timestampstatus 等响应内容就是错误属性 errorAttributes 的中包含的内容。
返回的数据中并没有显示异常信息,可以通过配置文件进行配置:

server.error.include-exception=true

也可以在添加 BasicErrorController 到 Spring 容器中时,设置错误属性 errorProperties

@Bean
public BasicErrorController basicErrorController() {
    ErrorProperties errorProperties = new ErrorProperties();
    errorProperties.setIncludeException(true);
    return new BasicErrorController(new DefaultErrorAttributes(), errorProperties);
}
使用浏览器访问

如果使用浏览器访问 http://localhost:8080/test,又会回到“解放前”,显示与 Tomcat 的默认错误处理页相同的内容。
这是因为使用浏览器访问时,将调用 BasicErrorController 中的 errorHtml() 控制器方法:

@RequestMapping(
    produces = {"text/html"}
)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
    HttpStatus status = this.getStatus(request);
    Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
    response.setStatus(status.value());
    ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
    return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}

该方法返回 ModelAndView,并且在没有添加新的错误视图的情况下,尝试寻找视图名称为 error 的视图。
这里既没有添加新的错误视图,也没有名称为 error 的视图,因此最终又会交由 Tomcat 进行处理。
尝试向 Spring 容器中添加一个 View 视图,Bean 的名字 必须error

@Bean
public View error() {
    return (model, request, response) -> {
        System.out.println(model);
        response.setContentType("text/html;charset=utf-8");
        response.getWriter().print("<h3>服务器内部错误</h3>");
    };
}

为了能够在查找指定名称的视图时按照 View 类型的 Bean 的名称进行匹配,还需要添加一个解析器:

@Bean
public ViewResolver viewResolver() {
    // View 类型的 Bean 的名称即为视图名称
    return new BeanNameViewResolver();
}
使用BasicErrorController自定义error视图.png

MVC 处理流程

当浏览器发送一个请求 http://localhost:8080/hello 后,请求到达服务器,其处理流程是:

  1. 服务器提供了 DispatcherServlet,它使用的是标准 Servlet 技术
  • 路径:默认映射路径为 /,即会匹配到所有请求 URL,可作为请求的统一入口,也被称之为前控制器
    • jsp 不会匹配到 DispatcherServlet
    • 其它有路径的 Servlet 匹配优先级也高于 DispatcherServlet
  • 创建:在 Boot 中,由 DispatcherServletAutoConfiguration 这个自动配置类提供 DispatcherServlet 的 bean 及其注册有关 Bean:PixPin_2024-08-22_21-25-06.png
  • 初始化:DispatcherServlet 初始化时会优先到容器里寻找各种组件,作为它的成员变量
    • HandlerMapping,初始化时记录映射关系
    • HandlerAdapter,初始化时准备参数解析器、返回值处理器、消息转换器
    • HandlerExceptionResolver,初始化时准备参数解析器、返回值处理器、消息转换器
    • ViewResolver
  1. DispatcherServlet 会利用 RequestMappingHandlerMapping 查找控制器方法
  • 例如根据 /hello 路径找到 @RequestMapping("/hello") 对应的控制器方法
  • 控制器方法(连同控制器本身)会被封装为 HandlerMethod 对象,并结合匹配到的拦截器一起返回给 DispatcherServlet
  • HandlerMethod 和拦截器合在一起称为 HandlerExecutionChain(调用链)对象
  1. DispatcherServlet 接下来会:
  2. 调用拦截器的 preHandle 方法
  3. RequestMappingHandlerAdapter 调用 handle 方法,准备数据绑定工厂、模型工厂、ModelAndViewContainer、将 HandlerMethod 完善为 ServletInvocableHandlerMethod
    • @ControllerAdvice 全局增强点1️⃣:补充模型数据 到 ModelAndViewContainer
    • @ControllerAdvice 全局增强点2️⃣:补充自定义类型转换器(@InitBinder)
    • 使用 HandlerMethodArgumentResolver 准备参数
      • @ControllerAdvice 全局增强点3️⃣:RequestBody 增强
    • 调用 ServletInvocableHandlerMethod
    • 使用 HandlerMethodReturnValueHandler 处理返回值
      • @ControllerAdvice 全局增强点4️⃣:ResponseBody 增强
    • 根据 ModelAndViewContainer 获取 ModelAndView
      • 如果返回的 ModelAndView 为 null,不走第 4 步视图解析及渲染流程
      • 例如,有的返回值处理器(@ResponseBody)调用了 HttpMessageConverter 来将结果转换为 JSON,这时 ModelAndView 就为 null
      • 如果返回的 ModelAndView 不为 null,会在第 4 步走视图解析及渲染流程
  4. 调用拦截器的 postHandle 方法
  5. 处理异常或视图渲染
    • 如果 1~3 出现异常,走 ExceptionHandlerExceptionResolver 处理异常流程
      • @ControllerAdvice 全局增强点5️⃣:@ExceptionHandler 异常处理
    • 正常,走视图解析及渲染流程
  6. 调用拦截器的 afterCompletion 方法
image-20240313195123509

此作者没有提供个人介绍
最后更新于 2024-08-22