SpringBoot 从入门到光头 第五章 Web 开发


SpringBoot 从入门到光头 —— 第五章 Web 开发


1. SpringMVC 自动配置概览

*Spring Boot provides auto-configuration for Spring MVC that works well with most applications.*(大多场景我们都无需自定义配置

The auto-configuration adds the following features on top of Spring’s defaults:

  • Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
    • 内容协商视图解析器和 BeanName 视图解析器
  • Support for serving static resources, including support for WebJars (covered later in this document).
    • 静态资源(包括webjars)
  • Automatic registration of Converter, GenericConverter, and Formatter beans.
    • 自动注册 ConverterGenericConverterFormatter
  • Support for HttpMessageConverters (covered later in this document).
    • 支持 HttpMessageConverters (后来我们配合内容协商理解原理)
  • Automatic registration of MessageCodesResolver (covered later in this document).
    • 自动注册 MessageCodesResolver (国际化用)
  • Static index.html support.
    • 静态 index.html 页支持
  • Cutom Favicon support (covered later in this document).
    • 自定义 Favicon
  • Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).
    • 自动使用 ConfigurableWebBindingInitializer ,(DataBinder负责将请求数据绑定到JavaBean上)

If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc.

不用 @EnableWebMvc 注解。使用 @Configuration + WebMvcConfigurer 自定义规则

If you want to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, and still keep the Spring Boot MVC customizations, you can declare a bean of type WebMvcRegistrations and use it to provide custom instances of those components.

声明 WebMvcRegistrations 改变默认底层组件

If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc, or alternatively add your own @Configuration-annotated DelegatingWebMvcConfiguration as described in the Javadoc of @EnableWebMvc.

使用 @EnableWebMvc + @Configuration+DelegatingWebMvcConfiguration 全面接管SpringMVC

2. 简单功能分析

2.1. 静态资源访问

1. 静态资源目录

只要静态资源放在 类路径 下:/static(或者是 /public/resources/META_INF/resources

访问:当前项目根路径/ + 静态资源名

原理:静态映射 /**

请求进来之后,先去找 Controller 看能不能处理。不能处理的所有请求都交给静态资源处理器。静态资源也找不到则响应 404 页面

改变默认的静态资源路径方法:

application.yaml

spring:
  mvc:
    static-path-pattern: /res/**

 web:
    resources:
      static-locations: [classpath:/haha/]

2. 静态资源访问前缀

默认情况下是没有前缀的

如何配置访问前缀:

application.yaml

spring:
  mvc:
    static-path-pattern: /res/**

访问路径: 当前项目 + static--path-pattern + 静态资源名 = 静态资源文件下查找

3. webjar

自动映射 /webjars/jquery/3.5.1/jquery.js/**

https://www.webjars.org/

pom.xml

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.5.1</version>
</dependency>

访问地址:http://localhost:8080/webjars/jquery/3.5.1/jquery.jswebjars 后面的地址需要按照依赖里面的包路径)

2.2. 欢迎页支持

  • 静态资源路径下 index.html

    • 可以配置静态资源路径

    • 但是不可以配置静态资源的访问前缀。否则导致 index.html 不能被默认访问

      spring:
      #  mvc:
      #    static-path-pattern: /res/**
      # 上面的配置会导致 welcome.page 失效
      
        web:
          resources:
            static-locations: [classpath:/haha/]
      
  • Controller 能处理 /index 请求

2.3. 自定义 Favicon

favicon.ico 放在静态资源目录下即可

注意⚠️:static-path-pattern: 配置也会导致 Favicon 功能失效

spring:
#  mvc:
#    static-path-pattern: /res/**
# 上面的配置会导致 Favicon 失效

2.4. 静态资源配置原理

  • SpringBoot 启动默认加载 xxxAutoConfiguration 类(自动配置类)

  • SpringMVC 功能的自动配置类 WebMvcAutoConfiguration,生效条件:

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnWebApplication(type = Type.SERVLET)
    @ConditionalOnClass(&#123; Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class &#125;)
    @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
    @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
    @AutoConfigureAfter(&#123; DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
            ValidationAutoConfiguration.class &#125;)
    public class WebMvcAutoConfiguration &#123;
    
    &#125;
    
  • 对容器的配置如下:

    @SuppressWarnings("deprecation")
    @Configuration(proxyBeanMethods = false)
    @Import(EnableWebMvcConfiguration.class)
    @EnableConfigurationProperties(&#123; WebMvcProperties.class,
            org.springframework.boot.autoconfigure.web.ResourceProperties.class, WebProperties.class &#125;)
    @Order(0)
    public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware &#123;
            
    &#125;
    
  • 配置文件的相关属性和什么进行了绑定:

    • WebMvcPropertiesspring.mvc
    • ResourcePropertiesspring.resources

1. 配置类只有一个有参构造器

org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration

public WebMvcAutoConfigurationAdapter(
        org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties,
        WebProperties webProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory,
        ObjectProvider<HttpMessageConverters> messageConvertersProvider,
        ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
        ObjectProvider<DispatcherServletPath> dispatcherServletPath,
        ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) &#123;
    this.resourceProperties = resourceProperties.hasBeenCustomized() ? resourceProperties
            : webProperties.getResources();
    this.mvcProperties = mvcProperties;
    this.beanFactory = beanFactory;
    this.messageConvertersProvider = messageConvertersProvider;
    this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
    this.dispatcherServletPath = dispatcherServletPath;
    this.servletRegistrations = servletRegistrations;
    this.mvcProperties.checkConfiguration();
&#125;
  • 有参构造器所有参数的值都会从容器中确定
  • ResourceProperties resourceProperties:获取和 spring.resources 绑定的所有值的对象
  • ListableBeanFactory beanFactory:Spring 的 beanFactory
  • HttpMessageConverters:找到所有的 HttpMessageConverter
  • ResourceHandlerRegistrationCustomizer:找到资源处理器的自定义器
  • DispatcherServletPathDispatcherServlet 允许处理的路径
  • ServletRegistrationBean<?>:给应用注册原生的 Servlet、Listener、Filter…

2. 资源处理的默认规则

org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) &#123;
    if (!this.resourceProperties.isAddMappings()) &#123;
        logger.debug("Default resource handling disabled");
        return;
    &#125;
  // 新版本使用 Lambda 表达式简化代码
    addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
    addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> &#123;
            registration.addResourceLocations(this.resourceProperties.getStaticLocations());
            if (this.servletContext != null) &#123;
                ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION);
                registration.addResourceLocations(resource);
            &#125;
    &#125;);
&#125;
  • 以下规则可以禁用所有静态资源加载:

    application.yaml

    spring:
      resources:
        add-mappings: false   # 禁用所有静态资源规则
    
  • webjars 的规则:

    addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
    
  • 静态资源的四个默认位置:

    org.springframework.boot.autoconfigure.web.WebProperties

    public static class Resources &#123;
            private static final String[] CLASSPATH_RESOURCE_LOCATIONS = &#123; "classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/" &#125;;
    &#125;
    

3.欢迎页的处理规则

org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration

@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
        FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) &#123;
    WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
            new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
            this.mvcProperties.getStaticPathPattern());
    welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
    welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
    return welcomePageHandlerMapping;
&#125;
  • HandlerMapping处理器映射器,保存了每一个 Handler 能处理那些请求

  • 要使用欢迎页默认静态路径必须是 /** 的原因:

    org.springframework.boot.autoconfigure.web.servlet.WelcomePageHandlerMapping

    WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
            ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) &#123;
        if (welcomePage != null && "/**".equals(staticPathPattern)) &#123;
            logger.info("Adding welcome page: " + welcomePage);
            setRootViewName("forward:index.html");
        &#125;
        else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) &#123;
            logger.info("Adding welcome page template: index");
            setRootViewName("index");
        &#125;
    &#125;
    
    • setRootViewName("index");:调用能够处理 /index 请求的 Controller

4. Favicon

浏览器会发送 /favicon.ico 请求获取到图标,在整个 session 期间不再获取

3. 请求参数处理

3.1. 请求映射

1. REST 的使用与原理

  • @xxxMapping

  • REST 风格支持(使用 HTTP 请求方式动词来表示对资源的操作)

    • 以前: /getUser 获取用户、/deleteUser 删除用户、/updateUser 修改用户、/addUser 添加用户

    • 现在: /user + 请求方式: GET 获取用户、DELETE 删除用户、PUT 修改用户、POST 添加用户

    • 核心 Filter: HiddenHttpMethodFilter

      • 用法(以 PUT 为例): 在表单中:method=post,隐藏域:_method=put

        <form action="/user" method="post">
            <input name="_method" type="hidden" value="PUT"/>
            <input value="REST-PUT 提交" type="submit"/>
        </form>
        
      • 注意⚠️:SpringBoot 中需要手动配置开启 REST 风格:

        spring:
          mvc:
            hiddenmethod:
              filter:
                enabled: true
        
      • 需要手动开启配置的原因:

        org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration

        @Bean
        @ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
        @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
        public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() &#123;
          return new OrderedHiddenHttpMethodFilter();
        &#125;
        
    • 使用示例:

      com.yourname.boot.controller.HelloController

      /**
       * @author gregPerlinLi
       * @since 2021-10-26
       */
      @RestController
      public class HelloController &#123;
          @RequestMapping("/Logo.jpg")
          public String hello() &#123;
              return "aaaa";
          &#125;
          @RequestMapping(value = "/user",method = RequestMethod.GET)
          public String getUser()&#123;
              return "GET-张三";
          &#125;
          @RequestMapping(value = "/user",method = RequestMethod.POST)
          public String saveUser()&#123;
              return "POST-张三";
          &#125;
          @RequestMapping(value = "/user",method = RequestMethod.PUT)
          public String putUser()&#123;
              return "PUT-张三";
          &#125;
          @RequestMapping(value = "/user",method = RequestMethod.DELETE)
          public String deleteUser()&#123;
              return "DELETE-张三";
          &#125;
      &#125;
      

Rest原理(表单提交要使用REST的时候)

  • 表单提交会带上 _method=PUT
  • 请求过来被 HiddenHttpMethodFilter 拦截
    • 请求是否正常,并且是POST
    • 获取到 _method 的值。
    • 兼容以下请求:PUTDELETEPATCH
    • 原生 Request(post),包装模式的 requestWrapper 重写了getMethod 方法,返回的是传入的值。
    • 过滤器链放行的时候使用 wrapper 作为 Request 的对象进行放行。以后的方法调用 getMethod 就是调用 requestWrapper
  • Rest 使用客户端工具
    • 在使用如 PostMan、Apifox 等客户端直接发送 PUTDELETE 等方式请求时,无需 Filter(也就意味着无需在配置文件中手动开启配置)

Handler 针对 REST 功能的简化:

com.yourname.boot.controller.HelloController

/**
 * @author gregPerlinLi
 * @since 2021-10-26
 */
@RestController
public class HelloController &#123;
    @RequestMapping(value = "/Logo.jpg")
    public String hello() &#123;
        return "aaaa";
    &#125;
    @GetMapping(value = "/user")
    public String getUser()&#123;
        return "GET-张三";
    &#125;
    @PostMapping(value = "/user")
    public String saveUser()&#123;
        return "POST-张三";
    &#125;
    @PutMapping(value = "/user")
    public String putUser()&#123;
        return "PUT-张三";
    &#125;
    @DeleteMapping(value = "/user")
    public String deleteUser()&#123;
        return "DELETE-张三";
    &#125;
&#125;

如何将 _method 自定义成自己想要的名字:

com.yourname.boot.config.WebConfig

/**
 * Customize filter
 * 
 * @author gregPerlinLi
 * @since 2021-10-27
 */
@Configuration(proxyBeanMethods = false)
public class WebConfig &#123;
    @Bean
    public HiddenHttpMethodFilter hiddenHttpMethodFilter()&#123;
        HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
        methodFilter.setMethodParam("_m");
        return methodFilter;
    &#125;
&#125;

2. 请求映射原理

ServletFamily

HttpServlet.doGet()FrameworkServlet.processRequestFrameworkServlet.doServer()DispatcherServlet.doService()DispatcherServlet.doDispatch()

对 SpringMVC 的功能分析都应从 org.springframework.web.servlet.DispatcherServlet.doDispatch() 方法开始

@SuppressWarnings("deprecation")
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception &#123;
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;

    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    try &#123;
        ModelAndView mv = null;
        Exception dispatchException = null;

        try &#123;
            processedRequest = checkMultipart(request);
            multipartRequestParsed = (processedRequest != request);

            // Determine handler for the current request.
            mappedHandler = getHandler(processedRequest);
            if (mappedHandler == null) &#123;
                noHandlerFound(processedRequest, response);
                return;
            &#125;

            // Determine handler adapter for the current request.
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

            // Process last-modified header, if supported by the handler.
            String method = request.getMethod();
            boolean isGet = HttpMethod.GET.matches(method);
            if (isGet || HttpMethod.HEAD.matches(method)) &#123;
                long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) &#123;
                    return;
                &#125;
            &#125;

            if (!mappedHandler.applyPreHandle(processedRequest, response)) &#123;
                return;
            &#125;

            // Actually invoke the handler.
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

            if (asyncManager.isConcurrentHandlingStarted()) &#123;
                return;
            &#125;

            applyDefaultViewName(processedRequest, mv);
            mappedHandler.applyPostHandle(processedRequest, response, mv);
        &#125;
        catch (Exception ex) &#123;
            dispatchException = ex;
        &#125;
        catch (Throwable err) &#123;
            // As of 4.3, we're processing Errors thrown from handler methods as well,
            // making them available for @ExceptionHandler methods and other scenarios.
            dispatchException = new NestedServletException("Handler dispatch failed", err);
        &#125;
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    &#125;
    catch (Exception ex) &#123;
        triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
    &#125;
    catch (Throwable err) &#123;
        triggerAfterCompletion(processedRequest, response, mappedHandler,
                new NestedServletException("Handler processing failed", err));
    &#125;
    finally &#123;
        if (asyncManager.isConcurrentHandlingStarted()) &#123;
            // Instead of postHandle and afterCompletion
            if (mappedHandler != null) &#123;
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
            &#125;
        &#125;
        else &#123;
            // Clean up any resources used by a multipart request.
            if (multipartRequestParsed) &#123;
                cleanupMultipart(processedRequest);
            &#125;
        &#125;
    &#125;
&#125;
  • mappedHandler = getHandler(processedRequest);:找到当前请求使用哪个 Handler(Controller的方法)处理

  • HandlerMapping 处理器映射

    DispatcherServletHandlerMapping
    • RequestMappingHandlerMapping 保存了所有的 @RequestMapping 和 Handler 映射规则

      MappingRegistry

所有的请求都保存在 HandlerMapping

  • SpringBoot 自动配置欢迎页的 WelcomePageHandlerMapping,当访问 / 的时候能访问到 index.html

  • SpringBoot 自动配置了默认 的 RequestMappingHandlerMapping

  • 请求进来,挨个尝试所有的 HandlerMapping 看是否有请求信息

    • 如果有就找到这个请求对应的 Handler
    • 如果没有就是下一个 HandlerMapping
  • 我们需要一些自定义的映射处理,我们也可以自己给容器中放 HandlerMapping,自定义 HandlerMapping

    protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception &#123;
        if (this.handlerMappings != null) &#123;
            for (HandlerMapping mapping : this.handlerMappings) &#123;
                HandlerExecutionChain handler = mapping.getHandler(request);
                if (handler != null) &#123;
                    return handler;
                &#125;
            &#125;
        &#125;
        return null;
    &#125;
    

3.2. 普通参数与基本注解

1. 使用注解

  • @PathVariable:路径变量
  • @RequestHeader:获取请求头
  • @RequestParam:获取请求参数
  • @CookieValue:获取 cookie 值
  • @RequestBody:获取请求体(用于 POST 请求)
  • @equestAttribute:获取 Request 域属性
  • @MatrixVariable:矩阵变量(常用于在无法使用 Cookie 时使用 URL 重写方法在矩阵变量中传递参数)

示例代码:

index.html

<!DOCTYPE html>
<html lang="en">
        <head>
                <meta charset="UTF-8">
            <title>Index</title>
            <style type="text/css">
                body &#123;
                    font-family: Futura, "PingFang SC", Helvetica, Arial, sans-serif;
                &#125;
            </style>
        </head>
        <body>
                <h1>Welcome to index</h1>
                测试REST风格;
                <form action="/user" method="get">
                    <input value="REST-GET 提交" type="submit"/>
                </form>
                <form action="/user" method="post">
                    <input value="REST-POST 提交" type="submit"/>
                </form>
                <form action="/user" method="post">
                    <input name="_method" type="hidden" value="delete"/>
                    <input name="_m" type="hidden" value="delete"/>
                    <input value="REST-DELETE 提交" type="submit"/>
                </form>
                <form action="/user" method="post">
                    <input name="_method" type="hidden" value="PUT"/>
                    <input value="REST-PUT 提交" type="submit"/>
                </form>
                <hr/>
                测试基本注解:
                <ul>
                    <a href="car/3/owner/lisi?age=18&inters=basketball&inters=game">car/&#123;id&#125;/owner/&#123;username&#125;</a>
                    <li>@PathVariable(路径变量)</li>
                    <li>@RequestHeader(获取请求头)</li>
                    <li>@RequestParam(获取请求参数)</li>
                    <li>@CookieValue(获取cookie值)</li>
                    <li>@RequestBody(获取请求体[POST])</li>
                
                    <li>@RequestAttribute(获取request域属性)</li>
                    <li>@MatrixVariable(矩阵变量)</li>
                </ul>
                /cars/&#123;path&#125;?xxx=xxx&aaa=ccc queryString 查询字符串。@RequestParam;<br/>
                /cars/sell;low=34;brand=byd,audi,yd  ;矩阵变量 <br/>
                页面开发,cookie禁用了,session里面的内容怎么使用;
                session.set(a,b)---> jsessionid ---> cookie ----> 每次发请求携带。
                url重写:/abc;jsesssionid=xxxx 把cookie的值使用矩阵变量的方式进行传递.
                /boss/1/2
                /boss/1;age=20/2;age=20
                <a href="/cars/sell;low=34;brand=byd,audi,yd">@MatrixVariable(矩阵变量)</a>
                <a href="/cars/sell;low=34;brand=byd;brand=audi;brand=yd">@MatrixVariable(矩阵变量)</a>
                <a href="/boss/1;age=20/2;age=10">@MatrixVariable(矩阵变量)/boss/&#123;bossId&#125;/&#123;empId&#125;</a>
                <br/>
                <form action="/save" method="post">
                    测试@RequestBody获取数据 <br/>
                    用户名:<input name="userName"/> <br>
                    邮箱:<input name="email"/>
                    <input type="submit" value="提交"/>
                </form>
                <ol>
                    <li>矩阵变量需要在SpringBoot中手动开启</li>
                    <li>根据RFC3986的规范,矩阵变量应当绑定在路径变量中!</li>
                    <li>若是有多个矩阵变量,应当使用英文符号;进行分隔。</li>
                    <li>若是一个矩阵变量有多个值,应当使用英文符号,进行分隔,或之命名多个重复的key即可。</li>
                    <li>如:/cars/sell;low=34;brand=byd,audi,yd</li>
                </ol>
                <br>
        </body>
</html>

com.yourname.boot.config.WebConfig

/**
 * Customize filter
 *
 * @author gregPerlinLi
 * @since 2021-10-27
 */
@Configuration(proxyBeanMethods = false)
public class WebConfig &#123;
    @Bean
    public HiddenHttpMethodFilter hiddenHttpMethodFilter()&#123;
        HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
        methodFilter.setMethodParam("_m");
        return methodFilter;
    &#125;
        /**
         * Enable matrix variable
         */
    @Bean
    public WebMvcConfigurer webMvcConfigurer() &#123;
        return new WebMvcConfigurer() &#123;
            @Override
            public void configurePathMatch(PathMatchConfigurer configurer) &#123;
                UrlPathHelper urlPathHelper = new UrlPathHelper();
                // Do not remove content after semicolon
                urlPathHelper.setRemoveSemicolonContent(false);
                configurer.setUrlPathHelper(urlPathHelper);
            &#125;
        &#125;;
    &#125;
&#125;

com.yourname.boot.controller.ParameterTestController

/**
 * @author gregPerlinLi
 * @since 2021-10-27
 */
@RestController
public class ParameterTestController &#123;
    @GetMapping(value = "/car/&#123;id&#125;/owner/&#123;username&#125;")
    public Map getCar(@PathVariable("id") Integer id,
                                      @PathVariable("username") String username,
                                      @PathVariable Map<String, String> pv,
                                      @RequestHeader("User-Agent") String userAgent,
                                      @RequestHeader Map<String, String> headers,
                                      @RequestParam("age") Integer age,
                                      @RequestParam("inters") List<String> inters,
                                      @RequestParam Map<String, String> params,
                                      @CookieValue("Idea-a5f91748") String idea,
                                      @CookieValue("Idea-a5f91748") Cookie cookie) &#123;
        Map<String, Object> map = new HashMap<>(1000);
        map.put("id", id);
        map.put("name", username);
        map.put("pv", pv);
        map.put("userAgent", userAgent);
        map.put("headers", headers);
        map.put("age", age);
        map.put("inters", inters);
        map.put("params", params);
        map.put("Idea-a5f91748", idea);
        System.out.println(cookie.getName() + " ===>> " + cookie.getValue());
        return map;
    &#125;

    @PostMapping(value = "/save")
    public Map<String, Object> postMethod(@RequestBody String content) &#123;
        Map<String, Object> map = new HashMap<>(1000);
        map.put("content", content);
        return map;
    &#125;
    /**
     * 1. Matrix grammar: /cars/sell;low=34;brand=byd,audi,yd<br/>
     * 2. SpringBoot disables matrix variables by default and needs to be enabled manually<br/>
     * 3. The matrix variable must have a URL path variable to be resolved
     *
     * @param low low
     * @param brand brand
     * @return map
     */
    @GetMapping(value = "/cars/&#123;path&#125;")
    public Map carsSell(@MatrixVariable("low") Integer low,
                        @MatrixVariable("brand") List<String> brand,
                        @PathVariable String path) &#123;
        Map<String, Object> map = new HashMap<>(1000);
        map.put("low", low);
        map.put("brand", brand);
        map.put("path", path);
        return map;
    &#125;
    @GetMapping(value = "/boss/&#123;bossId&#125;/&#123;empId&#125;")
    public Map boss(@MatrixVariable(value = "age", pathVar = "bossId") Integer bossAge,
                    @MatrixVariable(value = "age", pathVar = "empId") Integer empAge) &#123;
        Map<String, Object> map = new HashMap<>(1000);
        map.put("bossAge", bossAge);
        map.put("empAge", empAge);
        return map;
    &#125;
&#125;

com.yourname.boot.controller.RequestController

/**
 * @author gregPerlinLi
 * @since 2021-10-27
 */
@Controller
public class RequestController &#123;
    @GetMapping(value = "/goto")
    public String goToString(HttpServletRequest request) &#123;

        request.setAttribute("msg", "Success!!!");
        request.setAttribute("code", 200);
        return "forward:/success";
    &#125;
    @ResponseBody
    @GetMapping(value = "/success")
    public Map success(@RequestAttribute("msg") String msg,
                       @RequestAttribute("code") Integer code,
                       HttpServletRequest request) &#123;
        Object msg1 = request.getAttribute("msg");
        Map<String, Object> map = new HashMap<>(1000);
        map.put("reqMethod_msg", msg1);
        map.put("annotation_msg", msg);
        return map;
    &#125;
&#125;

2. Servlet API

  • WebRequest
  • ServletRequest
  • MultipartRequest
  • HttpSession
  • javax.servlet.http.PushBuilder
  • Principal
  • InputStream
  • Reader
  • HttpMethod
  • Locale
  • TimeZone
  • ZoneId

ServletRequestMethodArgumentResolver 可以解析以上部分的参数

org.springframework.web.servlet.mvc.method.annotation.ServletRequestMethodArgumentResolver.supportsParameter()

@Override
public boolean supportsParameter(MethodParameter parameter) &#123;
    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);
&#125;

3. 复杂函数

  • Map
  • Modelmapmodel 里面的数据会被放在 Request 的请求域 request.setAttribute
  • Errors / BindingResult
  • RedirectAttributes( 重定向携带数据)
  • ServletResponse( Response )
  • SessionStatus
  • UriComponentsBuilder
  • ServletUriComponentsBuilder

com.yourname.boot.controller.RequestController.testParam()

@GetMapping(value = "/params")
public String testParam(Map<String, Object> map,
                        Model model,
                        HttpServletRequest request,
                        HttpServletResponse response) &#123;
&#125;

上面的 MapModelHttpServletRequest 都可以给 Request 域中存放数据

Map / Model 类型的参数会返回 macContainer.getModel()BindingAwareModelMap 来获取值

org.springframework.web.method.support.ModelAndViewContainer

public class ModelAndViewContainer &#123;
  private final ModelMap defaultModel = new BindingAwareModelMap();
&#125;

由下图可知,Map(0)和 Model (1)类型都是由同一个对象(BindingAwareModelMap)来获取的

BindingAwareModelMap

4. 自定义对象参数

可以自动类型转换与格式化,可以级联封装

示例代码:

index.html

<!DOCTYPE html>
<html lang="en">
        <head>
            <meta charset="UTF-8">
            <title>Index</title>
            <style type="text/css">
                body &#123;
                    font-family: Futura, "PingFang SC", Helvetica, Arial, sans-serif;
                &#125;
            </style>
        </head>
        <body>
                <h1>Welcome to index</h1>
                测试封装POJO;
                <form action="/saveuser" method="post">
                    姓名: <input name="userName" value="zhangsan"/> <br/>
                    年龄: <input name="age" value="18"/> <br/>
                    生日: <input name="birth" value="2019/12/10"/> <br/>
                    宠物姓名:<input name="pet.name" value="阿猫"/><br/>
                    宠物年龄:<input name="pet.age" value="5"/>
                    <input type="submit" value="保存"/>
                </form>
        <br>
        </body>
</html>

com.yourname.boot.bean.Person

/**
 *     姓名: <input name="userName"/> <br/>
 *     年龄: <input name="age"/> <br/>
 *     生日: <input name="birth"/> <br/>
 *     宠物姓名:<input name="pet.name"/><br/>
 *     宠物年龄:<input name="pet.age"/>
 */
@Data
public class Person &#123;
    private String userName;
    private Integer age;
    private Date birth;
    private Pet pet;
&#125;

com.yourname.boot.bean.Pet

@Data
public class Pet &#123;
    private String name;
    private String age;
&#125;

com.yourname.boot.controller.ParameterController

/**
 * @author gregPerlinLi
 * @since 2021-10-27
 */
@RestController
public class ParameterTestController &#123;
    /**
     * Data binding: The data submitted by the page (GET, POST) can be bound to the object
     *
     * @param person person
     * @return person
     */
    @PostMapping(value = "/saveuser")
    public Person saveUser(Person person) &#123;
        return person;
    &#125;
&#125;

3.3 POJO 封装过程

  • ServletModelAttributeMethodProcessor

    ServletModelAttributeMethodProcessor

    • HandlerMethodReturnValueHandler:处理返回值
    • HandlerMethodArgumentResolver:处理参数

3.4. 参数处理原理

  • HandlerMapping 中找到能处理请求的 Handler(Controller.method()
  • 为当前 Handler 找一个适配器 HandlerAdapterRequestMappingHandlerAdapter
  • 适配器执行目标方法并确定方法参数的每一个值

1. HandlerAdapter

HandlerAdapter
  • 0支持方法上标注 @RequestMapping 的适配器
  • 1支持函数式编程的适配器

2. 执行目标方法

org.springframework.web.servlet.DispatcherServlet.doDispatch()

// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal()

// Execute target method
mav = invokeHandlerMethod(request, response, handlerMethod);

org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle()

Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);

org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest()

// 获取方法参数值
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);

3. 参数解析器(HandlerMethodArgumentResolver

确定我们将要执行的目标方法的每一个值

SpringMVC 目标方法能写多少种参数类型,取决于参数解析器

ArgumentsResolvers

参数解析器的接口设计:

HandlerMethodArgumentResolverDesign
  • 判断当前解析器是否支持解析这种参数
  • 支持解析就调用 resolveArgument

4. 返回值处理器

ReturnValueHandlers

5. 如何确定目标方法每一个参数值

org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues()

/**
 * Get the method argument values for the current request, checking the provided
 * argument values and falling back to the configured argument resolvers.
 * <p>The resulting array will be passed into &#123;@link #doInvoke&#125;.
 * @since 5.1.2
 */
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
        Object... providedArgs) throws Exception &#123;

    MethodParameter[] parameters = getMethodParameters();
    if (ObjectUtils.isEmpty(parameters)) &#123;
        return EMPTY_ARGS;
    &#125;

    Object[] args = new Object[parameters.length];
    for (int i = 0; i < parameters.length; i++) &#123;
        MethodParameter parameter = parameters[i];
        parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
        args[i] = findProvidedArgument(parameter, providedArgs);
        if (args[i] != null) &#123;
            continue;
        &#125;
        if (!this.resolvers.supportsParameter(parameter)) &#123;
            throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
        &#125;
        try &#123;
            args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
        &#125;
        catch (Exception ex) &#123;
            // Leave stack trace for later, exception may actually be resolved and handled...
            if (logger.isDebugEnabled()) &#123;
                String exMsg = ex.getMessage();
                if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) &#123;
                    logger.debug(formatArgumentError(parameter, exMsg));
                &#125;
            &#125;
            throw ex;
        &#125;
    &#125;
    return args;
&#125;
5.1. 逐个判断参数解析器那个支持解析此参数

org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.getArgumentResolver()

/**
 * Find a registered &#123;@link HandlerMethodArgumentResolver&#125; that supports
 * the given method parameter.
 */
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) &#123;
    HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
    if (result == null) &#123;
        for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) &#123;
            if (resolver.supportsParameter(parameter)) &#123;
                result = resolver;
                this.argumentResolverCache.put(parameter, result);
                break;
            &#125;
        &#125;
    &#125;
    return result;
&#125;
5.2. 解析这个参数的值

调用各自 HandlerMethodArgumentResolverresolveArgument() 方法即可

5.3. 自定义类型参数封装 POJO

ServletModelAttributeMethodProcessor 参数处理器支持封装

判断是否为简单类型

org.springframework.beans.BeanUtils.isSimpleValueType()

/**
 * Check if the given type represents a "simple" value type: a primitive or
 * primitive wrapper, an enum, a String or other CharSequence, a Number, a
 * Date, a Temporal, a URI, a URL, a Locale, or a Class.
 * <p>&#123;@code Void&#125; and &#123;@code void&#125; are not considered simple value types.
 * @param type the type to check
 * @return whether the given type represents a "simple" value type
 * @see #isSimpleProperty(Class)
 */
public static boolean isSimpleValueType(Class<?> type) &#123;
    return (Void.class != type && void.class != type &&
            (ClassUtils.isPrimitiveOrWrapper(type) ||
            Enum.class.isAssignableFrom(type) ||
            CharSequence.class.isAssignableFrom(type) ||
            Number.class.isAssignableFrom(type) ||
            Date.class.isAssignableFrom(type) ||
            Temporal.class.isAssignableFrom(type) ||
            URI.class == type ||
            URL.class == type ||
            Locale.class == type ||
            Class.class == type));
&#125;

org.springframework.web.method.annotation.ModelAttributeMethodProcessor.resolveArgument()

/**
 * Resolve the argument from the model or if not found instantiate it with
 * its default if it is available. The model attribute is then populated
 * with request values via data binding and optionally validated
 * if &#123;@code @java.validation.Valid&#125; is present on the argument.
 * @throws BindException if data binding and validation result in an error
 * and the next method parameter is not of type &#123;@link Errors&#125;
 * @throws Exception if WebDataBinder initialization fails
 */
@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
        NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception &#123;

    Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
    Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");

    String name = ModelFactory.getNameForParameter(parameter);
    ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
    if (ann != null) &#123;
        mavContainer.setBinding(name, ann.binding());
    &#125;

    Object attribute = null;
    BindingResult bindingResult = null;

    if (mavContainer.containsAttribute(name)) &#123;
        attribute = mavContainer.getModel().get(name);
    &#125;
    else &#123;
        // Create attribute instance
        try &#123;
            attribute = createAttribute(name, parameter, binderFactory, webRequest);
        &#125;
        catch (BindException ex) &#123;
            if (isBindExceptionRequired(parameter)) &#123;
                // No BindingResult parameter -> fail with BindException
                throw ex;
            &#125;
            // Otherwise, expose null/empty value and associated BindingResult
            if (parameter.getParameterType() == Optional.class) &#123;
                attribute = Optional.empty();
            &#125;
            else &#123;
                attribute = ex.getTarget();
            &#125;
            bindingResult = ex.getBindingResult();
        &#125;
    &#125;

    if (bindingResult == null) &#123;
        // Bean property binding and validation;
        // skipped in case of binding failure on construction.
        WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
        if (binder.getTarget() != null) &#123;
            if (!mavContainer.isBindingDisabled(name)) &#123;
                bindRequestParameters(binder, webRequest);
            &#125;
            validateIfApplicable(binder, parameter);
            if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) &#123;
                throw new BindException(binder.getBindingResult());
            &#125;
        &#125;
        // Value type adaptation, also covering java.util.Optional
        if (!parameter.getParameterType().isInstance(attribute)) &#123;
            attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
        &#125;
        bindingResult = binder.getBindingResult();
    &#125;

    // Add resolved attribute and BindingResult at the end of the model
    Map<String, Object> bindingResultModel = bindingResult.getModel();
    mavContainer.removeAttributes(bindingResultModel);
    mavContainer.addAllAttributes(bindingResultModel);

    return attribute;
&#125;
  • 创建 Web 数据绑定器:

    WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
    
  • WebDataBinder 作用: 将请求参数的值绑定到指定的 JavaBean 中

  • WebDataBinder 利用其中的 Convers 将请求数据转换成指定的数据类型,然后再次封装到 Java Bean 中

    Binder

GenericConverterService 在设置每一个值的时候,找其中所有的 Converter 哪个可以将这个数据(Request 带来的参数的字符串)转换到指定的类型(JavaBean 中定义的类型)

org.springframework.core.convert.support.GenericConversionService.find()

/**
 * Find a &#123;@link GenericConverter&#125; given a source and target type.
 * <p>This method will attempt to match all possible converters by working
 * through the class and interface hierarchy of the types.
 * @param sourceType the source type
 * @param targetType the target type
 * @return a matching &#123;@link GenericConverter&#125;, or &#123;@code null&#125; if none found
 */
@Nullable
public GenericConverter find(TypeDescriptor sourceType, TypeDescriptor targetType) &#123;
    // Search the full type hierarchy
    List<Class<?>> sourceCandidates = getClassHierarchy(sourceType.getType());
    List<Class<?>> targetCandidates = getClassHierarchy(targetType.getType());
    for (Class<?> sourceCandidate : sourceCandidates) &#123;
        for (Class<?> targetCandidate : targetCandidates) &#123;
            ConvertiblePair convertiblePair = new ConvertiblePair(sourceCandidate, targetCandidate);
            GenericConverter converter = getRegisteredConverter(sourceType, targetType, convertiblePair);
            if (converter != null) &#123;
                return converter;
            &#125;
        &#125;
    &#125;
    return null;
&#125;

Converters

之后我们可以给 WebDataBinder 中定义自己的 Converter 从而实现将任意类型转换为自己想要的类型:

private static final class StringToNumber<T extends Number> implements Converter<String T> &#123;
  
&#125;

Converter 的总接口:

org.springframework.core.convert.converter.Converter

/**
 * A converter converts a source object of type &#123;@code S&#125; to a target of type &#123;@code T&#125;.
 *
 * <p>Implementations of this interface are thread-safe and can be shared.
 *
 * <p>Implementations may additionally implement &#123;@link ConditionalConverter&#125;.
 *
 * @author Keith Donald
 * @author Josh Cummings
 * @since 3.0
 * @param <S> the source type
 * @param <T> the target type
 */
@FunctionalInterface
public interface Converter<S, T> &#123;

    /**
     * Convert the source object of type &#123;@code S&#125; to target type &#123;@code T&#125;.
     * @param source the source object to convert, which must be an instance of &#123;@code S&#125; (never &#123;@code null&#125;)
     * @return the converted object, which must be an instance of &#123;@code T&#125; (potentially &#123;@code null&#125;)
     * @throws IllegalArgumentException if the source cannot be converted to the desired target type
     */
    @Nullable
    T convert(S source);

    /**
     * Construct a composed &#123;@link Converter&#125; that first applies this &#123;@link Converter&#125;
     * to its input, and then applies the &#123;@code after&#125; &#123;@link Converter&#125; to the
     * result.
     * @param after the &#123;@link Converter&#125; to apply after this &#123;@link Converter&#125;
     * is applied
     * @param <U> the type of output of both the &#123;@code after&#125; &#123;@link Converter&#125;
     * and the composed &#123;@link Converter&#125;
     * @return a composed &#123;@link Converter&#125; that first applies this &#123;@link Converter&#125;
     * and then applies the &#123;@code after&#125; &#123;@link Converter&#125;
     * @since 5.3
     */
    default <U> Converter<S, U> andThen(Converter<? super T, ? extends U> after) &#123;
        Assert.notNull(after, "After Converter must not be null");
        return (S s) -> &#123;
            T initialResult = convert(s);
            return (initialResult != null ? after.convert(initialResult) : null);
        &#125;;
    &#125;

&#125;

自定义转换器示例:

index.html

<!DOCTYPE html>
<html lang="en">
        <head>
            <meta charset="UTF-8">
            <title>Index</title>
            <style type="text/css">
                body &#123;
                    font-family: Futura, "PingFang SC", Helvetica, Arial, sans-serif;
                &#125;
            </style>
        </head>
        <body>
                <h1>Welcome to index</h1>
                测试封装POJO;
                <form action="/saveuser" method="post">
                    姓名: <input name="userName" value="zhangsan"/> <br/>
                    年龄: <input name="age" value="18"/> <br/>
                    生日: <input name="birth" value="2019/12/10"/> <br/>
                    宠物: <input name="pet" value="啊猫,3"/>
                    <input type="submit" value="保存"/>
                </form>
        <br>
        </body>
</html>

com.yourname.boot.config.WebConfig

/**
 * Customize filter
 *
 * @author gregPerlinLi
 * @since 2021-10-27
 */
@Configuration(proxyBeanMethods = false)
public class WebConfig &#123;
    /**
     * WebMvcConfigure customizes SpringMVC functionality
     *
     * @return webMvcConfigure
     */
    @Bean
    public WebMvcConfigurer webMvcConfigurer() &#123;
        return new WebMvcConfigurer() &#123;
            @Override
            public void addFormatters(FormatterRegistry registry) &#123;
                registry.addConverter(new Converter<String, Pet>() &#123;
                    @Override
                    public Pet convert(String source) &#123;
                        // Cat,3
                        if (!StringUtils.isEmpty(source)) &#123;
                            Pet pet = new Pet();
                            String[] split = source.split(",");
                            pet.setName(split[0]);
                            pet.setAge(Integer.parseInt(split[1]));
                            return pet;
                        &#125;
                        return null;
                    &#125;
                &#125;);
            &#125;
        &#125;;
    &#125;
&#125;

6. 目标方法执行完成

将所有的数据都放在 ModelAdnViewContainer 中。其中包含了要转发的页面地址 View,还包含 Model 数据

ModelAndViewContainer

7. 处理派发结果

org.springframework.web.servlet.DispatcherServlet.doDispatch()

processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

org.springframework.web.servlet.view.AbstractView.render()

renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);

org.springframework.web.servlet.view.InternalResourceView.renderMergedOutputModel()

/**
 * Render the internal resource given the specified model.
 * This includes setting the model as request attributes.
 */
@Override
protected void renderMergedOutputModel(
        Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception &#123;

    // Expose the model object as request attributes.
    exposeModelAsRequestAttributes(model, request);

    // Expose helpers as request attributes, if any.
    exposeHelpers(request);

    // Determine the path for the request dispatcher.
    String dispatcherPath = prepareForRendering(request, response);

    // Obtain a RequestDispatcher for the target resource (typically a JSP).
    RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
    if (rd == null) &#123;
        throw new ServletException("Could not get RequestDispatcher for [" + getUrl() +
                "]: Check that the corresponding file exists within your web application archive!");
    &#125;

    // If already included or response already committed, perform include, else forward.
    if (useInclude(request, response)) &#123;
        response.setContentType(getContentType());
        if (logger.isDebugEnabled()) &#123;
            logger.debug("Including [" + getUrl() + "]");
        &#125;
        rd.include(request, response);
    &#125;

    else &#123;
        // Note: The forwarded resource is supposed to determine the content type itself.
        if (logger.isDebugEnabled()) &#123;
            logger.debug("Forwarding to [" + getUrl() + "]");
        &#125;
        rd.forward(request, response);
    &#125;
&#125;
  • 暴露模型作为请求域属性:

    // Expose the model object as request attributes.
    exposeModelAsRequestAttributes(model, request);
    

    org.springframework.web.servlet.view.InternalResourceView.exposeModelAsRequestAttributes()

    protected void exposeModelAsRequestAttributes(Map<String, Object> model,
            HttpServletRequest request) throws Exception &#123;
    
      // Model 中的所有数据遍历放到请求域中
        model.forEach((name, value) -> &#123;
            if (value != null) &#123;
                request.setAttribute(name, value);
            &#125;
            else &#123;
                request.removeAttribute(name);
            &#125;
        &#125;);
    &#125;
    

4. 数据响应与内容协商

4.1. 响应 JSON

4.1.1. jsckson.jar + @ResponseBody

引入 starter-web 场景,其中已包含 starter-json 场景(使用 Jackson 作为框架)

pom.xml

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

spring-boot-starter-web.pom

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-json</artifactId>
  <version>2.5.6</version>
  <scope>compile</scope>
</dependency>

spring-boot-starter-json.pom

<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.12.5</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.datatype</groupId>
  <artifactId>jackson-datatype-jdk8</artifactId>
  <version>2.12.5</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.datatype</groupId>
  <artifactId>jackson-datatype-jsr310</artifactId>
  <version>2.12.5</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.module</groupId>
  <artifactId>jackson-module-parameter-names</artifactId>
  <version>2.12.5</version>
  <scope>compile</scope>
</dependency>

示例代码:

com.yourname.boot.controller.ResponseTestController

@Controller
public class ResponseTestController &#123;
    @ResponseBody
    @GetMapping(value = "/test/person")
    public Person getPerson() &#123;
        Person person = new Person();
        person.setAge(28);
        person.setBirth(new Date());
        person.setUserName("XiaoMing");
        return person;
    &#125;
&#125;
1. 返回值解析器
ResponseReturnValueHandlers

org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle()

try &#123;
    this.returnValueHandlers.handleReturnValue(
            returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
&#125;

org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue()

/**
 * Iterate over registered &#123;@link HandlerMethodReturnValueHandler HandlerMethodReturnValueHandlers&#125; and invoke the one that supports it.
 * @throws IllegalStateException if no suitable &#123;@link HandlerMethodReturnValueHandler&#125; is found.
 */
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
        ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception &#123;

    HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
    if (handler == null) &#123;
        throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
    &#125;
    handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
&#125;

返回值的处理:

org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue()

@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
        ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
        throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException &#123;

    mavContainer.setRequestHandled(true);
    ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
    ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);

    // Try even with null return value. ResponseBodyAdvice could get involved.
  // 使用消息转换器进行写出操作
    writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
&#125;
2. 返回值解析器原理
HandlerMethodReturnValueHandler
  1. 返回值处理器判断是否支持这种类型返回值 supportsReturnType
  2. 返回值处理器调用 handleReturnValue 进行处理
  3. RequestResponseBodyMethodProcessor 可以处理返回值标注 @ResponseBody 注解的方法
    1. 利用 MessageConverters 进行处理 将数据写为 JSON
      1. 内容协商(浏览器默认会以请求头的方式告诉服务器能接受什么样的内容类型)
      2. 服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据
      3. SpringMVC 会挨个遍历所有容器底层的 HttpMessageConverter ,看谁能处理
        1. 得到 MappingJackson2HttpMessageConverter 可以将对象写为 JSON
        2. 利用 MappingJackson2HttpMessageConverter 将对象转为 JSON 再写出去

请求头:

ContentNegotiation

  • Accept:浏览器能接受的内容类型(q 为权重,值越大优先度越高)

4.1.2. SpringMVC 支持的返回值

  • ModelAndView
  • Model
  • View
  • ResponseEntity
  • ResponseBodyEmitter
  • StreamingResponseBody
  • HttpEntity
  • HttpHeaders
  • Callable
  • DeferredResult
  • ListenableFuture
  • CompletionStage
  • WebAsyncTask
  • @ModelAttribute 且为对象类型
  • @ResponseBody 注解 → RequestResponseBodyMethodProcessor

4.1.3. HTTPMessageConverter 原理

1. MessageConverter 规范
HttpMesageConverter

HttpMessageConverter 判断是否支持将提供的 Class 类型的对象转换为 MediaType 类型的数据

例如:将 Person 对象转换为 JSON 数据,或者是将 JSON 数据转换为 Person 对象

2. 系统中默认的 MessageConverter
DefaultMessageConverter
  • 0只支持 Byte 类型的

  • 1只支持 String 类型的

  • 2只支持 String 类型的

  • 3只支持 Resource 类型的

  • 4只支持 ResourceRegion 类型的

  • 5只支持如下 Source 类型:

    DOMSource.classSAXSource.classStAXSource.classStreamSource.classSource.class

  • 6只支持 MultiValueMap 类型的

  • 7true

  • 8true

最终 MappingJackson2HttpMessageConverter 将对象转换为 JSON 数据(利用底层的 Jackson 的 objectMapper 转换)

OutputMessage

4.2. 内容协商

根据客户端接收能力不同,返回不同媒体类型的数据

4.2.1. 引入 XML 的 Maven 依赖坐标

<dependency>
        <groupId>com.fasterxml.jackson.dataformat</groupId>
        <artifactId>jackson-dataformat-xml</artifactId>
</dependency>

4.2.2. Postman / Apifox 分别测试返回 JSON 和 XML

改变请求头中 Accept 字段,用于告诉服务器本客户端可以接受的数据类型

JSON:

ApifoxTestReturnJson

XML:

ApifoxTestReturnXml

4.2.3. 开启浏览器参数方式内容协商功能

为了方便内容协商,开启基于请求参数的内容协商功能:

application.yaml

spring:
  mvc:
    contentnegotiation:
      favor-parameter: true  #开启请求参数内容协商模式

发送请求:

http://localhost:8080/test/person?format=json
http://localhost:8080/test/person?format=xml
ContentNegotiationManager

确定客户端接收什么样的内容类型;

  1. Parameter 策略优先确定是要返回 JSON数据(获取请求头中的 format 的值)

    org.springframework.web.accept.ParameterContentNegotiationStrategy.getMediaTypeKey()

    @Override
    @Nullable
    protected String getMediaTypeKey(NativeWebRequest request) &#123;
        return request.getParameter(getParameterName());
    &#125;
    
  2. 最终进行内容协商返回给客户端 JSON 即可

4.2.4. 内容协商原理

  1. 判断当前响应头中是否已经有确定的媒体类型 MediaType

  2. 获取客户端(Postman / Apifox、浏览器)支持接收的内容类型(获取客户端 Accept 请求头字段:application/xml 或者是 application/json

  • contentNegotiationManager 内容协商管理器,默认使用基于请求头的策略
Strategies
  • HeaderContentNegotiationStrategy 确定客户端可以接受的内容类型

    org.springframework.web.accept.HeaderContentNegotiationStrategy.resolveMediaTypes()

String[] headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT);
if (headerValueArray == null) &#123;
    return MEDIA_TYPE_ALL_LIST;
&#125;
  1. 遍历循环所有当前系统的 MessageConverter,看其中哪个支持操作该对象(Person

    MessageConverters
  2. 找到支持操作 PersonConverter 支持的媒体类型统计出来

  3. 客户端需要 application/xml,服务端能力【10种、JSON、XML】

    ResponseTypeResult
  4. 进行内容协商的最佳匹配

    org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters()

    for (MediaType requestedType : acceptableTypes) &#123;
        for (MediaType producibleType : producibleTypes) &#123;
            if (requestedType.isCompatibleWith(producibleType)) &#123;
                mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
            &#125;
        &#125;
    &#125;
    
  5. 用支持将对象转为最佳匹配媒体类型的 Converter,并调用其进行转化 (当导入了 Jackson 处理 XML 的包之后,XML 的 Converter 就会自动加入进来)

导入了 Jackson 处理的 XML 的包,XML 的 Converter 就会自动导入进来

org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport

static &#123;
    ClassLoader classLoader = WebMvcConfigurationSupport.class.getClassLoader();
    romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", classLoader);
    jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", classLoader);
    jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
            ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
    jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
    jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
    jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader);
    gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
    jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", classLoader);
    kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader);
&#125;

org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport.addDefaultHttpMessageConverters()

if (!shouldIgnoreXml) &#123;
    if (jackson2XmlPresent) &#123;
        Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
        if (this.applicationContext != null) &#123;
            builder.applicationContext(this.applicationContext);
        &#125;
        messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
    &#125;
    else if (jaxb2Present) &#123;
        messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
    &#125;
&#125;

4.2.5. 自定义 MessageConverter

实现多协议数据兼容 JSON、XML、X-CUSTOM(自定义协议)

  1. @ResponseBody 响应数据出去调用 RequestResponseBodyMethodProcessor 处理
  2. Processor 处理方法返回值。通过 MessageConverter 处理
  3. 所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)
  4. 内容协商找到最终的 MessageConverter

无论是要改 SpringMVC 的什么功能,均从一个入口给容器中添加一个 WebMvcConfigurer

@Bean
public WebMvcConfigurer webMvcConfigurer() &#123;
    return new WebMvcConfigurer() &#123;
        @Override
        public void extendMessageConverters(List<HttpMessageConverter<?>> converters) &#123;
            
        &#125;
    &#125;;
&#125;

适配浏览器的参数方式内容协商功能:

原有的策略只包含了 XML 和 JSON 的

OldStrategy

在适配之后,strategyMediaTypes 中出现了 X-CUSTOM 协议

NewStraegy

示例代码:

com.yourname.boot.config.WebConfig

/**
 * Customize filter
 *
 * @author gregPerlinLi
 * @since 2021-10-27
 */
@Configuration(proxyBeanMethods = false)
public class WebConfig &#123;
    @Bean
    public HiddenHttpMethodFilter hiddenHttpMethodFilter()&#123;
        HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
        methodFilter.setMethodParam("_m");
        return methodFilter;
    &#125;
    /**
     * WebMvcConfigure customizes SpringMVC functionality
     *
     * @return webMvcConfigure
     */
    @Bean
    public WebMvcConfigurer webMvcConfigurer() &#123;
        return new WebMvcConfigurer() &#123;
            @Override
            public void extendMessageConverters(List<HttpMessageConverter<?>> converters) &#123;
                converters.add(new CustomMessageConverter());

            &#125;
            /**
             * Custom content negotiation policy
             *
             * @param configurer configurer
             */
            @Override
            public void configureContentNegotiation(ContentNegotiationConfigurer configurer) &#123;
                // Map<String, MediaType> mediaTypes
                Map<String, MediaType> mediaTypes = new HashMap<>(1000);
                mediaTypes.put("json", MediaType.APPLICATION_JSON);
                mediaTypes.put("xml", MediaType.APPLICATION_XML);
                mediaTypes.put("custom", MediaType.parseMediaType("application/x-custom"));
                // Specify which media types correspond to which parameters support resolution
                ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes);
HeaderContentNegotiationStrategy headerStrategy = new HeaderContentNegotiationStrategy();
                configurer.strategies(Arrays.asList(parameterStrategy, headerStrategy));
            &#125;
        &#125;;
    &#125;
&#125;

com.yourname.boot.converter.CustomMessageConverter

/**
 * Customize converter
 *
 * @author gregPerlinLi
 * @since 2021-11-02
 */
public class CustomMessageConverter implements HttpMessageConverter<Person> &#123;
    @Override
    public List<MediaType> getSupportedMediaTypes(Class<?> clazz) &#123;
        return HttpMessageConverter.super.getSupportedMediaTypes(clazz);
    &#125;
    @Override
    public boolean canRead(Class<?> clazz, MediaType mediaType) &#123;
        return false;
    &#125;
    @Override
    public boolean canWrite(Class<?> clazz, MediaType mediaType) &#123;
        return clazz.isAssignableFrom(Person.class);
    &#125;
    /**
     * The server needs to count what content types can be written out by all MessageConverters<br/>
     * application/x-custom
     *
     * @return MediaType list
     */
    @Override
    public List<MediaType> getSupportedMediaTypes() &#123;
        return MediaType.parseMediaTypes("application/x-custom");
    &#125;
    @Override
    public Person read(Class<? extends Person> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException &#123;
        return null;
    &#125;
    @Override
    public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException &#123;
        // Custom data write out
        String data = person.getUserName() + ";" +
                        person.getAge() + ";" +
                        person.getBirth() + ";" +
                        person.getPet() + ";";
        // Write out
        OutputStream body = outputMessage.getBody();
        body.write(data.getBytes(StandardCharsets.UTF_8));
    &#125;
&#125;

注意⚠️:添加的自定义功能可能会覆盖很多原有的功能,导致一些默认的功能失效!

5. 视图解析与模版解析

视图解析: SpringBoot 默认 不支持 JSP,需要引入第三方模版引擎技术实现页面渲染

5.1. 视图解析

视图解析原理流程
  1. 目标方法处理的过程中,所有数据都会被放在 ModelAndViewContainer 里面,包括数据和视图地址
  2. 方法的参数是一个自定义类型对象(从请求参数中确定的),把他重新放在 ModelAndViewContainer
  3. 任何目标方法执行完成以后都会返回 ModelAndView(数据和视图地址)
  4. ProcessDispatchResult 处理派发结果(页面改如何响应)
  • render**(**mv, request, response); 的页面渲染逻辑

    1. 根据方法的 String 返回值得到 View 对象(定义了页面的渲染逻辑)

    2. 所有的视图解析器尝试是否能根据当前返回值得到 View 对象

      ViewResolvers
    3. 得到了 redirect:/main.html → 在 Thymeleafnew RedirectView()

    4. ContentNegotiationViewResolver 里包含了上图中所有的视图解析器,内部还是利用其得到视图对象

      ViewResolver
    5. view.render(mv.getModelInternal(), request, response); 视图对象调用自定义的 Render 进行页面渲染工作

      View
    6. RedirectView 如何渲染(重定向到一个页面)

      • 获取目标 URL 地址
      • response.sendRedirect(encodedURL);

视图解析:

  • 返回值以 forward: 开始:new InternalResourceView(forwardUrl);→ 转发 request.getRequestDispatcher(path).forward(request, response);
  • 返回值以 redirect: 开始:new RedirectView();Render 就是重定向
  • 返回值是普通字符串: new ThymeleafView() → 渲染

5.2. 模版引擎——Thymeleaf

5.2.1. Thymeleaf 简介

Thymeleaf is a modern server-side Java template engine for both web and standalone environments, capable of processing HTML, XML, JavaScript, CSS and even plain text.

现代化、服务端Java模板引擎

5.2.2. 基本语法

1. 表达式
表达式名字 语法 用途
变量取值 ${...} 获取请求域、Session 域、对象等值
选择变量 *{...} 获取上下文对象值
消息 #{...} 获取国际化等值
链接 @{...} 生成链接
片段表达式 ~{...} jsp:include 作用,引入公共页面片段
2. 字面量
  • 文本值:'one text''Another one!'、…
  • 数字: 0343.012.3、…
  • 布尔值:true false
  • 空值:null
  • 变量: onetwo、…. 注意⚠️:变量不能有空格
3. 文本操作
  • 字符串拼接:+
  • 变量替换:|The name is ${name}|
4. 数学运算
  • 运算符:+-*/%
5、布尔运算
  • 运算符:andor
  • 一元运算:!not
6、比较运算
  • 比较: ><>=<=gtltgele
  • 等式:==!=eqne
7、条件运算
  • If-then: (if) ? (then)
  • If-then-else: (if) ? (then) : (else)
  • Default: (value) ?: (defaultvalue)
8、特殊操作
  • 无操作:_

5.2.3. 设置属性值—th:attr

1. 设置单个值
<form action="subscribe.html" th:attr="action=@&#123;/subscribe&#125;">
  <fieldset>
    <input type="text" name="email" />
    <input type="submit" value="Subscribe!" th:attr="value=#&#123;subscribe.submit&#125;"/>
  </fieldset>
</form>
2. 设置多个值
<img src="../../images/gtvglogo.png"  th:attr="src=@&#123;/images/gtvglogo.png&#125;,title=#&#123;logo&#125;,alt=#&#123;logo&#125;" />
3. 通用写法 th:xxx
<input type="submit" value="Subscribe!" th:value="#&#123;subscribe.submit&#125;"/>
<form action="subscribe.html" th:action="@&#123;/subscribe&#125;">
4. 所有兼容 HTML5 的标签写法

https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#setting-value-to-specific-attributes

5.2.4. 迭代

<tr th:each="prod : $&#123;prods&#125;">
        <td th:text="$&#123;prod.name&#125;">Onions</td>
        <td th:text="$&#123;prod.price&#125;">2.41</td>
        <td th:text="$&#123;prod.inStock&#125;? #&#123;true&#125; : #&#123;false&#125;">yes</td>
</tr>
<tr th:each="prod,iterStat : $&#123;prods&#125;" th:class="$&#123;iterStat.odd&#125;? 'odd'">
  <td th:text="$&#123;prod.name&#125;">Onions</td>
  <td th:text="$&#123;prod.price&#125;">2.41</td>
  <td th:text="$&#123;prod.inStock&#125;? #&#123;true&#125; : #&#123;false&#125;">yes</td>
</tr>

5.2.5. 条件运算

<a href="comments.html"
th:href="@&#123;/product/comments(prodId=$&#123;prod.id&#125;)&#125;"
th:if="$&#123;not #lists.isEmpty(prod.comments)&#125;">view</a>
<div th:switch="$&#123;user.role&#125;">
  <p th:case="'admin'">User is an administrator</p>
  <p th:case="#&#123;roles.manager&#125;">User is a manager</p>
  <p th:case="*">User is some other thing</p>
</div>

5.2.6. 属性优先级

Order Feature Attribute
1 Fragment inclusion th:insert
th:replace
2 Fragment iteration th:each
3 Conditional evaluation th:if
th:unless
th:switch
th:case
4 Local variable definition th:object
th:with
5 General attribute modification th:attr
th:attrprepend
th:attrappend
6 Specific attribute modification th:value
th:href
th:src
7 Text (Tag body modification) th:text
th:utext
8 Fragment specification th:fragment
9 Fragment removal th:remove

5.3. Thymeleaf 使用

1. 引入 Starter

pom.xml

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

2. 自动配置 Thymeleaf

org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration

/**
 * &#123;@link EnableAutoConfiguration Auto-configuration&#125; for Thymeleaf.
 *
 * @author Dave Syer
 * @author Andy Wilkinson
 * @author Stephane Nicoll
 * @author Brian Clozel
 * @author Eddú Meléndez
 * @author Daniel Fernández
 * @author Kazuki Shimizu
 * @author Artsiom Yudovin
 * @since 1.0.0
 */
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass(&#123; TemplateMode.class, SpringTemplateEngine.class &#125;)
@AutoConfigureAfter(&#123; WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class &#125;)
public class ThymeleafAutoConfiguration &#123;
  
&#125;

自动配置的策略:

  1. 所有 Thymeleaf 的配置值都在 ThymeleafProperties
  2. 自动配置 SpringTemplateEngine
  3. 自动配置 ThymeleafViewResolver
  4. 我们只需要直接开发页面即可

3. 页面开发

success.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" >
<head>
    <meta charset="UTF-8">
    <title>Success</title>
    <style type="text/css">
        body &#123;
            font-family: Futura, "PingFang SC", Helvetica, Arial, sans-serif;
        &#125;
    </style>
</head>
<body>
    <h1 th:text="$&#123;msg&#125;">Haha</h1>
    <h2>
        <a href="https://forum.gdutelc.com" th:href="$&#123;link&#125;">Go to baidu.com</a><br/>
        <a href="https://forum.gdutelc.com" th:href="@&#123;link&#125;">Go to baidu.com</a>
    </h2>
</body>
</html>

com.yourname.boot.controller.ViewTestController

/**
 * @author gregPerlinLi
 * @since 2021-11-03
 */
@Controller
public class ViewTestController &#123;
    @GetMapping(value = "/hello")
    public String hello(Model model) &#123;
        // The data in the model is placed in the request field request.setAttribute("a", aa)
        model.addAttribute("msg", "Hello world!");
        model.addAttribute("link", "https://www.baidu.com");
        return "success";
    &#125;
&#125;

4. 如何给项目加上访问前置路径

application.yaml

server:
  servlet:
    context-path: /world
  • 配置之前的访问路径:http://ip:port
  • 配置之后的访问路径:http://ip:port/world

6. 拦截器

6.1. HandlerInterceptor 接口

com.yourname.adminserver.intercepter.LoginInterceptor

/**
 * Login check<br/>
 * 1. Configure the interceptor to intercept those requests<br/>
 * 2. Put these configurations in a container<br/>
 *
 * @author gregPerlinLi
 * @since 2021-11-03
 */
@Slf4j
public class LoginInterceptor implements HandlerInterceptor &#123;
    /**
     * Interception point before the execution of a handler. Called after
     * HandlerMapping determined an appropriate handler object, but before
     * HandlerAdapter invokes the handler.
     * <p>DispatcherServlet processes a handler in an execution chain, consisting
     * of any number of interceptors, with the handler itself at the end.
     * With this method, each interceptor can decide to abort the execution chain,
     * typically sending an HTTP error or writing a custom response.
     * <p><strong>Note:</strong> special considerations apply for asynchronous
     * request processing. For more details see
     * &#123;@link AsyncHandlerInterceptor&#125;.
     * <p>The default implementation returns &#123;@code true&#125;.
     *
     * @param request  current HTTP request
     * @param response current HTTP response
     * @param handler  chosen handler to execute, for type and/or instance evaluation
     * @return &#123;@code true&#125; if the execution chain should proceed with the
     * next interceptor or the handler itself. Else, DispatcherServlet assumes
     * that this interceptor has already dealt with the response itself.
     * @throws Exception in case of errors
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception &#123;
        log.info("Current method is: preHandle");
        String requestURI = request.getRequestURI();
        log.info("The intercepted request path is: " + requestURI );
        // Login check logic
        HttpSession session = request.getSession();
        Object loginUser = session.getAttribute("loginUser");
        if ( loginUser != null ) &#123;
            // Release
            return true;
        &#125;
        // Intercept, not logged in, jump to login page
        request.setAttribute("msg", "Please log in first!");
        request.getRequestDispatcher("/").forward(request, response);
        return false;
    &#125;

    /**
     * Interception point after successful execution of a handler.
     * Called after HandlerAdapter actually invoked the handler, but before the
     * DispatcherServlet renders the view. Can expose additional model objects
     * to the view via the given ModelAndView.
     * <p>DispatcherServlet processes a handler in an execution chain, consisting
     * of any number of interceptors, with the handler itself at the end.
     * With this method, each interceptor can post-process an execution,
     * getting applied in inverse order of the execution chain.
     * <p><strong>Note:</strong> special considerations apply for asynchronous
     * request processing. For more details see
     * &#123;@link AsyncHandlerInterceptor&#125;.
     * <p>The default implementation is empty.
     *
     * @param request      current HTTP request
     * @param response     current HTTP response
     * @param handler      the handler (or &#123;@link org.springframework.web.method.HandlerMethod&#125;) that started asynchronous
     *                     execution, for type and/or instance examination
     * @param modelAndView the &#123;@code ModelAndView&#125; that the handler returned
     *                     (can also be &#123;@code null&#125;)
     * @throws Exception in case of errors
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception &#123;
        log.info("Current method is: postHandle " + modelAndView);
    &#125;

    /**
     * Callback after completion of request processing, that is, after rendering
     * the view. Will be called on any outcome of handler execution, thus allows
     * for proper resource cleanup.
     * <p>Note: Will only be called if this interceptor's &#123;@code preHandle&#125;
     * method has successfully completed and returned &#123;@code true&#125;!
     * <p>As with the &#123;@code postHandle&#125; method, the method will be invoked on each
     * interceptor in the chain in reverse order, so the first interceptor will be
     * the last to be invoked.
     * <p><strong>Note:</strong> special considerations apply for asynchronous
     * request processing. For more details see
     * &#123;@link AsyncHandlerInterceptor&#125;.
     * <p>The default implementation is empty.
     *
     * @param request  current HTTP request
     * @param response current HTTP response
     * @param handler  the handler (or &#123;@link org.springframework.web.method.HandlerMethod&#125;) that started asynchronous
     *                 execution, for type and/or instance examination
     * @param ex       any exception thrown on handler execution, if any; this does not
     *                 include exceptions that have been handled through an exception resolver
     * @throws Exception in case of errors
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception &#123;
        log.info("Current method is: afterCompletion " + ex);
    &#125;
&#125;

6.2. 配置拦截器

com.yourname.adminserver.config.AdminWebConfig

/**
 * 1. Write an interceptor to implement the <code>HandlerInterceptor</code> interface<br/>
 * 2. The interceptor is registered in the container (realization <code>WebMvcConfigurer.addInterceptors()</code>)<br/>
 * 3. Specify interception rules (If all resources are intercepted, static resources will also be intercepted)
 *
 * @author gregPerlinLi
 * @since 2021-11-03
 */
@Configuration
public class AdminWebConfig implements WebMvcConfigurer &#123;
    @Override
    public void addInterceptors(InterceptorRegistry registry) &#123;
        registry.addInterceptor(new LoginInterceptor())
                // All requests will be blocked, including static resources are also blocked
                .addPathPatterns("/**")
                // Request for release
                .excludePathPatterns("/", "/login", "/css/**", "/fonts/**", "/images/**", "/js/**");
    &#125;
&#125;

6.3. 拦截器原理

  1. 根据当前请求,找到 HandlerExecutionChain(可以处理请求的 Handler 以及 Handler 的所有拦截器)

    MappedHandler

  2. 顺序执行 所有拦截器的 preHandle() 方法

    1. 如果当前拦截器 preHandle() 返回为 true,则执行下一个拦截器的 preHandle()

    2. 如果当前拦截器 preHandle 返回为 false,直接倒叙执行所有已经执行了的拦截器的 afterCompletion() 方法

      org.springframework.web.servlet.HandlerExecutionChain.applyPreHandle()

      /**
       * Apply preHandle methods of registered interceptors.
       * @return &#123;@code true&#125; if the execution chain should proceed with the
       * next interceptor or the handler itself. Else, DispatcherServlet assumes
       * that this interceptor has already dealt with the response itself.
       */
      boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception &#123;
          for (int i = 0; i < this.interceptorList.size(); i++) &#123;
              HandlerInterceptor interceptor = this.interceptorList.get(i);
              if (!interceptor.preHandle(request, response, this.handler)) &#123;
                  triggerAfterCompletion(request, response, null);
                  return false;
              &#125;
              this.interceptorIndex = i;
          &#125;
          return true;
      &#125;
      
  3. 如果任何一个拦截器返回 false 直接跳出,不执行目标方法

  4. 所有拦截器都返回 true,执行目标方法

  5. 倒叙执行所有拦截器的 postHandle() 方法

    org.springframework.web.servlet.HandlerExecutionChain.allpyPostHandle

    /**
     * Apply postHandle methods of registered interceptors.
     */
    void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
            throws Exception &#123;
    
        for (int i = this.interceptorList.size() - 1; i >= 0; i--) &#123;
            HandlerInterceptor interceptor = this.interceptorList.get(i);
            interceptor.postHandle(request, response, this.handler, mv);
        &#125;
    &#125;
    
  6. 前面的步骤有任何异常,均会直接触发所有拦截器的 afterCompletion() 方法

  7. 页面成功渲染完成之后,也会倒序触发 afterCompletion() 方法

拦截器的执行流程:

IntercepterChain

7. 文件上传

7.1. 页面表单

form_layouts.html

<form role="form" th:action="@&#123;/upload&#125;" method="post" enctype="multipart/form-data">
   <div class="form-group">
       <label for="exampleInputEmail1">Email address</label>
       <input type="email" name="email" class="form-control" id="exampleInputEmail1" placeholder="Enter email">
   </div>
   <div class="form-group">
       <label for="exampleInputUsername">Username</label>
       <input type="text" name="username" class="form-control" id="exampleInputUsername" placeholder="Enter username">
   </div>
   <div class="form-group">
       <label for="exampleInputFile">Header image</label>
       <input type="file" name="headerImg" id="exampleInputFile">
   </div>
   <div class="form-group">
       <label for="exampleInputMultipleFile">life images</label>
       <input type="file" name="photos" id="exampleInputMultipleFile" multiple>
   </div>
   <div class="checkbox">
       <label>
           <input type="checkbox"> Check me out
       </label>
   </div>
   <button type="submit" class="btn btn-primary">Submit</button>
</form>

7.2. 文件上传代码

com.yourname.adminserver.controller.FormController

/**
 * File upload test
 *
 * @author gregPerlinLi
 * @since 2021-11-05
 */
@Slf4j
@Controller
public class FormController &#123;

    @GetMapping(value = "form_layouts")
    public String formLayouts() &#123;
        return "form/form_layouts";
    &#125;

    /**
     * <code>MultipartFile</code>: Automatically encapsulate uploaded files
     *
     * @param email email
     * @param username username
     * @param headerImg image
     * @param photos images
     * @return
     */
    @PostMapping(value = "/upload")
    public String upload(@RequestParam("email") String email,
                         @RequestParam("username") String username,
                         @RequestPart("headerImg") MultipartFile headerImg,
                         @RequestPart("photos") MultipartFile[] photos) throws IOException &#123;
        log.info("Upload information: email=&#123;&#125;, username=&#123;&#125;, headerImg=&#123;&#125;, photos=&#123;&#125;",
                email, username, headerImg.getSize(), photos.length);
        if ( !headerImg.isEmpty() ) &#123;
            // Save to file server, OSS server
            String originalFilename = headerImg.getOriginalFilename();
            headerImg.transferTo(new File("/Users/gregperlinli/Projects/Java/Spring/SpringBoot/admin-server/src/main/resources/cache/" + originalFilename));
        &#125;
        if ( photos.length > 0 ) &#123;
            for (MultipartFile photo : photos) &#123;
                if (!photo.isEmpty()) &#123;
                    String originalFilename = photo.getOriginalFilename();
                    photo.transferTo(new File("/Users/gregperlinli/Projects/Java/Spring/SpringBoot/admin-server/src/main/resources/cache/" + originalFilename));
                &#125;
                &#125;
        &#125;
        return "main";
    &#125;
&#125;

7.3. 配置文件最大上传大小限制

application.yaml

spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 100MB

7.4. 自动配置原理

文件上传配置类:org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration

  • 自动配置好了 StandardServletMultipartResolver(文件上传解析器)

  • 原理步骤:

    1. 请求进来后,使用文件上传解析器判断(isMultipart())并封装(调用 resolveMultipart 并返回 MultipartHttpServletRequest)文件上传请求

    2. 参数解析器解析请求中的文件内容,并封装成 multipartFile

      RequestPartMethodArgumentResolverInArgumentResolvers
    3. Request 中的文件信息封装为一个 MapMultiValueMap<String, MultipartFile>

      com.yourname.adminserver.controller.FormController

      @PostMapping(value = "/upload")
      public String upload(@RequestParam("email") String email,
                           @RequestParam("username") String username,
                           @RequestPart("headerImg") MultipartFile headerImg,
                           @RequestPart("photos") MultipartFile[] photos) throws IOException &#123;
      
    4. 使用 FileCopyUtils 实现文件流的拷贝

8. 异常处理

8.1. 默认规则

By default, Spring Boot provides an /error mapping that handles all errors in a sensible way, and it is registered as a “global” error page in the servlet container. For machine clients, it produces a JSON response with details of the error, the HTTP status, and the exception message. For browser clients, there is a “whitelabel” error view that renders the same data in HTML format (to customize it, add a View that resolves to error).

There are a number of server.error properties that can be set if you want to customize the default error handling behavior. See the “Server Properties” section of the Appendix.

To replace the default behavior completely, you can implement ErrorController and register a bean definition of that type or add a bean of type ErrorAttributes to use the existing mechanism but replace the contents.

  • 默认情况下,Spring Boot 提供 /error 处理所有错误的映射

  • 对于机器客户端,它将生成 JSON 响应,其中包含错误,HTTP 状态和异常消息的详细信息。对于浏览器客户端,响应一个 whitelabel 错误视图,以 HTML 格式呈现相同的数据

    • 机器客户端:

      MachineClientErrorMessage
    • 浏览器客户端:

      BrowserClientErrorMessage
  • 要对其进行自定义,添加 View 解析为 error

  • 要完全替换默认行为,可以实现 ErrorController 并注册该类型的 Bean 定义,或添加 ErrorAttributes 类型的组件以使用现有机制但替换其内容。

  • error 目录下的 4xx5xx 页面会被自动解析:

    ErrorPagesFor404&5xx

8.2. 定制错误处理逻辑

  • 自定义错误页:

    • error/404.htmlerror/5xx.html 有精确的错误状态码页面就匹配精确,没有就找 4xx.html 如果都没有就触发白页
  • @ControllerAdvice + @ExceptionHandler 处理全局异常,其底层是由 ExceptionHandlerExceptionResolver 提供处理支持(推荐使用此方法)

  • @ResponseStatus + 自定义异常,其底层是由 ResponseStatusExceptionResolver 提供处理支持,把 responseStatus 注解的信息组装成 ModelAndView 返回,底层会调用 response.sendError(statusCode, resolvedReason);,让 Tomcat 发送 /error

  • Spring 底层的异常,(如:参数类型转换异常),DefaultHandlerExceptionResolver 处理框架底层的异常

    response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage()); 
    

    Tomcat404Error

  • 自定义实现 HandlerExceptionResolver 处理异常,可以作为默认的全局异常处理规则

    CustomerHandlerExceptionResolver
  • ErrorViewResolver 实现自定义处理异常:

    • response.sendErrorerror 请求就会转给 Controller
    • 当异常没有任何解析器能够处理时,Tomcat 底层执行 response.sendErrorerror 请求就会转给 Controller
    • basicErrorController 要转去的页面地址是 ErrorViewResolver

8.3. 异常处理自动配置原理

  • ErrorMvcAutoConfiguration 自动配置异常处理规则

    • 容器中的组件:

      • 类型:DefaultErrorAttributes → ID:errorAttributes

        public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver &#123; &#125;
        
        • DefaultErrorAttributes:定义错误页面中可以包含哪些数据

          org.springframework.boot.web.servlet.error.DefaultErrorAttributes.getErrorAttributes()

          @Override
          public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) &#123;
              Map<String, Object> errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
              if (!options.isIncluded(Include.EXCEPTION)) &#123;
                  errorAttributes.remove("exception");
              &#125;
              if (!options.isIncluded(Include.STACK_TRACE)) &#123;
                  errorAttributes.remove("trace");
              &#125;
              if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) &#123;
                  errorAttributes.remove("message");
              &#125;
              if (!options.isIncluded(Include.BINDING_ERRORS)) &#123;
                  errorAttributes.remove("errors");
              &#125;
              return errorAttributes;
          &#125;
          
          private Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) &#123;
              Map<String, Object> errorAttributes = new LinkedHashMap<>();
              errorAttributes.put("timestamp", new Date());
              addStatus(errorAttributes, webRequest);
              addErrorDetails(errorAttributes, webRequest, includeStackTrace);
              addPath(errorAttributes, webRequest);
              return errorAttributes;
          &#125;
          
      • 类型:BasicErrorController → ID:basicErrorController(JSON + 白页适配响应)

        • 处理默认 /eroor 路径的请求,页面响应 new ModelAndView("error", model);
        • 容器中有组件 View,其 ID 为 error(相应默认错误页)
        • 容器中有组件 BeanNameViewResolver(视图解析器,按照返回的视图名称作为组件的 ID 去容器中找 View 对象)
      • 类型 DefaultErrorViewResolver → ID: conventionErrorViewResolver

        • 如果发生错误,会以 HTTP 状态码作为视图页地址(viewName),找到真正的页面
        • err/viewName.html(其中的 viewName 是 HTTP 状态码,如:404500 等)
  • 如果想要返回页面,就会寻找 error 视图 StaticView(默认是一个白页)

    • 返回 JSON 数据:

      org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error()

      @RequestMapping
      public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) &#123;
          HttpStatus status = getStatus(request);
          if (status == HttpStatus.NO_CONTENT) &#123;
              return new ResponseEntity<>(status);
          &#125;
          Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
          return new ResponseEntity<>(body, status);
      &#125;
      
    • 返回 HTML 页面:

      org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml()

      @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
      public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) &#123;
          HttpStatus status = getStatus(request);
          Map<String, Object> model = Collections
                  .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
          response.setStatus(status.value());
          ModelAndView modelAndView = resolveErrorView(request, response, status, model);
          return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
      &#125;
      

8.4. 异常处理步骤流程

  1. 执行目标方法,目标方法运行期间有任何异常都会被抓取并标志当前请求已结束,之后用 DispatchException 将其封装

  2. 进入视图解析流程(页面渲染)

    processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    
  3. processHandlerException()处理 Handler 发生的异常,处理完后返回 ModelAndView

    1. 遍历所有的 handlerExceptionResolver 看哪个可以处理当前异常(**HandlerExceptionResolver 处理器异常解析器**)

      HandlerExceptionResolver
    2. 系统默认的异常解析器:

      DefaultHandlerExceptionResolver
      1. DefaultErrorAttribute 先处理异常,将异常信息保存到 Request 域,并返回 null

      2. 当没有任何系统默认的异常解析器能够处理异常时,将会抛出异常

        1. 如果没有任何解析器能处理,最终底层会发送 /error 请求,被底层的 BasicErrorCOntroller 处理

        2. 解析错误视图:遍历所有的 ErrorViewResolver 看哪个可以解析

          DefaultErrorViewResolver
        3. 默认的 DefaultErrorViewResolver,作用是把响应状态码作为错误页的地址,拼接成 error/statusValue.html

        4. 模板引擎最终响应这个页面 error/statusValue.html

9. Web 原生组件注入(Servlet、Filter、Listener)

9.1. 使用原生 Servlet API

  • @ServletComponentScan(basePackages = "com.yourname") 指定原生Servlet组件都放在那里
  • @WebServlet(name = "MyServlet", urlPatterns = "/my") 指定原生 Servlet 应用,效果:直接响应, 而且不会经过 Spring 的拦截器(因为经过 Spring 拦截器的前提是要经过 DispatcherServletdoDispatch() 方法)
  • @WebFilter(filterName = "MyFilter",urlPatterns = {"/css/*", "/images/*"}) 指定原生的 Filter 过滤器(过滤某个目录下的所有请求时,原生 Servlet 过滤器使用 /*,SpringMVC 拦截器则使用 /**
  • @WebListener 指定原生的 Listener 监听器

示例代码:

com.yourname.adminserver.AdminServerApplication

/**
 * Handling exceptions in the entire web
 *
 * @author gregPerlinLi
 * @since 2021-11-05
 */
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler &#123;

    @ExceptionHandler(value = &#123;ArithmeticException.class, NullPointerException.class&#125;)
    public String handleArithmeticException(Exception e) &#123;

        log.error("The exception is: &#123;&#125;", e.toString());
        // Return view address
        return "login";
    &#125;
&#125;

com.yourname.adminserver.servlet.MyServlet

/**
 * @author gregPerlinLi
 * @since 2021-11-05
 */
@WebServlet(name = "MyServlet", urlPatterns = "/my")
public class MyServlet extends HttpServlet &#123;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException &#123;
        resp.getWriter().write("Hello servlet!");
    &#125;
&#125;

com.youranme.adminserver.filter.MyFilter

/**
 * @author gregPerlinLi
 * @since 2021-11-05
 */
@Slf4j
@WebFilter(filterName = "MyFilter",urlPatterns = &#123;"/css/*", "/images/*"&#125;)
public class MyFilter implements Filter &#123;
    @Override
    public void init(FilterConfig filterConfig) throws ServletException &#123;
        log.info("MyFilter initialize complete...");
    &#125;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException &#123;
        log.info("MyFilter is working...");
        chain.doFilter(request, response);
    &#125;

    @Override
    public void destroy() &#123;
            log.info("MyFilter destroyed...");
    &#125;
&#125;

com.yourname.adminserver.listener.MyServletContextListener

/**
 * @author gregPerlinLi
 * @since 2021-11-05
 */
@Slf4j
@WebListener
public class MyServletContextListener implements ServletContextListener &#123;
    @Override
    public void contextInitialized(ServletContextEvent sce) &#123;
        log.info("MyServletContextListener: listening to the completion of project initialization!");
    &#125;

    @Override
    public void contextDestroyed(ServletContextEvent sce) &#123;
        log.info("MyServletContextListener: listening to the project destruction!");
    &#125;
&#125;

扩展:DispatchServlet 如何注册进入 Servlet

  • 容器中自动配置了 DispatcherServlet 组件,并将属性绑定到了 WebMvcProperties,对应的配置文件配置项是 spring.mvc

  • 通过 ServletRegistrationBean<DispatcherServlet>DispatcherServlet 配置进来

    application.yaml

    spring:
      mvc:
        servlet:
          path: /
    
    • 默认映射的是 / 路径

    TomcatAndSpring

对于 Tomcat- Servlet,多个 Servlet 都能处理到同一层路径时,以精确优先为原则(例如,当 A 的路径为 /my/,B 的路径为 /my/1 时,B 优先于 A 执行)

拓展:Filter 和 Interceptor 的区别:

Filter: Filter 过滤器是 Servlet 定义的原生组件,优点是脱离了 Spring 框架也可以使用

Interceptor: Interceptor 是 Spring 定义的接口,优点是可以使用 Spring 定义的各种功能(如自动注入 @Autowired

9.2. 使用 RegistrationBean

原生 Web 组件加入 Spring 框架推荐使用此方式

  • ServletRegistrationBean 注册 Servlet 应用
  • FilterRegistrationBean 注册 Filter 过滤器
  • ServletListenerRegistrationBean 注册 Listener 监听器

示例代码:

com.yourname.adminserver.config.MyRegistrationConfig

/**
 * <code>@Configuration(proxyBeanMethods = true)</code>: Ensure that the dependent components are always single instance
 *
 * @author gregPerlinLi
 * @since 2021-11-05
 */
@Configuration(proxyBeanMethods = true)
public class MyRegistrationConfig &#123;

    @Bean
    public ServletRegistrationBean myServlet() &#123;
        MyServlet myServlet = new MyServlet();
        return new ServletRegistrationBean(myServlet, "/my", "/my02");
    &#125;

    @Bean
    public FilterRegistrationBean myFilter() &#123;
        MyFilter myFilter = new MyFilter();
        // return new FilterRegistrationBean(myFilter, myServlet());
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
        filterRegistrationBean.setUrlPatterns(Arrays.asList("/my", "/js/*"));
        return filterRegistrationBean;
    &#125;

    @Bean
    public ServletListenerRegistrationBean myListener() &#123;
        MyServletContextListener myServletContextListener = new MyServletContextListener();
        return new ServletListenerRegistrationBean(myServletContextListener);
    &#125;
&#125;

10. 嵌入式 Servlet 容器

10.1. 切换嵌入式 Servlet 容器

  • 默认支持的 Web Server:

    • Tomcat、Jetty 或者是 Undertow

    • ServletWebServerApplicationContext 容器启动寻找 ServletWebServerFactory 并引导创建服务器

    • 切换服务器

      WebServer

      pom.xml

      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
          <exclusions>
              <exclusion>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-tomcat</artifactId>
              </exclusion>
          </exclusions>
      </dependency>
      
  • 原理:

    • SpringBoot 应用启动发现当前是 Web 应用,Web 场景包 → 导入Tomcat
    • Web 应用会创建一个 Web 版的 IOC 容器 ServletWebServerApplicationContext
    • ServletWebServerApplicationContext 启动的时候寻找 ServletWebServerFactory(Servlet 的 Web 服务器工厂 → Servlet 的 Web 服务器)
    • SpringBoot 底层默认有很多的 WebServer 工厂,例如:TomcatServletWebServerFactoryJettyServletWebServerFactory 或者是 UndertowServletWebServerFactory
    • 底层直接会有一个自动配置类:ServletWebServerFactoryAutoConfiguration
    • ServletWebServerFactoryAutoConfiguration 导入了 ServletWebServerFactoryConfiguration(配置类)
    • ServletWebServerFactoryConfiguration 配置会根据动态判断系统中到底导入了那个 Web 服务器的包(默认是 web-starter 导入 Tomcat 包),容器中就有 TomcatServletWebServerFactory
    • TomcatServletWebServerFactory 创建出 Tomcat 服务器并启动,TomcatWebServer 的构造器拥有初始化方法initialize()this.tomcat.start();
    • 内嵌服务器,就是手动把启动服务器的代码调用(前提是 Tomcat 核心 Jar 包必须存在)

10.2. 定制 Servlet 容器

  • 实现 WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>
    • 把配置文件的值和 ServletWebServerFactory 进行绑定
  • 修改配置文件 server.xxx
  • 直接自定义 ConfigurableServletWebServerFactory

xxxCustomizer定制化器,可以改变 xxx 的默认规则

import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.stereotype.Component;

@Component
public class CustomizationBean implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> &#123;

    @Override
    public void customize(ConfigurableServletWebServerFactory server) &#123;
        server.setPort(9000);
    &#125;

&#125;

11. 定制化原理

11.1. 定制化常见方式

  • @Bean 替换、增加容器中默认组件,视图解析器

  • 修改配置文件

  • xxxCustomizer

  • 便携自定义的配置类 xxxConfigueation

  • Web 应用,编写一个配置类实现 WebMvcConfiguration 即可定制化 Web 功能,+ @Bean 给容器中再扩展一些组件

    @Configuration
    public class AdminWebConfig implements WebMvcConfigurer &#123;
      
    &#125;
    
  • @EnableWebMvc + WebMvcConfigurer@Bean 可以全面接管 SpringMVC,所有的规则全部自己重新设置,实现定制和拓展功能 (注意⚠️:如果启用全面接管, SpringBoot 所有的自动配置、解析器、转换器等等都将会失效,慎用!慎用!!慎用!!!没那个本事千万不要打开!)

    com.yourname.adminserver.config.AdminWebConfig

    /**
     * <p>1. Write an interceptor to implement the &#123;@code HandlerInterceptor&#125; interface</p>
     * <p>2. The interceptor is registered in the container (realization &#123;@code WebMvcConfigurer.addInterceptors()&#125;)</p>
     * <p>3. Specify interception rules (If all resources are intercepted, static resources will also be intercepted)</p>
     * <p>4. &#123;@code @EnableWebMvc&#125;: Full takeover SpringMVC</p>
     * <p>      1. Static resource, view resolver, welcome page... all invalid</p>
     *
     *
     * @author gregPerlinLi
     * @since 2021-11-03
     */
    @EnableWebMvc
    @Configuration
    public class AdminWebConfig implements WebMvcConfigurer &#123;
    
    
        /**
         * Define static resource behavior
         *
         * @param registry registry
         */
        @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) &#123;
            // Access /aa/** all requests are matched under classpath:/static/
            registry.addResourceHandler("/aa/**")
                    .addResourceLocations("classpath:/static/");
        &#125;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) &#123;
            registry.addInterceptor(new LoginInterceptor())
                    // All requests will be blocked, including static resources are also blocked
                    .addPathPatterns("/**")
                    // Request for release
                    .excludePathPatterns("/", "/login", "/css/**", "/fonts/**", "/images/**", "/js/**", "/aa/**");
        &#125;
    &#125;
    
    • 原理:

      1. WebMvcAutoConfiguration 是默认的 SpringMVC 的自动配置功能类,负责静态资源,欢迎页,视图解析器等的配置

      2. 一旦使用 @EnableWebMvc,将会执行 @Import(DelegatingWebMvcConfiguration.class)

      3. DelegatingWebMvcConfiguration 的作用:只保证 SpringMVC 最基本的使用

        • 获取所有系统中的 WebMvcConfigurer,所有功能的定制都由这些 WebMvcConfigurer 共同生效

        • 自动配置了一些非常底层的组件 RequestMappingHandlerMapping、这些组件所依赖的组件均在容器中获取:

          public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport &#123;
            
          &#125;
          
      4. 若要让 WebMvcAutoConfiguration 中的配置生效,必须实现以下条件注解:

        @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
        
      5. 因此,@EnableWebMvc 的启用将会导致 WebMvcAutoConfiguration 失效

  • ……

11.2. 原理分析套路

  • 场景 starter
  • xxxAutoConfiguration
  • 导入 xxx 组件
  • 绑定 xxxProperties
  • 绑定配置文件项


文章作者: gregPerlinLi
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 gregPerlinLi !
  目录