Spring Boot

Starter

Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can "just run".

  • 全新框架,目的是简化Spring应用的搭建和开发过程,不需要定义样板化的配置(例如xml文件)
  • 从根本上讲是一些库的集合(使用Maven/Gradle 构建项目,无需自行管理这些库的版本)
  • 特点:
    • 简化Maven配置
    • 自动配置Spring
    • 创建独立的Spring应用程序
    • 嵌入Web容器,无需部署war包(直接java -jar 就可运行)
    • 提供生产就绪型功能(如指标,健康检查和外部配置)
  • 场景:
    • 开发Restful风格的微服务架构
    • 微服务,自动化,横向扩展
    • 精简配置与整合其他工具
  • Vesion:
    • SpringBoot 1.x - Spring 4.x
    • SpringBoot 2.x - Spring 5.x

Spring Boot

三大特性

  1. 自动装配组件: Web MVC,Web Flux,JDBC,...

    • 激活:@EnableAutoConfiguration
        @EnableAutoConfiguration
        public class App {
            public static void main(String[] args) { 
                SpringApplication.run(App.class,args);
            }
        }
      
    • 实现:XxxAutoConfiguration , eg: WebMvcAutoConfiguration
        @Configuration
        @ConditionalOnWebApplication(type = Type.SERVLET)
        @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
        @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
        @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
        @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class,ValidationAutoConfiguration.class })
        public class WebMvcAutoConfiguration {
            @Bean
            //...
        }
      
    • 配置:/META-INF/spring.factories,eg: the spring.factories file under spring-boot-autoconfigure-xxx.jar
        # Auto Configure
        org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
        org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
        org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
        org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\
        org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
        ...
      
  2. 嵌入式Web容器:

    • Web Servlet: TomcatJettyUndertow
    • Web Reactive: Netty Web Server
  3. 生产准备特性:

    • dependency: spring-boot-starter-actuator
    • application.properties:
        # 开放所有的Web Endpoints
        # management.endpoints.web.exposure.include=*
        management.endpoints.web.exposure.include=health,info
      
    • visit /actuator (eg:http://localhost:8080/actuator )
    • jconsole
    • 端点:各类Web和JMX Endpoints
      • 指标: 内建Metrics,自定义Metrics (visit: /actuator/metrics )
      • 健康检查: Health,HealthIndicator (visit /actuator/health )
      • 外部化配置 (visit /actuator/configprops )

Web

  1. 传统Servlet

    • jar: javax.servlet-api (或embed server jar,eg: tomcat-embed-core)
    • 核心组件:Servlet,Filter,Listener
    • 注入:
      • Servlet注解: @WebServlet,@WebFilter,@WebListener
      • Spring Bean: @Bean
      • SpringBoot:RegistrationBean,@ServletComponentScan,ServletContextInitializer
    • Servlet 3.0 规范:加入异步Servlet
    • Servlet 3.1 规范:加入非阻塞Servlet
  2. Spring WebMVC

    • jar: spring-webmvc (dependency: spring-web)
    • 核心组件(jar: spring-webmvc, package:o.s.w.servlet...):
      • 总控 DispatcherServlet
      • 处理器管理
        • 映射 HandlerMapping (eg: RequestMappingHandlerMapping)
        • 适配 HandlerAdapter (eg: RequestMappingHandlerAdapter)
        • 执行 HandlerExecutionChain
      • 异常解析器 HandlerExceptionResolver
      • HandlerMethod: @RequestMapping/@XxxMapping标注的方法
        • HandlerMethod参数解析器: HandlerMethodArgumentResolver
        • HandlerMethod返回值解析器: HandlerMethodReturnValueHandler
      • 视图解析器 ViewResolver
        • 国际化 LocaleResolver,LocaleContextResolver
        • 个性化 ThemeResolver
        • 内容协商 ContentNegotiatingViewResolver
        • 默认 InternalResourceViewResolver
      • 视图渲染 View, eg:
        • RedirectView
        • JstlView
        • ThymeleafView
      • 配置
        • WebMvcConfigurer
        • ContentNegotiationConfigurer
    • 注解:
      • @Controller,@ControllerAdvice,@ExceptionHandler
      • @RequestMapping@GetMapping,@PostMapping,...)
      • @RequestBody,@RequestParm,@RequestHeader,@PathVariable,@CookieValue
      • @RestController,@RestControllerAdvice,@ResponseBody(for Rest)
      • @ModelAttribute,@Valid,@Validated
  3. Spring WebFlux

    • jar: spring-webflux (dependency: spring-web,reactor-core)
    • Reactive思想的一种实现(数据流Data Stream,非阻塞 Non-Blocking,推模式push-based,背压Backpressure
    • 依赖Reactor类库(jar:reactor-core)
      • 依赖reactive-streams
        • 核心组件:Publisher,Subscriber,Subscription,Processor
        • 背压处理
      • 核心组件:Mono,Flux,Scheduler
    • 依赖SpringWeb类库(jar: spring-web)
      • HttpHandler: Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response);
      • WebHandler: Mono<Void> handle(ServerWebExchange exchange);
    • 编程模型:
    • 核心组件(功能大同WebMvc, jar: spring-webflux, package:o.s.w.reactive...):
      • 总控 DispatcherHandler (implements WebHandler)
      • 处理器管理
        • 映射 HandlerMapping (eg: RequestMappingHandlerMapping)
        • 适配 HandlerAdapter (eg: RequestMappingHandlerAdapter)
        • 执行 HandlerResult
      • 异常处理 HandlerResult#exceptionHandler
      • HandlerMethod: @RequestMapping/@XxxMapping标注的方法
        • HandlerMethod参数解析器: HandlerMethodArgumentResolver
        • Handler返回值解析器: HandlerResultHandler
          • ResponseBodyResultHandler
          • ResponseEntityResultHandler
          • ServerResponseResultHandler
          • ViewResolutionResultHandler
            • RequestedContentTypeResolverBuilder
            • RequestedContentTypeResolver
      • 视图解析器 ViewResolver
      • 视图渲染 View, eg:
        • RedirectView
        • FreeMarkerView
        • HttpMessageWriterView
      • 配置
        • WebFluxConfigurer
        • WebFluxConfigurationSupport
      • for Functional Endpoints(函数式端点方式)
        • RouterFunction
        • RouterFunctionMapping (implements HandlerMapping)
        • HandlerFunctionAdapter (implements HandlerAdapter)
    • 使用场景:不适合RT(响应)敏感的RPC框架/Web应用,适合请求慢慢执行的场景(把请求丢过来,不care什么时候完成,完成后通知你下就即可),Reactive可以提升吞吐量,但RunTime反而会变得更长,且出现响应超时等问题
    • Spring WebMVC vs. Spring WebFlux
      • 均能使用注解驱动Controller,WebFlux还能使用函数式端点方式
      • 主要不同点在于并发模型和阻塞特性:
        • Spring WebMvc: Servlet应用,通常是阻塞服务,Servlet容器一般使用较大的线程池处理请求
        • Spring WebFlux: Reactive应用,通常是非阻塞服务,服务器可使用少量、固定大小的线程池处理请求
      • 目前WebFlux并未优于WebMvc:
        • 性能上没有明显的速度提升(甚至性能结果稍微更恶劣)
        • 在编程友好性方面,Reactive编程尽管没有新增大量的代码,却使编码(和调试)变得复杂了
        • 缺少文档

基本使用

Hello World(使用嵌入式容器)

  1. 创建SpringBoot项目

    • 方式一: 使用 start spring
        // 1. 网页中配置GAV:
        Spring Boot: 2.0.0
        Group: com.cj
        Artifact: springboot-demo-first
        Dependencies: Reactive Web
        //=> Generate Project
        // 2. Download project and import to IDEA 
        import project from external model -> Maven
      
    • 方式二:使用 Maven命令行
        # 1. mvn archetype:generate 创建项目
        > mvn archetype:generate -DinteractiveMode=false -DgroupId=com.cj -DartifactId=springboot-demo-second -Dversion=0.0.1-SNAPSHOT
        # 2. import the project to IDEA
      
      • 方式三:使用IDEA的Spring Initializr工具创建(Community版本不支持)
        File -> New -> Project -> Spring Initializr
        Initializr Server URL: http://start.spring.io -> Next -> 输入GAV等配置信息
        
  2. 配置依赖包(pom.xml )

    • basic
      • spring-boot-starter:核心模块,包括自动配置支持、日志和YAML,
      • spring-boot-starter-test:测试模块,包括JUnit、Hamcrest、Mockito
    • web
      • spring-boot-starter-web: 引入Web模块(支持SpringMVC),默认内嵌tomcat(by including: spring-boot-starter-tomcat)
      • spring-boot-starter-webflux:引入webflux模块(支持Reactive),默认内嵌Reactor Netty(by including:spring-boot-starter-reactor-netty)
    • embed web server -- no need to deploy WAR files , refer How to use another web-server
      • spring-boot-starter-tomcat
      • spring-boot-starter-jetty
      • spring-boot-starter-undertow
      • spring-boot-starter-reactor-netty
    • plugin

      • spring-boot-maven-plugin
    • Sample1: a typical pom

        <!-- Inherit defaults from Spring Boot -->
        <parent>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-parent</artifactId>
          <version>2.0.0.RELEASE</version>
        </parent>
      
        <!-- Add typical dependencies for a web application,也可使用spring-boot-starter-webflux -->
        <dependencies>
          <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-web</artifactId>
          </dependency>
        </dependencies>
      
        <!-- Package as an executable jar -->
        <build>
          <plugins>
            <plugin>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
          </plugins>
        </build>
      
    • Sample2: use another web server
        <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-web</artifactId>
              <exclusions>
                  <!-- Exclude the Tomcat dependency -->
                  <exclusion>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-starter-tomcat</artifactId>
                  </exclusion>
              </exclusions>
        </dependency>
        <!-- Use Jetty instead -->
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-jetty</artifactId>
        </dependency>
      
  3. Coding

     // 1. App.java
     @SpringBootApplication
     public class App {
         public static void main( String[] args ){
             SpringApplication.run(App.class, args);
         }
     }
    
     // 2. controller/HelloWorldController.java
     @RestController
     public class HelloWorldController {
         @GetMapping("")
         public String index(){
             return "Hello World!";
         }
     }
    
  4. 运行/发布

    • 方式一: IDEA中run App.java (适用于:开发环境调试 )
    • 方式二:打Jar/War包运行 (适用于: 生产环境,使用脚本启动 )

        # 1.打包 ( -U  表示更新maven包)
        cd myProject
        mvn -Dmaven.test.skip -U clean package
        cd web/target
      
        # 2. 运行jar
        # pom.xml中配置packaging为jar
        java -jar myProject-xxx.jar
      
        # 3. 运行war
        # pom.xml中配置packaging为war (需要有webapp/WEB-INFO/web.xml文件),也可将war包放入外部tomcat中运行
        java -jar myProject-xxx.war
      
    • 方式三:Maven Plugin运行 (适用于: 无图形化界面的开发环境 )

        # 1.打包安装到本地仓库
        cd myProject
        mvn -Dmaven.test.skip -U clean install
      
        # 2.运行
        mvn spring-boot:run
      
  5. visit http://localhost:8080 to verify -> should show "Hello World!" in the page

Hello World(使用独立容器)

  1. pom.xml

     <packaging>war</packaging>
     <dependencies>
         <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
           <exclusions>
               <!-- Exclude the Tomcat dependency -->
               <exclusion>
                   <groupId>org.springframework.boot</groupId>
                   <artifactId>spring-boot-starter-tomcat</artifactId>
               </exclusion>
           </exclusions>
         </dependency>
         <!-- Servlet 3.1 API 依赖-->
         <dependency>
           <groupId>javax.servlet</groupId>
           <artifactId>javax.servlet-api</artifactId>
           <scope>provided</scope>
         </dependency>
     </dependencies>
     <build>
         <plugins>
           <plugin>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-maven-plugin</artifactId>
           </plugin>
         </plugins>
     </build>
    
  2. Coding

     // 1. SpringBootServletInitializer(implements WebApplicationInitializer)
     public class MySpringBootServletInitializer extends SpringBootServletInitializer{
    
          protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
              System.out.println("MySpringBootServletInitializer configure");
              builder.sources(App.class);
              return builder;
          }
      }
    
      @SpringBootApplication
      public class App {
      }
    
     // 2. controller/HelloWorldController.java
     @RestController
     public class HelloWorldController {
         @GetMapping("")
         public String index(){
             return "Hello World!";
         }
     }
    
  3. 打包运行

     cd myProject
     mvn -Dmaven.test.skip -U clean package
     # 将target下生成的war包丢到Servlet容器运行(eg:Tomcat)
    
  4. 也可使用maven插件直接打包运行,eg: 使用Jetty plugin,运行jetty:run

     <!-- pom.xml 中增加 jetty plugin -->
     <plugin>
         <groupId>org.eclipse.jetty</groupId>
         <artifactId>jetty-maven-plugin</artifactId>
         <version>9.4.12.v20180830</version>
         <configuration>
          <scanIntervalSeconds>10</scanIntervalSeconds>
           <webApp>
            <contextPath>/demo</contextPath>
          </webApp>
          <httpConnector>
              <port>9090</port>
              <idleTimeout>60000</idleTimeout>
          </httpConnector>
          <stopKey>foo</stopKey>
             <stopPort>9999</stopPort>
         </configuration>
     </plugin>
    

Hello World

Summary

  1. 嵌入式容器

    • package:war/jar
    • dependency jar: spring-boot-starter-web (default including spring-boot-starter-tomcat,也可exclude default,then include other embed servlet container)
    • coding: @SpringBootAplication + main()
    • run main 或者 java -jar xxx运行可执行包 或者 mvn spring-boot:run
  2. 独立容器

    • package:war
    • dependency jar:spring-boot-starter-web (exlude default embed tomcat) + javax.servlet-api<scope>provided</scope>(or include other container include servlet api,eg: spring-boot-starter-tomcat)
    • coding: @SpringBootApplication + extends SpringBootServletInitializer(implements WebApplicationInitializer)
    • 将war包发布到tomcat容器运行 或者 使用maven jetty plugin jetty:run

eg: 整合上面两种运行方式

  1. pom.xml

     <packaging>war</packaging>
     <dependencies>
         <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
           <exclusions>
               <!-- Exclude the Tomcat dependency -->
               <exclusion>
                   <groupId>org.springframework.boot</groupId>
                   <artifactId>spring-boot-starter-tomcat</artifactId>
               </exclusion>
           </exclusions>
         </dependency>
         <!-- 兼容嵌入式运行(embed tomcat 中包含了servlet api)-->
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-tomcat</artifactId>
             <scope>provided</scope>
         </dependency>
     </dependencies>
     <build>
         <plugins>
           <plugin>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-maven-plugin</artifactId>
           </plugin>
         </plugins>
     </build>
    
  2. Coding

     // 1. 使用嵌入式Servlet容器:App main 运行
     @SpringBootApplication
     public class App {
         public static void main( String[] args ){
                 SpringApplication.run(App.class, args);
             }
     }
    
     // 2. 使用独立Servlet容器:打包发布到运行的Servlet容器
     // extends SpringBootServletInitializer(implements WebApplicationInitializer)
     public class MySpringBootServletInitializer extends SpringBootServletInitializer{
      protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
          System.out.println("MySpringBootServletInitializer configure");
          builder.sources(App.class);
          return builder;
      }
     }
    

热部署

监听classpath下的文件(class类,配置文件)变化,自动重启应用 (默认静态资源改变:/public/resource/META-INF,...,会重新加载,但不会重启应用)

  • 方式一:使用springloaded

      <plugins>
        <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <dependencies>
        <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>springloaded</artifactId>
        <version>1.2.8.RELEASE</version>
        </dependency>
        </dependencies>
        </plugin>
      </plugins>
    
  • 方式二:使用spring-boot-devtools

      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-devtools</artifactId>
         <optional>true</optional>
      </dependency>
    
    • 额外配置示例:

        #热部署生效
        spring.devtools.restart.enabled: true
      
        #设置重启的目录
        #spring.devtools.restart.additional-paths: src/main/java
        #classpath目录下的WEB-INF文件夹内容修改不重启
        spring.devtools.restart.exclude: WEB-INF/**
      
  • 启动运行:

    • 方式一: mvn spring-boot:run
    • 方式二:直接运行main方法
  • 注:热部署自身不会去主动编译 Java 文件

  • 示例:配置在Java 文件改动时,自动编译成 Class 文件(然后热部署工具创造的新的类加载器才会加载改变后的 Class 文件)

    • IDEA :使用工具时配置自动编译:
        1. File -> Settings -> Complier -> 勾选Make project automatically
        2. Shift+Ctrl+Alt+/,Shift+Ctrl+A -> Registry -> 勾选compiler.automake.allow.when.app.running
      
      • Eclipse
        <plugins>
             <plugin>
             <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
             <fork>true</fork>
        </configuration>
        </plugin>
        </plugins>
        

热部署 VS. 热加载

  1. 共点:编译/部署项目不重启服务器,都基于Java的类加载器实现 (在容器启动时,启动一后台线程,定时监测class文件的时间戳,判断是否变化了)

  2. 热部署:在服务器运行时重新部署项目 —— 多用于生产环境 (会释放内存,比热加载更加干净彻底)

  3. 热加载:在运行时重新加载改变的class —— 多用于开发环境 ( 直接修改字节码的方式,难以监控和记录逻辑变化)

配置Tomcat实现热部署的三种方式:

  • 方式一:直接将项目(解压后的web项目)放在tomcat的webapp目录下

  • 方式二:%tomcat_home%/conf/server.xml<host>内添加<context>配置

       <Context debug="0" docBase="D:/space/web" path="/hot" privileged="true" reloadable="true"/>
       <!-- 
        注意配置了path(虚拟路径),访问网址需使用此项目名/hot
        将项目目录下的内容(META-INF,WEB-INF,...)copy到docBase的指定路径
       -->
    
  • 方式三:%tomcat_home%/conf/Catalina/localhost目录下添加一个xml文件,eg: hot.xml

      <?xml version="1.0" encoding="UTF-8"?>
      <Context docBase="D:/space/web" reloadable="true"/>
      <!-- 注意xml文件名为hot,则访问网址使用此项目名/hot -->
    

单元测试

  1. Test Service:

     @RunWith(SpringRunner.class)
     @SpringBootTest(classes=TagServiceTest.class)
     public class TagServiceTest {
    
         @Autowired
         private TagService tagService;
    
         @Test
         @Transactional
         public void listTest(){
             List<Tag> list=this.tagService.list("Benefit");
             for(Tag item:list) {
                 System.out.println(item);
             }
         }
     }
    
  2. Test Controller:

     @RunWith(SpringRunner.class)
     @SpringBootTest
     @AutoConfigureMockMvc
     public class HelloControllerTest {
    
         @Autowired
         private MockMvc mvc;
    
         @Test
         public void sayHello() throws Exception {
             mvc.perform(MockMvcRequestBuilders.get("/say").accept(MediaType.APPLICATION_JSON))
                 .andExpect(status().isOk())
                 .andExpect(content().string(equalTo("Hello world!")))
                 ;
         }
     }
    
  3. Test Web:

     @RunWith(SpringRunner.class)
     @SpringBootTest(webEnvironment=SpringBootTest.WebEnvironment.RANDOM_PORT)
     public class HelloControllerIT {
    
         @LocalServerPort
         private int port;
    
         private URL base;
    
         @Autowired
         private TestRestTemplate template;
    
         @Before
         public void setup() throws MalformedURLException {
             this.base=new URL("http://localhost:"+port+"/hello");
             System.out.println(base);
         }
    
         @Test
         public void say() throws Exception {
             ResponseEntity<String> response=template.getForEntity(base.toString()+"/say", String.class);
             assertThat(response.getBody(),equalTo("Hello world!"));
         }
     }
    

属性配置

Refer Common application properties

  1. 配置文件:resources/application.propertiesresources/application.yml

  2. 注解:

    • @Value, eg: @Value(${content})
    • @ConfigurationProperties,eg: @ConfigurationProperties(prefix = "hello")

示例1:@Value,@ConfigurationProperties使用

  1. 配置文件resources/application.yml

     say:
       prefix: "Hi"
       target: "Tom"
       times: 5
     content: "Test: ${say.prefix} ${say.target}"
    
  2. 获取注入配置的属性值

     // 1. 使用@ConfigurationProperties
     // config/SayProperties.java
     @Component
     @ConfigurationProperties(prefix = "say")
     public class SayProperties {
         private String prefix;
         private String target;
         private Integer times;
    
         /*getter and setter...*/
     }
    
     // 2. 使用@Value
     // controller/SayController
     @RestController
     public class SayController {
    
         @Value("${content}")
         private String content;
         @Autowired
         private SayProperties sayProperties;
    
         @GetMapping("/testSay")
         public String say(){
             return content+" -- "+sayProperties;
         }
     }
    
  3. visit http://localhost:8080/testSay to verify,shoud see: "Test: Hi Tom -- SayProperties [prefix=Hi, target=Tom, times=5]"

示例2:多环境配置(不同环境使用不同配置)

  1. resource/application-dev.yml

     server:
       port: 8080
       servlet:
         context-path: /hello
     say:
       prefix: "Hi"
       target: "DevEnv"
       times: 5
    
  2. resource/application-prod.yml

     server:
       port: 8090
       servlet:
         context-path: /hello
     say:
       prefix: "Hi"
       target: "ProdEnv"
       times: 5
    
  3. resource/application.yml

     spring:
       profiles:
         active: dev
     content: "Test: ${say.prefix} ${say.target}"
    
  4. Verify

     cd springboot-helloworld
     mvn -Dmaven.test.skip -U clean package
    
     # Note:cmd line args的优先级高于properties file的配置
    
     # test dev
     # visit: http://localhost:8080/hello/testSay
     # should show: Test: Hi DevEnv -- SayProperties [prefix=Hi, target=DevEnv, times=5]
     java -jar target/springboot-helloworld-0.0.1-SNAPSHOT.jar
    
     # test prod 
     # visit: http://localhost:8090/hello/testSay
     # should show: Test: Hi ProdEnv -- SayProperties [prefix=Hi, target=ProdEnv, times=5]
     java -jar target/springboot-helloworld-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod
    

AOP

  • 一种编程范式,与语言无关,是一种程序设计思想,eg:
    • AOP 面向切面 Aspect Oriented Programming
    • OOP 面向对象 Object Oriented Programming
    • POP 面向过程 Procedure Oriented Programming
  • 从面向过程到面向对象,换了个角度看世界,换个姿势处理问题
  • AOP 将通用逻辑从业务逻辑中分离出来
  • 应用:AOP统一处理请求日志,记录每一个http请求
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

aspect/HttpAspect.java

@Aspect
@Component
public class HttpAspect{

    @PointCut("execution(...)")
    public void log(){}

    @Before("log()")
    public void doBefore(JoinPorint joinPoint ){
        ...
    }
}

异常处理

@ExceptionHandler

@Controller
public class HelloController {
    @RequestMapping(value="/{num}",method=RequestMethod.GET)
    public String doExTest(@PathVariable int num,Model model){
        model.addAttribute("message","number:"+num);
        return "index";
    }

    @ExceptionHandler(Throwable.class)
    public ResponseEntity<String> onException(Throwable ex){
        return ResponseEntity.ok("handle exception:"+ex.getMessage());
    }
}

或者:

@Controller
public class HelloController {
    @RequestMapping(value="/{num}",method=RequestMethod.GET)
    public String doExTest(@PathVariable int num,Model model){
        model.addAttribute("message","number:"+num);
        return "index";
    }
}

@ControllerAdvice(assignableTypes=HelloController.class)
public class HelloControllerAdvice {

    @ExceptionHandler(Throwable.class)
    public ResponseEntity<String> onException(Throwable ex){
        return ResponseEntity.ok("handle exception:"+ex.getMessage());
    }
}

装配

  1. Spring装配技术:

    • 模式注解装配:@Component,@Repository,@Service,@Controller,@Configuration
    • Enable模块装配: @Enable模块
    • 条件装配:@Profile,@Conditional
    • 工厂加载机制: SpringFactoriesLoader + META-INF/spring.factories
  2. SpringBoot自动装配:

    • 依赖Spring装配技术,基于约定大于配置的原则,实现自动装配
    • 实现步骤:
      • 激活自动装配 @EnableAutoConfiguration
      • 实现装配 XxxAutoConfiguration
      • 配置 META-INF/spring.factories

Spring 装配技术

模式注解装配

  1. 模式注解(Stereotype Annotations):

    an annotation that is used to declare the role that a component plays within the application.

  2. 模式注解装配:@ComponentScan/<context:component-scan>

    扫描并找到@Component或者其派生Annotation(eg: @Repository,@Service,@Controller,@Configuration) 标注的Class,将它们注册为Spring Bean

  3. 使用示例:

    • 使用<context:component-scan>
        <?xml version="1.0" encoding="UTF-8"?>
        <beans xmlns="http://www.springframework.org/schema/beans"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xmlns:context="http://www.springframework.org/schema/context"
            xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context 
            http://www.springframework.org/schema/context/springcontext.xsd">
            <!-- 激活注解驱动特性 -->
            <context:annotation-config />
            <!-- 找到指定package下@Component及其派生标注的类,注册为SpringBean对象 -->
            <context:component-scan base-package="com.cj.demo" />
        </beans>
      
    • 使用@ComponentScan
        @ComponentScan(basePackages="com.cj.demo")
        public class HelloWorldSpringScanApplication {
             public static void main(String[] args) {
                ConfigurableApplicationContext context = new SpringApplicationBuilder(HelloWorldSpringScanApplication.class)
                    .web(WebApplicationType.NONE)
                    .run(args);                    
                Hello hello = context.getBean("hello",Hello.class);
                System.out.println("Hello Bean : "+hello);    
                context.close();
            }
        }
      

模块装配

@Enable模块 (模块:具备相同领域的功能组件集合, 组合所形成一个独立的单元)

  1. Spring Framework 3.1 开始支持,eg:

    框架实现 @Enable注解模块 激活模块
    Spring Framework @EnableWebMvc Web MVC 模块
    @EnableTransactionManagement 事务管理模块
    @EnableCaching Caching(缓存)模块
    @EnableMBeanExport JMX(Java管理扩展)模块
    @EnableAsync 异步处理模块
    @EnableWebFlux Web Flux 模块
    @EnableAspectJAutoProxy AspectJ 代理模块
    Spring Boot @EnableAutoConfiguration 自动装配模块
    @EnableManagementContext Actuator 管理模块
    @EnableConfigurationProperties 配置属性绑定模块
    @EnableOAuth2Sso OAuth2 单点登录模块
    Spring Cloud @EnableEurekaServer Eureka服务器模块
    @EnableConfigServer 配置服务器模块
    @EnableFeignClients Feign客户端模块
    @EnableZuulProxy 服务网关 Zuul 模块
    @EnableCircuitBreaker 服务熔断模块
  2. 实现方式:

    • 注解驱动方式,eg: @EnableWebMvc

        @Retention(RetentionPolicy.RUNTIME)
        @Target(ElementType.TYPE)
        @Documented
        @Import(DelegatingWebMvcConfiguration.class) // import: Configuration class
        public @interface EnableWebMvc {
        }
      
        @Configuration
        public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
            //@Bean,eg:
            // RequestMappingHandlerMapping
            // RequestMappingHandlerAdapter
            // ContentNegotiationManager
            // ...
        }
      
    • 接口编程方式,eg: @EnableCaching

        @Target(ElementType.TYPE)
        @Retention(RetentionPolicy.RUNTIME)
        @Documented
        @Import(CachingConfigurationSelector.class) // import: interface ImportSelector 实现类,用于获取Configuration类名
        public @interface EnableCaching {
            //...
        }
      
        public interface ImportSelector {
            /**
             * Select and return the names of which class(es) should be imported based on
             * the {@link AnnotationMetadata} of the importing @{@link Configuration} class.
             */
            String[] selectImports(AnnotationMetadata importingClassMetadata);
        }
      
        public class CachingConfigurationSelector 
            extends AdviceModeImportSelector<EnableCaching> { 
            // Note: AdviceModeImportSelector implements ImportSelector
      
            // 实现interface ImportSelector#selectImports(AnnotationMetadata importingClassMetadata)
            // 返回要import的Congifuration类名
            // eg:
            // return new String[] { AutoProxyRegistrar.class.getName(), ProxyCachingConfiguration.class.getName() };
            // return new String[] { AnnotationConfigUtils.CACHE_ASPECT_CONFIGURATION_CLASS_NAME };
        }
      
  3. 示例:自定义一个@EnableHelloWorld并使用

    • 方式一:@EnableHelloWorld Import Configuration class

        @Retention(RetentionPolicy.RUNTIME)
        @Target(ElementType.TYPE)
        @Documented
        @Import(HelloWorldConfiguration.class)
        public @interface EnableHelloWorld {
        }
      
        //@Configuration
        public class HelloWorldConfiguration {
      
            // @Bean:
            // 方法级别上的注解,产生一个Bean对象并交给Spring管理,默认bean的名称就是其方法名
            // 好处:
            // 1. 动态获取一个Bean对象
            // 2. 可将非Spring组件交给Spring管理
            @Bean
            public String helloWorld() {
                return new String("Hello world!");
            }
      
            @Bean
            public HelloController helloController() {
                return new HelloController();
            }
        }
      
    • 方式二:@EnableHelloWorld Import ImportSelector implements Class that used to return Configuration Classname (适合需要使用多个Configuration类的情况)

        @Retention(RetentionPolicy.RUNTIME)
        @Target(ElementType.TYPE)
        @Documented
        @Import(HelloWorldImportSelector.class)
        public @interface EnableHelloWorld {
        }
      
        public class HelloWorldImportSelector implements ImportSelector {
            @Override
            public String[] selectImports(AnnotationMetadata importingClassMetadata) {
                // 更灵活,例如可在这里加入判断分支
                return new String[]{HelloWorldConfiguration.class.getName()};
            }
        }
      
        // HelloWorldConfiguration 内容同上
        public class HelloWorldConfiguration {
            // @Bean
            // ...
        }
      
    • 测试使用@EnableHelloWorld

        @EnableHelloWorld
        public class HelloWorldSpringEnableApplication {
            public static void main(String[] args) {
                ConfigurableApplicationContext context = new SpringApplicationBuilder(HelloWorldSpringScanApplication.class)
                    .web(WebApplicationType.NONE)
                    .run(args);
      
                HelloController helloController = context.getBean("helloController",HelloController.class);
                System.out.println("helloController Bean : "+helloController);    
      
                String helloWorld = context.getBean("helloWorld",String.class);
                System.out.println("helloWorld Bean : "+helloWorld);    
      
                context.close();
            }
        }
        // Result:
        // helloController Bean : com.cj.outside.HelloController@12164beb
        // helloWorld Bean : Hello world!
      

条件装配

Bean装配时根据前置判断是否装配该Bean

  • 实现方式:
    • 注解方式: 使用@Profile 配置化条件装配(Spring 3.1+)
    • 编程方式: 使用@Conditional 编程条件装配 (Spring 4.0+)
  1. @Profile 配置化条件装配(Spring 3.1+)

     @Profile("Java7")
     @Service
     public class Java7CalculateService implements CalculateService {
    
         @Override
         public Integer sum(Integer... values) {
             Integer sum=0;
             for(Integer item:values) {
                 sum+=item;
             }
             System.out.println("Java7 sum result:"+sum);
             return sum;
         }
     }
    
     @Profile("Java8")
     @Service
     public class Java8CalculateService implements CalculateService {
    
         @Override
         public Integer sum(Integer... values) {
    
             Integer sum=Stream.of(values).reduce(0, Integer::sum);
             System.out.println("Java8 sum result:"+sum);
             return sum;
         }
     }
    
     public interface CalculateService {
         public Integer sum(Integer...values);
     }
    
     // Test
     @ComponentScan(basePackages="com.cj.autoconfig.service")
     public class ProfileApplication {
         public static void main(String[] args) {
             ConfigurableApplicationContext context = new SpringApplicationBuilder(ProfileApplication.class)
                 .web(WebApplicationType.NONE)
                 .profiles("Java8")        // switch Profile
                 .run(args);
    
             CalculateService calculateService = context.getBean(CalculateService.class);
             System.out.println("calculateService Bean : "+calculateService);    
             calculateService.sum(1,2,3,4,5,6,7,8,9,10);
    
             context.close();
         }
     }
    
  2. @Conditional 编程条件装配 (Spring 4.0+)

     // 自定义一个Annotation
     @Retention(RetentionPolicy.RUNTIME)
     @Target({ ElementType.TYPE, ElementType.METHOD })
     @Documented
     @Conditional(SystemPropertyCondition.class)
     public @interface ConditionalOnSystemProperty {
         String name() default "";
         String value() default "";
     }
    
     // Condition实现类
     public class SystemPropertyCondition implements Condition {
         @Override
         public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
             Map<String, Object> attrs= metadata.getAnnotationAttributes(ConditionalOnSystemProperty.class.getName());
             String name=String.valueOf(attrs.get("name"));
             String value=String.valueOf(attrs.get("value"));
             String systemPropertyValue=System.getProperty(name);
             System.out.println("condition--"+value+" vs. "+systemPropertyValue);
             return systemPropertyValue.equals(value);
         }
     }
    
     // Test
     public class ConditionApplication {
    
         @Bean
         @ConditionalOnSystemProperty(name="user.name",value="jinc")
         public String helloWorld() {
             return "Hello world!";
         }
    
         public static void main(String[] args) {
             ConfigurableApplicationContext context = new SpringApplicationBuilder(ConditionApplication.class)
                     .web(WebApplicationType.NONE)
                     .run(args);
    
             String helloWorld = context.getBean("helloWorld",String.class);    
             System.out.println("helloWorld Bean:"+helloWorld);
    
             context.close();
         }
     }
    

工厂加载机制

使用SpringFactoriesLoader

public abstract class SpringFactoriesLoader {
    public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
    public static <T> List<T> loadFactories(Class<T> factoryClass, @Nullable ClassLoader classLoader) {
        ClassLoader classLoaderToUse = classLoader!=null? classLoader:SpringFactoriesLoader.class.getClassLoader();
        List<String> factoryNames = loadFactoryNames(factoryClass, classLoaderToUse);
        List<T> result = new ArrayList<>(factoryNames.size());
        for (String factoryName : factoryNames) {
            result.add(instantiateFactory(factoryName, factoryClass, classLoaderToUse));
        }
        AnnotationAwareOrderComparator.sort(result);
        return result;
    }
}
  • 方法:
    • SpringFactoriesLoader#loadFactoryNames : 读取配置文件获取要装载的类名
    • SpringFactoriesLoader#loadFactories : 按顺序装载配置的类的实例
  • 配置:META-INF/spring.factories
  • 排序:AnnotationAwareOrderComparator#sort (要装载的类可通过注解@Orderimplements Ordered来配置顺序)
  • 使用示例:SpringApplication#getSpringFactoriesInstances

SpringBoot自动装配

依赖Spring装配技术,基于约定大于配置的原则,实现自动装配

  1. 实现步骤:

    • 实现装配配置类 XxxAutoConfiguration
        @Configuration
        public class XxxxConfiguration {
            //@Bean
            // ...
        }
      
    • 配置 META-INF/spring.factories到resources下
        # Auto Configure
        org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
        xxx.xxx....xxx.XxxAutoConfiguration,\
        #...
      
    • 激活自动装配 @EnableAutoConfiguration
        @SpringBootApplication     // @SpringBootApplication中使用了@EnableAutoConfiguration,会激活自动装配
        public class App {
            SpringApplication.run(App.class, args)
        }
      
  2. 源码分析:SpringBoot自动装配WebMvc模块

    • @EnableAutoConfiguration: Spring@Enable模块装配技术

        @Target(ElementType.TYPE)
        @Retention(RetentionPolicy.RUNTIME)
        @Documented
        @Inherited
        @AutoConfigurationPackage
        @Import(AutoConfigurationImportSelector.class)    //  import: interface ImportSelector 实现类,用于获取Configuration类名 
        public @interface EnableAutoConfiguration {
            String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
            Class<?>[] exclude() default {};
            String[] excludeName() default {};
        }
      
        public class AutoConfigurationImportSelector
                implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware,
                BeanFactoryAware, EnvironmentAware, Ordered {
            //...
            @Override
            public String[] selectImports(AnnotationMetadata annotationMetadata) {
                if (!isEnabled(annotationMetadata)) {
                    return NO_IMPORTS;
                }
                AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
                                                        .loadMetadata(this.beanClassLoader);
                AnnotationAttributes attributes = getAttributes(annotationMetadata);
                List<String> configurations = getCandidateConfigurations(annotationMetadata,attributes);
                    =>{
                        List<String> configurations = SpringFactoriesLoader
                                .loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());        
                            // 使用Spring工厂加载机制(读取配置文件`META-INF/spring.factories`)获取Configuration类名
                        Assert.notEmpty(configurations,
                                "No auto configuration classes found in META-INF/spring.factories. If you "
                                + "are using a custom packaging, make sure that file is correct.");
                        return configurations;
                    }
                // remove duplicates,remove exclusions,filter,fire imported events of exclusions
                //..
                return StringUtils.toStringArray(configurations);
            }    
        }
      
        // => `SpringFactoriesLoader` : 
        public abstract class SpringFactoriesLoader {
            public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
            public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
                String factoryClassName = factoryClass.getName();
                return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
            }
            //...
        }
      
        // => `META-INF/spring.factories` (jar: spring-boot-autoconfigure): 配置装载了`WebMvcAutoConfiguration`配置类
        # Auto Configure
        org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
        org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
        org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
        org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\
        org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
        #...
      
    • WebMvcAutoConfiguration:负责WebMvc相关Bean装配的Configure类
        @Configuration                            // 使用了Spring模式装配
        @ConditionalOnWebApplication            // 使用了Spring 条件装配
        @ConditionalOnClass({ Servlet.class, DispatcherServlet.class,WebMvcConfigurerAdapter.class })
        @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
        @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
        @AutoConfigureAfter(DispatcherServletAutoConfiguration.class)
        public class WebMvcAutoConfiguration {
            //@Bean
        }
      
    • @SpringBootApplication中使用了@EnableAutoConfiguration,使用@SpringBootApplication就会激活使用自动装配这个功能模块

        @Target(ElementType.TYPE)
        @Retention(RetentionPolicy.RUNTIME)
        @Documented
        @Inherited
        @SpringBootConfiguration
        @EnableAutoConfiguration            // 使用了自动装配模块
        @ComponentScan(excludeFilters = {
                @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
                @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
        public @interface SpringBootApplication {
            @AliasFor(annotation = EnableAutoConfiguration.class)
            Class<?>[] exclude() default {};
      
            @AliasFor(annotation = EnableAutoConfiguration.class)
            String[] excludeName() default {};
      
            @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
            String[] scanBasePackages() default {};
      
            @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
            Class<?>[] scanBasePackageClasses() default {};
        }
      
  3. 示例:自定义一个配置类,实现自动装配

    1. 自定义一个配置类 HelloWorldAutoConfiguration

      • 方式一:
          @Configuration
          @ConditionalOnSystemProperty(name="user.name",value="jinc")
          public class HelloWorldAutoConfiguration {
              @Bean
              public String helloWorld() {
                  System.out.println("new bean:helloWrold");
                  return new String("Hello world!");
              }
              @Bean
              public HelloController helloController() {
                  System.out.println("new bean:helloController");
                  return new HelloController();
              }
          }
        
      • 方式二:使用了Spring的@Enable模块装配技术

          @Configuration
          @EnableHelloWorld                                            // Spring @Enable模块装配
          @ConditionalOnSystemProperty(name="user.name",value="jinc")
          public class HelloWorldAutoConfiguration {
          }
        
          @Retention(RetentionPolicy.RUNTIME)
          @Target(ElementType.TYPE)
          @Documented
          @Import(HelloWorldConfiguration.class)
          public @interface EnableHelloWorld {
        
          }
        
          public class HelloWorldImportSelector implements ImportSelector {
              @Override
              public String[] selectImports(AnnotationMetadata importingClassMetadata) {
                  System.out.println("HelloWorld--selectImports");
                  return new String[] {HelloWorldConfiguration.class.getName()};
              }
          }
          public class HelloWorldConfiguration {
              @Bean
              public String helloWorld() {
                  System.out.println("new bean:helloWrold");
                  return new String("Hello world!");
              }
              @Bean
              public HelloController helloController() {
                  System.out.println("new bean:helloController");
                  return new HelloController();
              }
          }
        
    2. resources/META-INF/spring.factories中配置自定义的AutoConfiguration类
       org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
       com.cj.autoconfig.auto.HelloWorldAutoConfiguration
      
    3. 激活自动装配,并测试

       @EnableAutoConfiguration                // 激活自动装配 或使用@SpringBootApplication也可
       public class HelloWorldAutoApplication {
           public static void main(String[] args) {
               ConfigurableApplicationContext context = new SpringApplicationBuilder(HelloWorldAutoApplication.class)
                   .web(WebApplicationType.NONE)
                   .run(args);
      
               HelloController helloController = context.getBean("helloController",HelloController.class);
               System.out.println("helloController Bean : "+helloController);    
      
               String helloWorld = context.getBean("helloWorld",String.class);
               System.out.println("helloWorld Bean : "+helloWorld);    
      
               context.close();
           }
       }
      

SpringApplication

SpringApplication : SpringBoot启动运行 (jar:spring-boot)

使用

  1. SpringApplication.run

     ConfigurableApplicationContext ctx=SpringApplication.run(ApplicationConfiguration.class, args);
    
  2. new SpringApplication

     //SpringApplication app = new SpringApplication(ApplicationConfiguration.class);
     SpringApplication app = new SpringApplication();
     Set<String> sources = new HashSet<String>();
     sources.add(ApplicationConfiguration.class.getName());
     app.setSources(sources);
     app.setWebApplicationType(WebApplicationType.NONE);
     app.setAdditionalProfiles("Java8");
     ConfigurableApplicationContext context=app.run(args);
    
  3. new SpringApplicationBuilder

     ConfigurableApplicationContext context = new SpringApplicationBuilder(ApplicationConfiguration.class)
                     .web(WebApplicationType.NONE)    // NONE,SERVLET,REACTIVE
                     .profiles("Java8")
                     .run(args);
    

示例:

  1. 最简式:

     @SpringBootApplication
     public class DemoSpringApplication {
         public static void main(String[] args) {
             SpringApplication.run(DemoSpringApplication.class,args)
         }
     }
    
  2. 可配置式:

     public class DemoSpringApplication {
         public static void main(String[] args) {
    
             // 1
     //        ConfigurableApplicationContext context=SpringApplication.run(ApplicationConfiguration.class, args);
    
             // 2
     //        //SpringApplication app = new SpringApplication(ApplicationConfiguration.class);
     //        SpringApplication app = new SpringApplication();
     //        Set<String> sources = new HashSet<String>();
     //        sources.add(ApplicationConfiguration.class.getName());
     //        app.setSources(sources);
     //        app.setWebApplicationType(WebApplicationType.NONE);
     //        app.setAdditionalProfiles("Java8");
     //        ConfigurableApplicationContext context=app.run(args);
    
             // 3
             ConfigurableApplicationContext context = new SpringApplicationBuilder(ApplicationConfiguration.class)
                     .web(WebApplicationType.NONE)    // NONE,SERVLET,REACTIVE
                     .profiles("Java8")
                     .run(args);
    
             /*
              - WebApplicationType.NONE:
                 + ConfigurableApplicationContext: AnnotationConfigApplicationContext 
                 + Environment: StandardEnvironment
    
              - WebApplicationType.SERVLET:
                 + ConfigurableApplicationContext: AnnotationConfigServletWebServerApplicationContext
                 + Environment: StandardServletEnvironment
    
              - WebApplicationType.REACTIVE:
                 + ConfigurableApplicationContext: AnnotationConfigReactiveWebServerApplicationContext
                 + Environment: StandardEnvironment
             */
             System.out.println("ConfigurableApplicationContext 类型:" + context.getClass().getName());    
             System.out.println("Environment 类型:" + context.getEnvironment().getClass().getName());
    
             CalculateService calculateService = context.getBean(CalculateService.class);
             System.out.println("calculateService Bean : "+calculateService);    
             calculateService.sum(1,2,3,4,5,6,7,8,9,10);
    
             context.close();
         }
    
         @SpringBootApplication
         public static class ApplicationConfiguration {
    
         }
     }
    

启动过程

  1. 准备阶段(构造函数)

    • 1.1 推断WebApplicationType:根据当前应用ClassPath 中是否存在相关实现类来推断WebApplication的类型
      • WebApplicationType.REACTIVE: Web Reactive
      • WebApplicationType.SERVLET: Web Servlet
      • WebApplicationType.NONE: 非Web
    • 1.2 加载ApplicationContextInitializer上下文初始器: 利用 Spring 工厂加载机制,实现类实例化,并排序对象集合
    • 1.3 加载ApplicationListener 事件监听器: 利用 Spring 工厂加载机制,实现类实例化,并排序对象集合
    • 1.4 推断引导类(Main Class): 根据 Main 线程执行堆栈判断实际的引导类
  2. 运行阶段 (run函数)

    • 2.1 加载并运行SpringApplicationRunListeners运行监听器,监听SpringBoot/Spring事件
    • 2.2 创建 Environment: 根据准备阶段推断的WebApplicationType创建对应的ConfigurableEnvironment实例
      • Web Reactive: StandardEnvironment
      • Web Servlet: StandardServletEnvironment
      • 非 Web: StandardEnvironment
    • 2.3 创建Spring应用上下文:根据准备阶段推断的WebApplicationType创建对应的ConfigurableApplicationContext实例
      • Web Reactive: AnnotationConfigReactiveWebServerApplicationContext
      • Web Servlet: AnnotationConfigServletWebServerApplicationContext
      • 非 Web: AnnotationConfigApplicationContext
    • 2.4 加载SpringBootExceptionReporter故障分析报告(利用 Spring 工厂加载机制),供以后失败时使用
    • 2.5 加载Bean到应用上下文:加载Annotation/XML配置的Bean给Spring管理

源码分析

  1. constructor:

    • WebApplicationType Web应用类型
      • WebApplicationType.REACTIVE
      • WebApplicationType.SERVLET
      • WebApplicationType.NONE
    • ApplicationContextInitializer 上下文初始器
        public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {
            /**
             * Initialize the given application context.
             * @param applicationContext the application to configure
             */
            void initialize(C applicationContext);
        }
      
    • ApplicationListener 事件监听器
        @FunctionalInterface
        public interface ApplicationListener<E extends ApplicationEvent> 
            extends EventListener {
            /**
             * Handle an application event.
             * @param event the event to respond to
             */
            void onApplicationEvent(E event);
        }
      
    • Main Class
  2. run:

    • SpringApplicationRunListener 运行监听器
        public interface SpringApplicationRunListener {
            void starting();
            void environmentPrepared(ConfigurableEnvironment environment);
            void contextPrepared(ConfigurableApplicationContext context);
            void contextLoaded(ConfigurableApplicationContext context);
            void started(ConfigurableApplicationContext context);
            void running(ConfigurableApplicationContext context);
            void failed(ConfigurableApplicationContext context, Throwable exception);
        }
      
      • eg: SpringBoot实现类EventPublishingRunListener (order:0)
        方法 说明 实现
        0. EventPublishingRunListener(application,args) 构造方法 application.getListeners() -> SimpleApplicationEventMulticaster#addApplicationListener 所以此Listener下面广播的Event会被SpringApplication的ApplicationListener监听到
        1. starting() 应用开始启动 广播 ApplicationStartingEvent
        2. environmentPrepared(env) Environment准备好了(允许将其调整了) 广播 ApplicationEnvironmentPreparedEvent
        3. contextPrepared(ctx) ApplicationContext准备好了(允许将其调整了)
        4. contextLoaded(ctx) ApplicationContext装载好了(但仍未启动) 广播 ApplicationPreparedEvent
        5. started(ctx) ApplicationContext已刷新,应用启动完成(此时 Spring Bean 已完成初始化) 广播 ApplicationStartedEvent
        6. running(ctx) 应用开始运行 广播 ApplicationReadyEvent
        7. failed(ctx,Throwable) 应用运行失败 广播 ApplicationFailedEvent
    • listeners.starting()
    • ConfigurableEnvironment 环境对象
      • StandardEnvironment (for REACTIVE,NONE)
      • StandardServletEnvironment (for SERVLET)
      • listeners.environmentPrepared(environment)
    • ConfigurableApplicationContext 应用上下文
      • AnnotationConfigReactiveWebServerApplicationContext (for REACTIVE)
      • AnnotationConfigServletWebServerApplicationContext (for SERVLET)
      • AnnotationConfigServletWebServerApplicationContext (for NONE)
    • SpringBootExceptionReporter 故障分析报告
        @FunctionalInterface
        public interface SpringBootExceptionReporter {
                boolean reportException(Throwable failure);
        }
      
    • prepare context
      • context.setEnvironment(environment)
      • applyInitializers(context): ApplicationContextInitializer#initialize
      • listeners.contextPrepared(context);
      • load sources
      • listeners.contextLoaded(context);
    • refresh context
      • AbstractApplicationContext#refresh
    • listeners.started(context);
    • listeners.running(context);
  3. 总结 key:

    • WebApplicationType.NONE/SERVLET/REACTIVE
    • ApplicationContextInitializer#initialize(ctx)
    • ApplicationListener#onApplicationEvent(event)
    • mainClass
    • SpringApplicationRunListener#starting,environmentPrepared,contextPrepared,contextLoaded,started,running,failed (eg: EventPublishingRunListener)
    • ConfigurableEnvironment (StandardEnvironment,StandardServletEnvironment)
    • ConfigurableApplicationContext (AnnotationConfigServletWebServerApplicationContext,AnnotationConfigReactiveWebServerApplicationContext)
    • SpringBootExceptionReporter#reportException
    • context: setEnvironment,applyInitializers(trigger ApplicationContextInitializer#initialize),loadSources, refresh

源码:

package org.springframework.boot;
public class SpringApplication {

    // 1. 构造函数(准备阶段)
    public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
        this.resourceLoader = resourceLoader;
        Assert.notNull(primarySources, "PrimarySources must not be null");
        this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));

        // 1.1 根据当前应用ClassPath中是否存在相关实现类来推断WebApplication的类型
        this.webApplicationType = deduceWebApplicationType();
        =>{
            if (ClassUtils.isPresent(REACTIVE_WEB_ENVIRONMENT_CLASS, null)
                && !ClassUtils.isPresent(MVC_WEB_ENVIRONMENT_CLASS, null)) {
                return WebApplicationType.REACTIVE;
            }
            for (String className : WEB_ENVIRONMENT_CLASSES) {
                if (!ClassUtils.isPresent(className, null)) {
                    return WebApplicationType.NONE;
                }
            }
            return WebApplicationType.SERVLET;

        }

        // 1.2 加载`ApplicationContextInitializer`上下文初始器(利用 Spring 工厂加载机制)
        setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
        =>{
            getSpringFactoriesInstances(ApplicationContextInitializer.class);
            // META-INF/spring.factories (jar:spring-boot-autoconfigure):
            # Initializers
            org.springframework.context.ApplicationContextInitializer=\
            org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
            org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

            // META-INF/spring.factories (jar:spring-boot):
            # Application Context Initializers
            org.springframework.context.ApplicationContextInitializer=\
            org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
            org.springframework.boot.context.ContextIdApplicationContextInitializer,\
            org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
            org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer
        }

        // 1.3 加载`ApplicationListener` 事件监听器 (利用 Spring 工厂加载机制)
        setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
        =>{
            getSpringFactoriesInstances(ApplicationListener.class);
            // META-INF/spring.factories (jar:spring-boot-autoconfigure):
            # Application Listeners
            org.springframework.context.ApplicationListener=\
            org.springframework.boot.autoconfigure.BackgroundPreinitializer

            // META-INF/spring.factories (jar:spring-boot-autoconfigure):
            # Application Listeners
            org.springframework.context.ApplicationListener=\
            org.springframework.boot.ClearCachesApplicationListener,\
            org.springframework.boot.builder.ParentContextCloserApplicationListener,\
            org.springframework.boot.context.FileEncodingApplicationListener,\
            org.springframework.boot.context.config.AnsiOutputApplicationListener,\
            org.springframework.boot.context.config.ConfigFileApplicationListener,\
            org.springframework.boot.context.config.DelegatingApplicationListener,\
            org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
            org.springframework.boot.context.logging.LoggingApplicationListener,\
            org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener
        }

        // 1.4 推断引导类(Main Class): 根据 Main 线程执行堆栈判断实际的引导类
        this.mainApplicationClass = deduceMainApplicationClass();
        =>{
            try {
                StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
                for (StackTraceElement stackTraceElement : stackTrace) {
                    if ("main".equals(stackTraceElement.getMethodName())) {
                        return Class.forName(stackTraceElement.getClassName());
                    }
                }
            }
            catch (ClassNotFoundException ex) {
                // Swallow and continue
            }
            return null;
        }
    }

    // 2. run函数(运行阶段)
    public ConfigurableApplicationContext run(String... args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
        configureHeadlessProperty();

        // 2.1 加载运行`SpringApplicationRunListeners`运行监听器(利用 Spring 工厂加载机制)
        SpringApplicationRunListeners listeners = getRunListeners(args);
        =>{
            Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
            return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances(
                    SpringApplicationRunListener.class, types, this, args));

            // META-INF/spring.factories (jar:spring-boot):
            # Run Listeners
            org.springframework.boot.SpringApplicationRunListener=\
            org.springframework.boot.context.event.EventPublishingRunListener
        }
        listeners.starting();    // Trigger: SpringApplicationRunListener#starting

        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

            // 2.2 创建`Environment`: 根据准备阶段(构造函数中)推断的`WebApplicationType`,创建对应的`ConfigurableEnvironment`实例
            ConfigurableEnvironment environment = prepareEnvironment(listeners,applicationArguments);
            =>{
                // Create and configure the environment
                ConfigurableEnvironment environment = getOrCreateEnvironment();
                configureEnvironment(environment, applicationArguments.getSourceArgs());
                listeners.environmentPrepared(environment);     // Trigger: SpringApplicationRunListener#environmentPrepared
                bindToSpringApplication(environment);
                if (this.webApplicationType == WebApplicationType.NONE) {
                    environment = new EnvironmentConverter(getClassLoader())
                            .convertToStandardEnvironmentIfNecessary(environment);
                }
                ConfigurationPropertySources.attach(environment);
                return environment;
            }

            configureIgnoreBeanInfo(environment);
            Banner printedBanner = printBanner(environment);

            // 2.3 创建Spring应用上下文:根据准备阶段推断的`WebApplicationType`创建对应的`ConfigurableApplicationContext`实例
            context = createApplicationContext();
            =>{
                Class<?> contextClass = this.applicationContextClass;
                if (contextClass == null) {
                    try {
                        switch (this.webApplicationType) {
                        case SERVLET:
                            contextClass = Class.forName(DEFAULT_WEB_CONTEXT_CLASS);
                            break;
                        case REACTIVE:
                            contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
                            break;
                        default:
                            contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
                        }
                    }
                    catch (ClassNotFoundException ex) {
                        throw new IllegalStateException(
                                "Unable create a default ApplicationContext, "
                                + "please specify an ApplicationContextClass",
                                ex);
                    }
                }
                return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
            }

            // 2.4 加载`SpringBootExceptionReporter`故障分析报告(利用 Spring 工厂加载机制),供以后失败时使用
            exceptionReporters = getSpringFactoriesInstances(
                    SpringBootExceptionReporter.class,
                    new Class[] { ConfigurableApplicationContext.class }, context);
            =>{
                // META-INF/spring.factories (jar:spring-boot):
                # Error Reporters
                org.springframework.boot.SpringBootExceptionReporter=\
                org.springframework.boot.diagnostics.FailureAnalyzers
            }

            // 2.5 加载Bean到应用上下文:加载Annotation/XML配置的Bean给Spring管理
            prepareContext(context, environment, listeners, applicationArguments,printedBanner);
            =>{
                context.setEnvironment(environment);
                postProcessApplicationContext(context);    
                applyInitializers(context);                // Trigger: ApplicationContextInitializer#initialize
                listeners.contextPrepared(context);        // Trigger: SpringApplicationRunListener#contextPrepared

                // Add boot specific singleton beans
                context.getBeanFactory().registerSingleton("springApplicationArguments",applicationArguments);
                if (printedBanner != null) {
                    context.getBeanFactory().registerSingleton("springBootBanner", printedBanner);
                }

                // Load the sources
                Set<Object> sources = getAllSources();
                Assert.notEmpty(sources, "Sources must not be empty");
                load(context, sources.toArray(new Object[0]));

                listeners.contextLoaded(context);        // Trigger: SpringApplicationRunListener#contextLoaded
            }
            refreshContext(context);
            =>{
                ((AbstractApplicationContext) applicationContext).refresh();    //Trigger: AbstractApplicationContext#refresh
            }
            afterRefresh(context, applicationArguments);
            stopWatch.stop();
            if (this.logStartupInfo) {
                new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
            }
            listeners.started(context);                                      // Trigger: SpringApplicationRunListener#started
            callRunners(context, applicationArguments);
        }
        catch (Throwable ex) {
            handleRunFailure(context, ex, exceptionReporters, listeners); // Trigger: SpringApplicationRunListener#failed
            throw new IllegalStateException(ex);
        }
        try {
            listeners.running(context);                                     // Trigger: SpringApplicationRunListener#running
        }
        catch (Throwable ex) {
            handleRunFailure(context, ex, exceptionReporters, null);     // Trigger: SpringApplicationRunListener#failed
            throw new IllegalStateException(ex);
        }
        return context;
    }
}

使用扩展

  • 利用Spring工厂加载机制,按指定顺序装载自定义的扩展类

  • 示例1: 装载自定义的ApplicationInitializer

    1. 自定义 ApplicationInitializer

       @Order(Ordered.HIGHEST_PRECEDENCE+10)
       public class HelloApplicationInitializer<C extends ConfigurableApplicationContext> 
           implements ApplicationContextInitializer<C> {
      
           @Override
           public void initialize(C applicationContext) {
               System.out.println("Hello: ApplicationContext id="+applicationContext.getId());
           }
      
       }
       public class WorldApplicationInitializer<C extends ConfigurableApplicationContext> 
           implements ApplicationContextInitializer<C>,Ordered {
      
           @Override
           public void initialize(C applicationContext) {
               System.out.println("World: ApplicationContext id="+applicationContext.getId());
           }
      
           @Override
           public int getOrder() {
               return Ordered.HIGHEST_PRECEDENCE+10-1;
           }
       }
      
    2. 配置META-INF/spring.factories

       # ApplicationContextInitializer
       org.springframework.context.ApplicationContextInitializer=\
       com.cj.application.initializer.HelloApplicationInitializer,\
       com.cj.application.initializer.WorldApplicationInitializer
      
    3. 测试

       public class DemoSpringApplication {
           public static void main(String[] args) {
               SpringApplication.run(DemoSpringApplication.class, args);
           }
       }
       /*
       Result:
       World: ApplicationContext id=org.springframework.context.annotation.AnnotationConfigApplicationContext@69b794e2
       Hello: ApplicationContext id=org.springframework.context.annotation.AnnotationConfigApplicationContext@69b794e2
       */
      
  • 示例2: 装载自定义的ApplicationListener

    1. 自定义 ApplicationListener

       // HelloApplicationListener -- 监听所有ApplicationEvent
       @Order(Ordered.HIGHEST_PRECEDENCE+20)
       public class HelloApplicationListener<C extends ApplicationEvent> implements ApplicationListener<C>{
      
           @Override
           public void onApplicationEvent(C event) {
               System.out.println("Hello ApplicationEvent: "+event.getClass().getSimpleName());
           }
       }
      
       // WorldApplicationListener -- 监听 ContextRefreshedEvent
       public class WorldApplicationListener implements ApplicationListener<ContextRefreshedEvent>,Ordered{
           @Override
           public void onApplicationEvent(ContextRefreshedEvent event) {
               System.out.println("World ApplicationEvent: "+event.getClass().getSimpleName());
           }
           @Override
           public int getOrder() {
               return Ordered.HIGHEST_PRECEDENCE+30;
           }
       }
      
       // MySmartApplicationListener -- 监听所有ApplicationEvent,处理ApplicationEnvironmentPreparedEvent
       public class MySmartApplicationListener implements SmartApplicationListener{
           @Override
           public void onApplicationEvent(ApplicationEvent event) {
               System.out.println("MySmart ApplicationEvent: "+ event.getClass().getSimpleName());
               if (event instanceof ApplicationEnvironmentPreparedEvent) {
                   ApplicationEnvironmentPreparedEvent preparedEvent = (ApplicationEnvironmentPreparedEvent) event;
                   Environment environment = preparedEvent.getEnvironment();
                   System.out.println("environment: " + environment);
               }
           }
           @Override
           public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
               return ApplicationEnvironmentPreparedEvent.class.isAssignableFrom(eventType)
                       || ContextRefreshedEvent.class.isAssignableFrom(eventType);
           }
           @Override
           public boolean supportsSourceType(Class<?> sourceType) {
               return true;
           }
           @Override
           public int getOrder() {
               return Ordered.HIGHEST_PRECEDENCE+25;
           }
       }
      
    2. 配置META-INF/spring.factories
       # ApplicationListener
       # Listener处理顺序(根据Order排序):Hello -> MySmart -> World
       org.springframework.context.ApplicationListener=\
       com.cj.application.listener.HelloApplicationListener,\
       com.cj.application.listener.WorldApplicationListener,\
       com.cj.application.listener.MySmartApplicationListener
      
    3. 测试

       public class DemoSpringApplication {
           public static void main(String[] args) {
               SpringApplication.run(DemoSpringApplication.class, args);
           }
       }
       /*
       Hello ApplicationEvent: ApplicationStartingEvent
      
       Hello ApplicationEvent: ApplicationEnvironmentPreparedEvent
       MySmart ApplicationEvent: ApplicationEnvironmentPreparedEvent
       environment: StandardEnvironment {activeProfiles=[Java8], defaultProfiles=[default], propertySources=[MapPropertySource {name='systemProperties'}, OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}, RandomValuePropertySource {name='random'}]}
      
       World: ApplicationContext id=org.springframework.context.annotation.AnnotationConfigApplicationContext@15d9bc04
       Hello: ApplicationContext id=org.springframework.context.annotation.AnnotationConfigApplicationContext@15d9bc04
      
       Hello ApplicationEvent: ApplicationPreparedEvent
      
       Hello ApplicationEvent: ContextRefreshedEvent
       MySmart ApplicationEvent: ContextRefreshedEvent
       World ApplicationEvent: ContextRefreshedEvent
      
       Hello ApplicationEvent: ApplicationStartedEvent
       Hello ApplicationEvent: ApplicationReadyEvent
       Hello ApplicationEvent: ContextClosedEvent
       */
      

Spring事件监听模型

  • 事件(Event): ApplicationEvent,ApplicationContextEvent
  • 监听器(Listener): ApplicationListener,@EventListener
  • 广播器(Multicaster): ApplicationEventMulticaster (执行模式:同步或异步)
  • 示例:ApplicationContext注册Listener & 发送Event

      public class DemoSpringApplicationEvent {
           public static void main(String[] args) {
              // 创建上下文
              AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    
              // 注册应用事件监听器
              context.addApplicationListener(event -> {
                  if(event instanceof PayloadApplicationEvent){
                      PayloadApplicationEvent payloadEvent=(PayloadApplicationEvent)event;
                      System.out.println("监听到Payload事件: " + payloadEvent.getPayload());
                  }else
                      System.out.println("监听到事件: " + event.getClass().getSimpleName()
                          +"[source="+event.getSource()+"]");
              });
    
              // 启动上下文
              context.refresh();
              // 发送事件
              context.publishEvent("HelloWorld");
              context.publishEvent(new ApplicationEvent("Outside") {});
    
              // 关闭上下文
              context.close();
          }
      }
    
      /*
      监听到事件: ContextRefreshedEvent[source=org.springframework.context.annotation.AnnotationConfigApplicationContext@50cbc42f: startup date [Feb Oct 09 02:03:32 CST 2018]; root of context hierarchy]
      监听到Payload事件: HelloWorld
      监听到事件: [source=Outside]
      监听到事件: ContextClosedEvent[source=org.springframework.context.annotation.AnnotationConfigApplicationContext@50cbc42f: startup date [Sun Feb 09 02:03:32 CST 2018]; root of context hierarchy]
      */
    

外部化配置

Refer SpringBoot Doc

Spring Boot allows you to externalize your configuration so you can work with the same application code in different environments. You can use properties files, YAML files, environment variables and command-line arguments to externalize configuration.

加载外部配置资源的内容到程序变量中用于动态控制:

  1. 外部化配置资源(保存到PropertySource对象)

    • 常见的加载项顺序:
      • @TestPropertySource#properties
      • @SpringBootTest#properties
      • @TestPropertySource#location
      • cmd line args (eg:-Duser.id=13)
      • system properties (eg:-Duser.city.post_code=0731),可通过System.getProperties()获取
      • os env variables (eg:USER_CITY_POST_CODE=001)
      • application property files/yaml variants (eg:user.city.post-code=0571)
        • application-{profile}.propertiesoutside pkg jar
        • application-{profile}.properties packaged inside jar
        • application.properties outside pkg jar
        • application.properties packaged inside jar
      • @PropertySouce on @Configuration class
      • default properties
    • 注意:
      • 前面的优先级更高(后面同属性的值不会覆盖前面已经加载到的)
      • 允许松散绑定(特例:@ConditionalOnProperty prefix name 要与 application.properties 完全一致,在环境变量里面,允许松散绑定)
    • PropertySource
      • 带有名称的外部化配置属性源(eg source : Properties file,YAML file,environment variables, and command-line arguments,Map键值对)
      • 主要方法:abstract Object getProperty(String name)
      • Environment对应关系
        • Environment : PropertySources:PropertySource= 1:1:N
        • eg: ConfigurableEnvironment : MutablePropertySources:PropertySource = 1:1:N
        • 参考SpringApplication#run -> prepareEnvironment -> configureEnvironment -> configurePropertySources
      • 查看Environment中加载各个的PropertySource: environment.getPropertySources().forEach
          MutablePropertySources propertySources=environment.getPropertySources();
          propertySources.forEach(item->{
              System.out.printf("[%s] : %s\n", item.getName(), item);
          });
        
  2. 绑定到变量

    • XML方式:<bean> -> <property name="xxx" value="${...}" />
    • Annotation方式: @Value ,@ConfigurationProperties

加载外部化配置(XML方式)

示例:

  1. Spring应用

    • Key:

      • 配置装载属性占位符:PropertyPlaceholderConfigurer,装载外部化配置文件
      • 配置bean:<property name="xxx" value="${...}" />,注意bean需要有get/set方法
    • domain: User

        public class User{
            private Long id;
            private String name;
            private Integer age;
            // get/set function -- must need!
        }
      
    • resouces/spring-context.xml

        <?xml version="1.0" encoding="UTF-8"?>
        <beans xmlns="http://www.springframework.org/schema/beans"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
            <!-- 属性占位符配置-->
            <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
                <!-- Properties 文件 classpath 路径 -->
                <property name="location" value="classpath:test.properties"/>
                <!-- 文件字符编码 -->
                <property name="fileEncoding" value="UTF-8"/>
            </bean>  
            <!-- User Bean -->
            <bean id="user" class="com.cj.demo1.domain.User">
                <property name="id" value="${user.id}"/>
                <property name="name" value="${user.name}"/>
            </bean>
        </beans>
      
    • resources/test.properties

        user.id=12
        user.name=Tom
      
    • verify

        public class SpringPlaceholderApp {
             public static void main(String[] args) {
                String[] locations = {"spring-context.xml"};
                ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(locations);
                User user = ctx.getBean("user", User.class);
                System.err.println(user);    // User{id=12, name='Tom', age=null}
                ctx.close();
            }
        }
      
  2. SpringBoot应用

    • Key:
      • 自动装配不需要另外配置装载属性占位符:PropertyPlaceholderConfigurer
      • 配置bean:<property name="xxx" value="${...}" />,注意bean需要有get/set方法
    • domain: User

        public class User{
            private Long id;
            private String name;
            private Integer age;
            // get/set function -- must need!
        }
      
    • resources/bean.xml

        <?xml version="1.0" encoding="UTF-8"?>
        <beans xmlns="http://www.springframework.org/schema/beans"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
            <!-- User Bean -->
            <bean id="user" class="com.cj.demo1.domain.User">
                <property name="id" value="${user.id}"/>
                <property name="name" value="${user.name}"/>
            </bean>
        </beans>
      
    • resources/application.properties

        user.id=15
        user.name=Lucy
      
    • verify

        @ImportResource("bean.xml") // 加载 XML 文件
        @EnableAutoConfiguration
        public class SpringBootPlaceholderApp {
            public static void main(String[] args) {
                ConfigurableApplicationContext ctx =
                        new SpringApplicationBuilder(SpringBootPlaceholderApp.class)
                                .web(WebApplicationType.NONE) // 非 Web 应用
                                .run(args);
                User user = ctx.getBean("user", User.class);
                System.err.println(user);
                System.err.printf("System.getProperty(\"%s\") : %s \n", "user.name", System.getProperty("user.name"));            
                ConfigurableEnvironment environment = ctx.getEnvironment();
                environment.getPropertySources().forEach(item->{
                    System.out.printf("[%s] : %s\n", item.getName(), item);
                });            
                ctx.close();
            }
        }
        // Note:
        // 会发现不是Tom,而是User{id=15, name='cj', age=null}
        // 因为System Property的优先级高于property file
      

Annotation方式

  1. @Value
  2. @ConfigurationProperties
  3. 扩展:
    • @Validated 校验
    • @ConditionalOnProperty 判断

源码分析

SpringBoot中Environment的生命周期(参考SpringApplication#run

  • Key:

    • prepareEnvironment:
      • create
      • configure: PropertySources
      • trigger SpringApplicationRunListener#environmentPrepared
      • convert
    • prepareContext:
      • setEnvironment
      • trigger ApplicationContextInitializer#initialize
      • trigger SpringApplicationRunListener#contextPrepared
      • ...
      • trigger SpringApplicationRunListener#contextLoaded
  • PropertySource

      public abstract class PropertySource<T> {
          protected final String name;
          protected final T source;
          public abstract Object getProperty(String name);
          //...
      }
    
    • ConfigurationPropertySourcesPropertySource
    • AnnotationsPropertySource
    • CommandLinePropertySource
    • MapPropertySource
    • ServletConfigPropertySource
    • ServletContextPropertySource
    • ...
  • SpringApplication#run:

      // 1. SpringApplicationRunListeners
      // 2. ConfigurableEnvironment: 
      ConfigurableEnvironment environment = prepareEnvironment(listeners,applicationArguments);
      =>{
          // Create and configure the environment
          ConfigurableEnvironment environment = getOrCreateEnvironment();
          configureEnvironment(environment, applicationArguments.getSourceArgs());
          =>{
              // Add, remove or re-order any {@link PropertySource}s in this application's environment.
              configurePropertySources(environment, args);
              // setActiveProfiles for this application's environment.
              configureProfiles(environment, args);
          }
    
          listeners.environmentPrepared(environment);    // Trigger: SpringApplicationRunListener#environmentPrepared
    
          bindToSpringApplication(environment);
          if (this.webApplicationType == WebApplicationType.NONE) {
              environment = new EnvironmentConverter(getClassLoader())
                      .convertToStandardEnvironmentIfNecessary(environment);
          }
          ConfigurationPropertySources.attach(environment);
          return environment;
      }
      // 3. ConfigurableApplicationContext
      // 4. SpringBootExceptionReporter
      // 5. prepare context
      prepareContext(context, environment, listeners, applicationArguments,printedBanner);
      =>{
          context.setEnvironment(environment);
          postProcessApplicationContext(context);    
          applyInitializers(context);                // Trigger: ApplicationContextInitializer#initialize
          listeners.contextPrepared(context);        // Trigger: SpringApplicationRunListener#contextPrepared
    
          //...
          listeners.contextLoaded(context);        // Trigger: SpringApplicationRunListener#contextLoaded
      }
      // 6. refresh context
    

获取Environment

  • 方法/构造器

      @Component
      public class A{
          private Environment environment;
          @Autowired
          public A(Environment environment){
              this.environment = environment;
          }
      }
    
      @Configuration
      public class B{
          @Bean
          @Autowired    // will auto inject Environment,not add this annotation still work.
          public String hello(Environment environment){
              return environment.getRequiredProperty("say", String.class);
          }
      }
    
  • @Autowired

      @Component
      public class A{
          @Autowired
          private Environment environment;
      }
    
  • implementsBeanFactoryAware

     @Component
     public class A implements BeanFactoryAware {
         private Environment environment;
         @Override
         public void setBeanFactory(BeanFactory beanFactory) 
             throws BeansException {
             this.environment=beanFactory.getBean(Environment.class);
         }
     }
    
  • implements EnvironmentAware

      @Component
      public class A implements EnvironmentAware{
          private Environment environment;
          @Override
          public void setEnvironment(Environment environment) {
              this.environment=environment;
          }
      }
    

实时扩展外部化配置属性源

实时扩展外部化配置属性源: 基于Environment抽象实现

  • Environment 可以
  • @Value 不行
  • @ConfiguratinProperties 不行
  1. 可考虑扩展以下类,并依据Spring工厂加载机制配置装配:

    • SpringApplicationRunListener#environmentPrepared
        EventPublishingRunListener#environmentPrepared 
            -> ApplicationListener#onApplicationEvent : ApplicationEnvironmentPreparedEvent
                -> ConfigFileApplicationListener#onApplicationEvent : ApplicationEnvironmentPreparedEvent
                    -> EnvironmentPostProcessor#postProcessEnvironment : ApplicationEnvironmentPreparedEvent
      
      • ApplicationListener#onApplicationEvent
      • EnvironmentPostProcessor#postProcessEnvironment
    • ApplicationContextInitializer#initialize
    • SpringApplicationRunListener#contextPrepared
    • SpringApplicationRunListener#contextLoaded
    • 注:根据Environment生命周期,需在refresh context前,且注意触发调用顺序(参考SpringApplication#run
  2. 示例

    • 示例1:使用EnvironmentPostProcessor#postProcessEnvironment自定义实现类

        public class ExtendPropertySourcesEnvironmentPostProcessor implements EnvironmentPostProcessor{
            @Override
            public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
                System.err.println("execute ExtendPropertySourcesEnvironmentPostProcessor#postProcessEnvironment: put user.id=60");
                MutablePropertySources propertySources=environment.getPropertySources();
                Map<String,Object> source=new HashMap<String,Object>();
                source.put("user.id",60);
                MapPropertySource ps=new MapPropertySource("from-postProcessEnvironment", source);
                propertySources.addFirst(ps);
            }
        }
      
        // config: resources/META-INF/spring.factories
        # EnvironmentPostProcessor
        org.springframework.boot.env.EnvironmentPostProcessor=\
        com.cj.demo2.processor.ExtendPropertySourcesEnvironmentPostProcessor
      
    • 示例2:使用ApplicationListener#onApplicationEvent自定义实现类

        // after `ConfigFileApplicationListener`
        public class ExtendPropertySourcesApplicationListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
            @Override
            public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
                System.err.println("execute ExtendPropertySourcesApplicationListener#onApplicationEvent(ApplicationEnvironmentPreparedEvent): put user.id=55");
                MutablePropertySources propertySources=event.getEnvironment().getPropertySources();
                Map<String,Object> source=new HashMap<String,Object>();
                source.put("user.id",55);
                MapPropertySource ps=new MapPropertySource("from-applicationEnvironmentPreparedEvent", source);
                propertySources.addFirst(ps);
            }
        }
      
        // config: resources/META-INF/spring.factories
        # Application Listeners
        org.springframework.context.ApplicationListener=\
        com.cj.demo2.listener.ExtendPropertySourcesApplicationListener
      
    • 示例3:使用SpringApplicationRunListener#environmentPrepared自定义实现类

        // after `EventPublishingRunListener`
        public class ExtendPropertySourcesRunListener implements SpringApplicationRunListener,Ordered{
            private final SpringApplication application;
            private final String[] args;
            public ExtendPropertySourcesRunListener(SpringApplication application, String[] args) {
                this.application = application;
                this.args = args;
            }
            @Override
            public int getOrder() {
                return 1;    // set after EventPublishingRunListener(order=0)
            }
            @Override
            public void environmentPrepared(ConfigurableEnvironment environment) {
                System.err.println("execute ExtendPropertySourcesRunListener#environmentPrepared: put user.id=50");
                MutablePropertySources propertySources=environment.getPropertySources();
                Map<String,Object> source=new HashMap<String,Object>();
                source.put("user.id",50);
                MapPropertySource ps=new MapPropertySource("from-environmentPrepared", source);
                propertySources.addFirst(ps);
            }
            //...
        }
      
        // config: resources/META-INF/spring.factories
        # Run Listeners, Refer META-INF/spring.factories (jar:spring-boot):
        org.springframework.boot.SpringApplicationRunListener=\
        com.cj.demo2.listener.ExtendPropertySourcesRunListener
      
    • 示例4:使用ApplicationContextInitializer#initialize自定义实现类

        public class ExtendPropertySourcesApplicationContextInitializer<C extends ConfigurableApplicationContext> implements ApplicationContextInitializer<C> {
            @Override
            public void initialize(ConfigurableApplicationContext applicationContext) {
                System.err.println("execute ExtendPropertySourcesApplicationContextInitializer#initialize: put user.id=65");
                MutablePropertySources propertySources=applicationContext.getEnvironment().getPropertySources();
                Map<String,Object> source=new HashMap<String,Object>();
                source.put("user.id",65);
                MapPropertySource ps=new MapPropertySource("from-applicationContextInitialize", source);
                propertySources.addFirst(ps);
            }
        }
      
        // config: resources/META-INF/spring.factories
        # Application Context Initializers
        org.springframework.context.ApplicationContextInitializer=\
        com.cj.demo2.initializer.ExtendPropertySourcesApplicationContextInitializer
      

Servlet

Servlet:

是一种基于 Java 技术的 Web 组件,用于生成动态内容,由容器管理。 类似于其他 Java 技术组件,Servlet 是平台无关的 Java 类组成,且由 Java Web 服务器加载执行。通常情况,由 Servlet容器提供运行时环境。

Servlet 容器/引擎:

作为Web服务器或应用服务器的一部分,管理Servlets实例以及它们的生命周期 通过请求和响应对话,提供Web Client与Servlets交互的能力

从功能上:

Servlet 介于 CGI(Common Gateway Interface)与服务扩展(如:Netscape Server API 或 Apache 模块)之间

在体系上:

Servlet 技术(或者规范)属于 Java EE 技术(规范)的一部分

  1. Servlet 版本

    规范版本 发布时间 Java 平台 主要更新
    Servlet 4.0 2017 年 9 月 Java EE 8 支持 HTTP/2
    Servlet 3.1 2013 年 5 月 Java EE 7 非阻塞 I/O、HTTP 协议更新机制(WebSocket)
    Servlet 3.0 2009 年 12 月 Java EE 6 可插拔、简化部署、异步 Servlet、安全、文件上传
    Servlet 2.5 2005 年 9 月 Java EE 5 Annotation 支持
    Servlet 2.4 2003 年 11 月 J2EE 1.4 web.xml 支持 XML Scheme
    Servlet 2.3 2001 年 8 月 J2EE 1.3 新增 Filter、事件/监听器、Wrapper
    Servlet 2.2 1999 年 8 月 J2EE 1.2 作为 J2EE 的一部分, 以 .war 文件作为独立 web 应用
  2. Servlet 核心组件 (Servlet,Filter,Listener,Context)

    核心组件 API 说明 起始版本 Spring Framework 代表实现
    javax.servlet.Servlet 动态内容组件 1.0 DispatcherServlet
    javax.servlet.Filter Servlet 过滤器 2.3 CharacterEncodingFilter
    javax.servlet.ServletContext Servlet 应用上下文 / /
    javax.servlet.AsyncContext 异步上下文 3.0
    javax.servlet.ServletContextListener ServletContext 生命周期监听器 2.3 ContextLoaderListener
    javax.servlet.ServletRequestListener ServletRequest 生命周期监听器 2.3 RequestContextListener
    javax.servlet.http.HttpSessionListener HttpSession 生命周期监听器 2.3 HttpSessionMutexListener
    javax.servlet.AsyncListener 异步上下文监听器 3.0 StandardServletAsyncWebRequest
    javax.servlet.ServletContainerInitializer Servlet 容器初始化器 3.0 SpringServletContainerInitializer
  3. Servlet 组件的生命周期

    • Servlet
      • init(ServletConfig)
      • service(ServletRequest,ServletResponse)
      • destroy()
    • Filter
      • init(FilterConfig)
      • doFilter(ServletRequest,ServletResponse,FilterChain)
      • destroy()
    • Listener
      • contextInitialized(ServletContextEvent)
      • contextDestroyed(ServletContextEvent)
  4. Servlet 组件注册

    组件 传统方式 注解方式 编程方式(利用Servlet SPI:ServletContainerInitializer#onStartup(classes,ctx)
    Servlet web.xml: <servlet>,<servlet-mapping> @WebServlet ServletContext#addServlet
    Filter web.xml: <filter>,<filter-mapping> @WebFilter ServletContext#addFilter
    Listener web.xml: <listener>,<context-param> @WebListener ServletContext#addListener
  5. Servlet容器

    Servlet容器 启动 初始化器
    独立Servlet容器 独立Servlet容器 -> 启动 App Servlet SPI(3.0): ServletContainerInitializer; Spring 适配:SpringServletContainerInitializer+ @HandlesTypes(WebApplicationInitializer.class)
    嵌入式Servlet容器 App -> 启动嵌入式Servlet容器 SpringBoot: ServletContextInitializer
  6. Servlet -> Spring -> SpringBoot:

    Jar XML方式 注解方式 编程方式
    Servlet web.xml @WebServlet等 interface ServletContainerInitializer
    Spring(独立Servlet容器) SpringServletContainerInitializer(implements ServletContainerInitializer) + @HandlesTypes(WebApplicationInitializer.class)
    SpringBoot(独立Servlet容器) × SpringBootServletInitializer(implements WebApplicationInitializer)
    note: 支持使用ServletContextInitializer注入的Servlet组件
    eg:
    @Bean ServletContextInitializer
    @Bean ServletRegisterBean,FilterRegisterBean,ServletListenerRegisterBean (implements ServletContextInitializer)
    SpringBoot(嵌入式Servlet容器) × limited: need to add @ServletComponentScan only can use ServletContextInitializer
    note: 不支持使用SpringBootServletInitializer(implements WebApplicationInitializer)

Servlet 容器启动初始化

ServletContainerInitializer Servlet容器初始化器

  1. Servlet interface (jar: javax.servlet-api / tomcat-embed-core)

     package javax.servlet;
     public interface ServletContainerInitializer {
         public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException; 
     }
    
    • Servlet 3.0 新增的一个接口,主要用于在容器启动阶段通过编程风格注册Servlet/Filter/Listener,以取代通过web.xml配置注册
    • 这样利于开发内聚的web应用框架,例如:对于SpringWeb应用,Servlet3.0前需要在web.xml中配置注册一些Spring实现的Servlet/Filter/Listener组件,相当于将框架和容器紧耦合了; 3.x后注册的功能内聚到Spring里,这个SpringWeb应用就变成了一个即插即用的组件,不用依据应用环境定义一套新的配置
    • 参考:
  2. Spring实现类 SpringServletContainerInitializer (Version 3.1后): 接受启动WebApplicationInitializer类型的initializers (jar: spring-web)

     package org.springframework.web;
     @HandlesTypes(WebApplicationInitializer.class)
     public class SpringServletContainerInitializer implements ServletContainerInitializer {
         @Override
         public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException{
             List<WebApplicationInitializer> initializers = new LinkedList<>();
             for:webAppInitializerClasses
                 initializers.add
             AnnotationAwareOrderComparator.sort(initializers);
             for:initializers
                 initializer.onStartup(servletContext)
         }
     }
    
     /**
      * Interface to be implemented in Servlet 3.0+ environments in order to configure the
      * {@link ServletContext} programmatically -- as opposed to (or possibly in conjunction
      * with) the traditional {@code web.xml}-based approach.
      */
     public interface WebApplicationInitializer {
         /**
          * Configure the given {@link ServletContext} with any servlets, filters, listeners
          * context-params and attributes necessary for initializing this web application. 
          */
         void onStartup(ServletContext servletContext) throws ServletException;
     }
    
    • WebApplicationInitializer 实现类:
      • AbstractContextLoaderInitializer
        • AbstractDispatcherServletInitializer 编程驱动
          • AbstractAnnotationConfigDispatcherServletInitializer 注解驱动
      • AbstractReactiveWebInitializer
      • AbstractSecurityWebApplicationInitializer
      • SpringBootServletInitializer (jar: spring-boot)

Spring WebMvc 示例:

Key:

  • 使用xml配置WebMvc相关组件
  • 使用web.xml 或者自定义的WebApplicationInitializer实现类注册DispatcherServlet
  • 自定义一个Servlet,并通过@WebServlet注册
  • 使用独立Servlet启动运行
  1. pom.xml

     <packaging>war</packaging>
     <dependencies>
         <!-- Spring Web MVC 依赖 -->
         <dependency>
              <groupId>org.springframework</groupId>
              <artifactId>spring-webmvc</artifactId>
         </dependency>
    
         <!-- Servlet 3.1 API 依赖-->
          <dependency>
              <groupId>javax.servlet</groupId>
              <artifactId>javax.servlet-api</artifactId>
              <scope>provided</scope>
          </dependency>
     </dependencies>
     <build>
         <finalName>demo</finalName>
         <plugins> 
             <!-- jetty:run -->
             <plugin>
               <groupId>org.eclipse.jetty</groupId>
               <artifactId>jetty-maven-plugin</artifactId>
               <version>9.4.12.v20180830</version>
               <configuration>
                 <scanIntervalSeconds>10</scanIntervalSeconds>
                  <webApp>
                   <contextPath>/demo</contextPath>
                 </webApp>
                 <httpConnector>
                     <port>9090</port>
                     <idleTimeout>60000</idleTimeout>
                 </httpConnector>
                 <stopKey>foo</stopKey>
                    <stopPort>9999</stopPort>
               </configuration>
             </plugin>
    
             <!-- tomcat -->
             <!-- mvn -Dmaven.test.skip -U clean package -->
             <!-- java -jar target/xxx-war-exec.jar -->
             <!-- visit: http://localhost:8080/demo -->
             <plugin>
                 <groupId>org.apache.tomcat.maven</groupId>
                 <artifactId>tomcat7-maven-plugin</artifactId>
                 <version>2.1</version>
                 <executions>
                     <execution>
                         <id>tomcat-run</id>
                         <goals>
                             <goal>exec-war-only</goal>
                         </goals>
                         <phase>package</phase>
                         <configuration>
                             <!-- ServletContext path -->
                             <path>/demo</path>
                         </configuration>
                     </execution>
                 </executions>
             </plugin>
    
         </plugins>
     </build>
    
  2. /src/main/webapp/WEB-INF/app-context.xml (配置Web MVC组件)

     <?xml version="1.0" encoding="UTF-8"?>
     <beans xmlns="http://www.springframework.org/schema/beans"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns:context="http://www.springframework.org/schema/context"
         xsi:schemaLocation="
         http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans.xsd
         http://www.springframework.org/schema/context
         http://www.springframework.org/schema/context/spring-context.xsd">
    
         <context:component-scan base-package="com.cj.demo"/>
         <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>
         <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"/>
         <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
             <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
             <property name="prefix" value="/WEB-INF/jsp/"/>
             <property name="suffix" value=".jsp"/>
         </bean>
     </beans>
    
  3. /src/main/webapp/jsp/index.jsp
     This index page: Hello World
    
  4. Controller
     @Controller
     public class HelloController {
         @RequestMapping(value={"/"},method=RequestMethod.GET)
         public String index() {
             return "index";
         }
     }
    
  5. 部署 DispatcherServlet

    • 使用web.xml的传统方式注册Servlet组件
        <web-app>
            <!-- 配置注册 DispatchServlet-->
            <servlet>
                 <servlet-name>app</servlet-name>
                 <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
                 <load-on-startup>1</load-on-startup>
                 <init-param>
                    <param-name>contextConfigLocation</param-name>
                    <param-value>/WEB-INF/app-context.xml</param-value> <!-- classpath*:/META-INF/spring/spring-context.xml -->
                </init-param>
             </servlet>
             <servlet-mapping>
                 <servlet-name>app</servlet-name>
                 <url-pattern>/</url-pattern>
             </servlet-mapping>
        </web-app>
      
    • 编程取代传统的web.xml方式注册Servlet组件( Spring SpringServletContainerInitializer -> WebApplicationInitializer#onStartup

        // 使用自定义的WebApplicationInitializer实现类注册DispatchServlet
        /*
        WebApplicationInitializer
            - AbstractContextLoaderInitializer
                - AbstractDispatcherServletInitializer
                    - AbstractAnnotationConfigDispatcherServletInitializer
        */
        @ImportResource("/WEB-INF/app-context.xml")
        public class MyAnnotationConfigDispatcherServletInitializer 
            extends AbstractAnnotationConfigDispatcherServletInitializer{
      
            @Override
            protected Class<?>[] getRootConfigClasses() {    //web.xml
                return new Class[0];
            }
            @Override
            protected Class<?>[] getServletConfigClasses() {    // DispatchServlet
                System.out.println("get servlet config classes.......");
                return new Class[] {this.getClass()};
            }
            @Override
            protected String[] getServletMappings() {
                System.out.println("get servlet mappings......");
                return new String[]{"/"};
            }
        }
      
  6. 自定义一个Servlet并注入

     // 自定义一个Servlet,使用注解@WebServlet来注入此Servlet (不依赖ComponentScan)
     @WebServlet(urlPatterns="/myServlet",name="myServlet",asyncSupported=true)
     public class MyServlet extends HttpServlet{
         public MyServlet() {
             println("MyServlet!");
         }
         protected void doGet(HttpServletRequest req,HttpServletResponse resp)
                 throws ServletException,IOException {
             println("Hello world!");
              if(req.isAsyncSupported()) {
                 AsyncContext ctx=req.startAsync();
                 ctx.setTimeout(500L);
                 ctx.start(()->{
                     try{
                         println("Async Hello world!");    
                         resp.getWriter().println("Async Hello world!");
                         ctx.complete();    // must call complete
                     }catch(IOException e){
                         e.printStackTrace();
                     }
                 });
             }
             resp.getWriter().println("Hello world!");
         }
         private static void println(Object object) {
             String threadName = Thread.currentThread().getName();
             System.out.println("MyServlet[" + threadName + "]: " + object);
         }
     }
    
  7. verify: pom.xml maven build... -> jetty:run (使用tomcat maven插件生成可运行的jar包也可以)

    • curl -i http://localhost:9090/demo/
    • curl -i http://localhost:9090/demo/myServlet

SpringBoot WebMvc 示例:

Key:

  • 使用@EnableAutoConfiguration自动装配DispatcherServlet
  • 使用XML/Annotation(@Configuration) 配置WebMvc相关组件 (Spring中也可使用这种注解配置方式 + @ComponentScan(basePackages="com.cj.demo")配置扫描到配置类)
  • 使用SpringBootServletInitializer配置主引导类 (SpringBootServletInitializer implements WebApplicationInitializer)
  • 使用独立Servlet启动运行

  • pom.xml

     <packaging>war</packaging>
     <dependencies>
         <!-- Spring Boot 依赖 -->
         <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-web</artifactId>
              <exclusions>
                   <!-- Exclude the Tomcat dependency -->
                   <exclusion>
                       <groupId>org.springframework.boot</groupId>
                       <artifactId>spring-boot-starter-tomcat</artifactId>
                   </exclusion>
              </exclusions>
         </dependency> 
    
         <!-- Servlet 3.1 API 依赖-->
          <dependency>
              <groupId>javax.servlet</groupId>
              <artifactId>javax.servlet-api</artifactId>
              <scope>provided</scope>
          </dependency>
     </dependencies>
     <build>
         <finalName>demo</finalName>
         <plugins> 
             <!-- jetty:run -->
             <plugin>
               <groupId>org.eclipse.jetty</groupId>
               <artifactId>jetty-maven-plugin</artifactId>
               <version>9.4.12.v20180830</version>
               <configuration>
                 <scanIntervalSeconds>10</scanIntervalSeconds>
                  <webApp>
                   <contextPath>/demo</contextPath>
                 </webApp>
                 <httpConnector>
                     <port>9090</port>
                     <idleTimeout>60000</idleTimeout>
                 </httpConnector>
                 <stopKey>foo</stopKey>
                    <stopPort>9999</stopPort>
               </configuration>
             </plugin>
         </plugins>
     </build>
    
  • 使用@Configuration注解方式配置WebMvc组件

     package com.cj.demo.config;
     @Configuration
     // 还可以implements WebMvcConfigurer,方便实现更多配置,例如: @Override addInterceptors
     public class WebMvcConfig {
         /*
          <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
             <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
             <property name="prefix" value="/WEB-INF/jsp/"/>
             <property name="suffix" value=".jsp"/>
          </bean>
          */
         @Bean
         public ViewResolver viewResolver(){
             InternalResourceViewResolver viewResolver=new InternalResourceViewResolver();
             viewResolver.setViewClass(JstlView.class);
             viewResolver.setPrefix("/WEB-INF/jsp/");
             viewResolver.setSuffix(".jsp");
             return viewResolver;
         }
     }
    
  • /src/main/webapp/jsp/index.jsp

     This index page: Hello World
    
  • Controller

     @Controller
     public class HelloController {
         @RequestMapping(value={"/"},method=RequestMethod.GET)
         public String index() {
             return "index";
         }
     }
    
  • 通过SpringBootServletInitializer配置主引导类

     package com.cj.demo.initializer;
     // Note:SpringBootServletInitializer implements WebApplicationInitializer)
     public class MySpringBootServletInitializer extends SpringBootServletInitializer{
    
         protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
             System.out.println("MySpringBootServletInitializer configure");
             builder.sources(SpringBootDemoApp.class);
             return builder;
         }
     }
    
     @EnableAutoConfiguration
     public class SpringBootDemoApp {
     }
    
  • verify: pom.xml -> maven build... -> jetty:run -> curl -i http://localhost:9090/demo/

Servlet 嵌入式容器启动初始化

SpringBoot 实现 (jar: spring-boot) :

  1. ServletContextInitializer

     package org.springframework.boot.web.servlet;
     @FunctionalInterface
     public interface ServletContextInitializer{
         void onStartup(ServletContext servletContext) throws ServletException;
     }
    
    • 为减少风险,嵌入式容器(eg: embed jetty/tomcat/...) 不能使用Servlet interface ServletContainerInitializer和Spring interfaceWebApplicationInitializer. Refer 27.4.2 Servlet Context Initialization
    • SpringBoot中提供ServletContextInitializer接口(代替嵌入式容器无法使用的Servlet interface ServletContainerInitializer)来实现ServletContext的initialization.
  2. ServletContextInitializer实现类

    • RegistrationBean
      • DynamicRegistrationBean
        • AbstractFilterRegistrationBean
          • DelegatingFilterProxyRegistrationBean
          • FilterRegistrationBean : @WebFilter
        • ServletRegistrationBean : @WebServlet
      • ServletListenerRegistrationBean : @WebListener
  3. @ServletComponentScan

    • 嵌入式容器无法自动识别并处理Servlet注解@WebServlet@WebFilter@WebListener
    • SpringBoot提供@ServletComponentScan来扫描识别这些Servlet注解
    • 处理过程:扫描 package -> @WebServlet/@WebFilter/@WebListener -> RegistrationBean BeanDefinition -> RegistrationBeanBean: 嵌入式容器调用ServletContextInitializer#onStartup(ServletContext)时装配识别出的Servlet组件
  4. 部分源码:

     @Target(ElementType.TYPE)
     @Retention(RetentionPolicy.RUNTIME)
     @Documented
     @Import(ServletComponentScanRegistrar.class)
     public @interface ServletComponentScan
    
     package org.springframework.boot.web.servlet;
     class ServletComponentScanRegistrar implements ImportBeanDefinitionRegistrar {
         @Override
         public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
                 BeanDefinitionRegistry registry) {
             Set<String> packagesToScan = getPackagesToScan(importingClassMetadata);
             updatePostProcessor/addPostProcessor(registry, packagesToScan);
             =>{
                 GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
                 beanDefinition.setBeanClass(ServletComponentRegisteringPostProcessor.class); // Note !
                 beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(packagesToScan);
                 beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
                 registry.registerBeanDefinition(BEAN_NAME, beanDefinition);
             } 
         }
     }
     //=>
     class ServletComponentRegisteringPostProcessor implements BeanFactoryPostProcessor, ApplicationContextAware {
         private static final List<ServletComponentHandler> HANDLERS;
         static {
             List<ServletComponentHandler> servletComponentHandlers = new ArrayList<>();
             servletComponentHandlers.add(new WebServletHandler());    // For @WebServlet
             servletComponentHandlers.add(new WebFilterHandler());    // For @WebFilter
             servletComponentHandlers.add(new WebListenerHandler()); // For @WebListener
             HANDLERS = Collections.unmodifiableList(servletComponentHandlers);
         }
         @Override
         public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
                 throws BeansException {
             if (isRunningInEmbeddedWebServer()) {    // Note: only for embed webserver!
                 ClassPathScanningCandidateComponentProvider componentProvider = createComponentProvider();
                 for (String packageToScan : this.packagesToScan) 
                     scanPackage(componentProvider, packageToScan);
                     => {
                         for beanDef:componentProvider.findCandidateComponents(packageToScan)
                             for handler:HANDLERS
                                 handler.handle(beanDef,this.applicationContext)
                     }
             }
         }
     }
     //=> eg: WebServletHandler
     class WebServletHandler extends ServletComponentHandler {
         WebServletHandler() {
             super(WebServlet.class);
         }
    
         @Override
         public void doHandle(Map<String, Object> attributes,
             ScannedGenericBeanDefinition beanDefinition,BeanDefinitionRegistry registry) {
             BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ServletRegistrationBean.class); // Note !
             builder.addPropertyValue(...); // ...
             registry.registerBeanDefinition(name, builder.getBeanDefinition());
         }
     }
     //=> 
     //ServletRegistrationBean extends RegistrationBean implements ServletContextInitializer
     ServletRegistrationBean#onStartup(ServletContext)
    
  5. 总结:Spring Boot 嵌入式Servlet容器限制

    Servlet 特性 兼容性 解决方案
    web.xml 不支持 ServletContextInitializer/RegistrationBean @Bean 注册
    ServletContainerInitializer 不支持 ServletContextInitializer
    @WebServlet 等 有限支持 依赖@ServletComponentScan

SpringBoot 示例:

Key:

  • 使用xml/annotation方式配置WebMvc组件
  • 使用@EnableAutoConfiguration自动部署DispatcherServlet
  • 自定义Servlet,通过@WebServlet+@ServletComponentScan,或RegistrationBean,或ServletContextInitializer#onStartup->servletContext.addServlet方式注册
  • 使用嵌入式容器(直接运行main方法)/独立容器启动进行验证

  • pom.xml

     <dependencies>
         <!-- default embed webserver by spring-boot-starter-tomcat-->
         <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-web</artifactId>
         </dependency> 
     </dependencies>
    
  • 使用XML方式配置Web MVC组件:/src/main/webapp/WEB-INF/app-context.xml

     <?xml version="1.0" encoding="UTF-8"?>
     <beans xmlns="http://www.springframework.org/schema/beans"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns:context="http://www.springframework.org/schema/context"
         xsi:schemaLocation="
         http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans.xsd
         http://www.springframework.org/schema/context
         http://www.springframework.org/schema/context/spring-context.xsd">
    
         <context:component-scan base-package="com.cj.demo"/>
         <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>
         <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"/>
         <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
             <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
             <property name="prefix" value="/WEB-INF/jsp/"/>
             <property name="suffix" value=".jsp"/>
         </bean>
     </beans>
    
  • /src/main/webapp/jsp/index.jsp

     This index page: Hello World
    
  • Controller

     @Controller
     public class HelloController {
         @RequestMapping(value={"/"},method=RequestMethod.GET)
         public String index() {
             return "index";
         }
     }
    
  • 部署 DispatcherServlet

     // @SpringBootApplication 包括 @EnableAutoConfiguration
     // 会自动通过DispatcherServletAutoConfiguration来注入DispatchServlet(mapped=/)
     @SpringBootApplication(scanBasePackages="com.cj.demo")
     public class SpringBootStartApp {
         public static void main(String[] args) {
             SpringApplication.run(SpringBootStartApp.class, args);
         }
     }
    
  • 自定义一个Servlet并注入

     // 自定义一个Servlet
     public class MyServlet extends HttpServlet{
         public MyServlet() {
             println("MyServlet!");
         }
         protected void doGet(HttpServletRequest req,HttpServletResponse resp)
                 throws ServletException,IOException {
             println("Hello world!");
              if(req.isAsyncSupported()) {
                 AsyncContext ctx=req.startAsync();
                 ctx.setTimeout(500L);
                 ctx.start(()->{
                     try{
                         println("Async Hello world!");    
                         resp.getWriter().println("Async Hello world!");
                         ctx.complete();    // must call complete
                     }catch(IOException e){
                         e.printStackTrace();
                     }
                 });
             }
             resp.getWriter().println("Hello world!");
         }
         private static void println(Object object) {
             String threadName = Thread.currentThread().getName();
             System.out.println("MyServlet[" + threadName + "]: " + object);
         }
     }
    
    • 方式一:使用RegistrationBean
        @Bean
        public ServletRegistrationBean<Servlet> myServletRegistrationBean(){
            return new ServletRegistrationBean<Servlet>(new MyServlet(),"/myServlet");    // default asyncSupported is true.
        }
      
    • 方式二:使用@WebServlet + @ServletComponentScan
      • 在自定义Servlet上添加@WebServlet(urlPatterns="/myServlet",name="myServlet",asyncSupported=true)
      • 在main class上添加@ServletComponentScan(basePackages="com.cj.demo.servlet") 以扫描到使用@WebServlet注入的Servlet
      • Note: @ServletComponentScan only effective for EmbeddedWebServer, will scan @WebServlet,@WebFilter,@WebListener
    • 方式三:使用ServletContextInitializer#onStartup->servletContext.addServlet
        @Bean
        public ServletContextInitializer myServletContextInitializer(){
            return servletContext->{
                ServletRegistration.Dynamic reg=servletContext.addServlet("myServlet", new MyAsyncServlet());
                reg.addMapping("/myServlet");
                reg.setAsyncSupported(true);        // set asyncSupported to be true
            };
        }
      
    • 注:
      • 以上优先级 RegistrationBean > @ServletComponentScan > ServletContextInitializer
      • 同mappingUrl/servletName,优先级高的先注册了会自动skip掉后来的 (特: 若重复发生在ServletContextInitializer中,会报错,嵌入式容器启动失败)
  • verify: run main

    • curl -i http://localhost:8080/ -> OK
    • curl -i http://localhost:8080/myServlet -> OK

组件Servlet源码分析

  • Servlet (jar: servlet-api)

    • GenericServlet (jar: servlet-api)
      • HttpServletBean -> FrameworkServlet -> DispatchServlet (jar:spring-webmvc)
  • servlet-api: javax.servlet.http (eg: javax.servlet-api-3.1.0.jar)

    • Servlet (init,service,destroy,getServletConfig)
      • GenericServlet (abstract service,init,...)
        • HttpServlet (service,doGet,doPost,doXxx...)
    • Source Code:

        interface Servlet
        {
            public void init(ServletConfig config) throws ServletException;
            public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
            public void destroy();
            public ServletConfig getServletConfig();
                =>{ //ServletConfig:
                    public String getServletName();
                    public ServletContext getServletContext();
                    public String getInitParameter(String name);
                    public Enumeration<String> getInitParameterNames();
                }
            public String getServletInfo();
        }
      
        abstract class GenericServlet implements Servlet, ServletConfig,java.io.Serializable
        {
            // ...
            public void init(ServletConfig config) throws ServletException {
                this.config = config;
                this.init();
            }
            public abstract void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
        }
      
        abstract class HttpServlet extends GenericServlet 
        {
            @Override
            public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException{
                 service((HttpServletRequest)request, (HttpServletResponse)response); // protected func
                    =>{
                        req.getMethod()? doHead/doGet/doPost/doPut/doXxx...    // protected func
                    }
            }
        }
      
  • Spring: org.springframework.web.servlet (jar: spring-webmvc)

    • HttpServletBean : final init -> initBeanWrapper,initServletBean
      • FrameworkServlet : final initServletBean -> initWebApplicationContext -> onRefresh; service -> abstract doService
        • DispatchServlet : onRefresh -> initStrategies; doService -> doDispatch
    • DispatchServlet

      • init:
          HttpServlet.init(ServletConfig)
          -> final HttpServletBean.init()
           -> final FrameworkServlet.initServletBean()
            -> protected FrameworkServlet.initWebApplicationContext()
              -> protected DispatcherServlet.onRefresh(wac)
               -> protected DispatcherServlet.initStrategies(wac)
                  {    
                      // 初始化 DispatcherServlet 各种组件
                      initMultipartResolver
                      initLocaleResolver
                      initThemeResolver
                      initHandlerMappings
                      initHandlerAdapters
                      initHandlerExceptionResolvers
                      initRequestToViewNameTranslator
                      initViewResolvers
                      initFlashMapManager
                  }
        
      • Service:
          public HttpServlet.service(ServletRequest,ServletResponse) 
          -> protected FrameworkServlet.service(HttpServletRequest,HttpServletResponse)
           -> protected HttpServlet.service(HttpServletRequest,HttpServletResponse)
            -> final FrameworkServlet.doGet/doPost/doPut/doDelete/doOptions/doTrace(req,resp)
             -> abstract FrameworkServlet.doService(req,resp)
              -> protected DispatchServlet.doService(req,resp)
               -> protected DispatchServlet.doDispatch(req,resp)
                   {
                      HandlerExecutionChain mappedHandler = getHandler(processedRequest);
                      HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
                      ModelAndView mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                      processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);                    
                   }
        
    • Source Code:

        abstract class HttpServletBean extends HttpServlet implements EnvironmentCapable, EnvironmentAware 
        {    
            @Override
            public final void init() throws ServletException {
                PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
                BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
                ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
                bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
                initBeanWrapper(bw); // protected func
                bw.setPropertyValues(pvs, true);
                initServletBean(); // protected func
            }
        }
      
        public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware 
        {
            public FrameworkServlet() {}
            public FrameworkServlet(WebApplicationContext webApplicationContext) {
                this.webApplicationContext = webApplicationContext;
            }
      
            @Override
            protected final void initServletBean() throws ServletException {
                this.webApplicationContext = initWebApplicationContext();
                    =>{
                        WebApplicationContext rootContext = WebApplicationContextUtils
                                    .getWebApplicationContext(getServletContext());
                        WebApplicationContext wac = this.webApplicationContext;
                        wac!=null && instanceof ConfigurableWebApplicationContext => wac.setParent(rootContext); 
                        configureAndRefreshWebApplicationContext(wac);
                        wac!=null => wac = findWebApplicationContext()
                        wac!=null => wac = createWebApplicationContext(rootContext)
                        if(!this.refreshEventReceived) 
                            onRefresh(wac); 
                                => {
                                    // DispatchServlet#onRefresh: 
                                    initStrategies(context);
                                        =>{
                                            initMultipartResolver(context);
                                            initLocaleResolver(context);
                                            initThemeResolver(context);
                                            initHandlerMappings(context);
                                            initHandlerAdapters(context);
                                            initHandlerExceptionResolvers(context);
                                            initRequestToViewNameTranslator(context);
                                            initViewResolvers(context);
                                            initFlashMapManager(context);
                                        }
                                }
                        if (this.publishContext)
                            getServletContext().setAttribute(getServletContextAttributeName(), wac);
                        return wac;
                    }
                initFrameworkServlet();
            }
      
            @Override
            protected void service(HttpServletRequest request, HttpServletResponse response)
                    throws ServletException, IOException {
                HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
                if (httpMethod == HttpMethod.PATCH || httpMethod == null) 
                    processRequest(request, response);
                else
                    super.service(request, response);
                    => {
                        FrameworkServlet#doGet/doPost/doPut/doDelete/doOptions/doTrace 
                          => FrameworkServlet#processRequest(request,response)
                    }
            }
      
            protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
                    throws ServletException, IOException {
                initContextHolders(request, localeContext, requestAttributes);
                doService(request, response);
                =>{
                    // DispatchServlet#doService:
                    doDispatch(request, response);
                    =>{
                        HandlerExecutionChain mappedHandler = getHandler(processedRequest);
                        HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
                        ModelAndView mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
                    }
                }
                resetContextHolders(request, previousLocaleContext, previousAttributes);
                publishRequestHandledEvent(request, response, startTime, failureCause);
            }
        }
      

Servlet异步支持(3.0+)

  1. 技术:

    • DeferredResult (spring-web)
    • Callable (java)
    • CompletionStage (java)
  2. 示例:

     package com.cj.demo.controller;
    
     @RestController
     public class HelloAsyncController {
    
         @GetMapping("/hello")
         public String hello() {
             return "Hello!";
         }
    
         @GetMapping("/deffered")
         public DeferredResult<String> helloDeffered(){
             DeferredResult<String> result=new DeferredResult<>(50L);
             result.setResult("[DefferedResult] Hello world!");
             println("[DefferedResult] Hello world!");
    
             result.onCompletion(()->{
                 println("[DefferedResult] execute complete");
             });
             result.onTimeout(()->{
                 println("[DefferedResult] execute timeout");
             });
             return result;
         }
    
         @GetMapping("/callable")
         public Callable<String> helloCallable() {
             final long startTime = System.currentTimeMillis();
             println("[Callable] Hello world!");
    
             return () -> {
                 long costTime = System.currentTimeMillis() - startTime;
                 println("[Callable] cost " + costTime + " ms.");
                 return "[Callable] Hello world!";
             };
         } 
    
         @GetMapping("/completionStage")
         public CompletionStage<String> helloCompletionStage() {
             final long startTime = System.currentTimeMillis();
             println("[CompletionStage] Hello world!");
    
             return CompletableFuture.supplyAsync(() -> {
                 long costTime = System.currentTimeMillis() - startTime;
                 println("[CompletionStage] cost " + costTime + " ms.");
                 return "[CompletionStage] Hello world!";
             });
         }
    
         private static void println(Object object) {
             String threadName = Thread.currentThread().getName();
             System.out.println("HelloAsyncController[" + threadName + "]: " + object);
         }
     }
    
    • visit:/deffered,/callable,/completionStage

WebMVC

webmvc

  1. 核心组件 (jar:spring-webmvc)

    组件 主要方法 说明
    1 DispatcherServlet doService,doDispatch 总控 HttpServlet#service -> FrameworkServlet#service -> DispatcherServlet#doService -> DispatcherServlet#doDispatch
    2 MultipartResolver resolveMultipart -> MultipartHttpServletRequest 解析多部分请求(如: 文件上传),封装Request为MultipartHttpServletRequest对象
    3 HandlerMapping getHandler -> HandlerExecutionChain(Handler+Interceptors) mapping Request:Handler (+HandlerInterceptors)
    eg: RequestMappingHandlerMapping支持标注@RequestMappingmethod
    4 HandlerAdapter handle -> ModelAndView(modelMap+viewName) invoke Handler -> resolveArgument + doInvoke + handleReturnValue -> ModelAndView(modelMap+viewName)
    eg: 1. RequestMappingHandlerAdapter支持标注@RequestMappingmethod invoke and handle
    eg: 2. RequestResponseBodyMethodProcessor(implements HandlerMethodArgumentResolver)支持标注@RequestBodymethod的arguments resolve
    eg: 3. RequestResponseBodyMethodProcessor(implements HandlerMethodReturnValueHandler)支持标注@ResponseBodymethod的returnValue handle
    5 HandlerExceptionResolver resolveException -> ModelAndView 处理以上发生的异常,返回一个ModelAndView对象供后续渲染处理
    6 LocaleResolver resolveLocale -> Locale 从Request中解析出本地化对象Locale 供后面解析View及View的渲染使用,实现国际化
    7 ViewResolver resolveViewName -> View 根据ModelAndView#viewName + Locale 解析得到最佳 View
    eg: ContentNegotiatingViewResolver
    eg: InternalResourceViewResolver
    eg: ThymeleafViewResolver
    8 View render 渲染视图(ctx: ModelAndView#modelMap),得到最终效果供Response
  2. 常用注解 (jar: spring-web: org.springframework.web.bind.annotation)

    • 控制器:
      • @Controller
      • @RestController = @Controller+@ResponseBody
    • 映射:
      • @RequestMapping
      • @GetMapping,@PostMapping,@PutMapping,@DeleteMapping,@PatchMapping
    • 请求:
      • @RequestParam
      • @PathVariable
      • @CookieValue
      • @RequestPart
      • @RequestHeader
      • @RequestBody
    • 响应:
      • @ResponseBody
    • 拦截(切面通知/处理):
      • @ControllerAdvice
      • @RestControllerAdvice
      • @ExceptionHandler
    • 属性:
      • @ModelAttribute
      • @RequestAttribute
      • @SessionAttribute
    • 跨域:
      • @CrossOrigin
  1. 视图处理

    • 视图解析 ViewResolver#resolveViewName -> View
      • Order
      • return Matched View
      • Resource Location
    • 视图渲染 View#render
      • Context
      • TemplateEngine
  2. 内容协商处理

    • 解析器 ContentNegotiatingViewResolver (resolveViewName)
    • 工厂 ContentNegotiationManagerFactoryBean(构建ContentNegotiationManager)
    • 管理器 ContentNegotiationManager(管理ContentNegotiationStrategy)
    • 策略 ContentNegotiationStrategy (resolveMediaTypes)
    • 配置 ContentNegotiationConfigurer
  3. 框架:

    • Spring WebMvc
      • WebMvc总控DispatcherServlet注册: web.xml / WebApplicationInitializer
      • WebMvc其他组件配置:.xml / @Configuration
      • 独立Servlet容器启动运行
    • SpringBoot WebMvc(依赖SpringWebMvc,只是增加了自动化装配和配置部分)
      • 自动装配 @EnableAutoConfiguration :
        • DispatcherServletAutoConfiguration: 装配WebMvc总控DispatcherServlet
        • ServletWebServerFactoryAutoConfiguration: 装配Servlet容器
        • WebMvcAutoConfiguration: 装配WebMvc其他组件
      • 配置: 自定义配置类装配 / 外部化配置
        • 自定义类 @Configuration(可implements WebMvcConfigurer方便添加其他配置)+@Bean
        • 外部化配置 WebMvcProperties : spring.mvc.xxx (eg: resources/application.properties)
      • 独立/嵌入式Servlet容器启动运行
    • 示例:参考上章 Servlet的例子
      • Spring: Xml 方式
      • Spring: Annotation方式
      • SpringBoot: Auto config

模版视图之JSP

Key:

  • 视图解析器:InternalResourceViewResolver
  • 视图:JstlView
  • 外部化配置:WebMvcProperties

示例(SpringBoot WebMvc):

  1. pom.xml

     <dependencies>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>
         </dependency>
         <!-- jstl -->
         <dependency>
             <groupId>javax.servlet</groupId>
             <artifactId>jstl</artifactId>
         </dependency>
         <!-- 对jsp的支持的依赖 -->
         <dependency>
             <groupId>org.apache.tomcat.embed</groupId>
             <artifactId>tomcat-embed-jasper</artifactId>
             <scope>provided</scope>
         </dependency>
     </dependencies>
    
  2. JSP Template:src/main/webapp/WEB-INF/jsp/hello.jsp

     <!-- 使用jsp指令生成xml格式的文件: -->
     <jsp:root 
         xmlns:jsp="http://java.sun.com/JSP/Page" 
         xmlns:c="http://java.sun.com/jsp/jstl/core"
         version="2.0">
         <jsp:directive.page isELIgnored="false"/>
         <Hello>
             <message>${message}</message>
             <language>${acceptLanguage}</language>
             <jsessionId>${jsessionId}</jsessionId>
             <users>
             <c:forEach items="${users}"  var="u">
               <user>${u}</user>
             </c:forEach>
             </users>
         </Hello>
     </jsp:root>
    
  3. Configure: resources/application.properties

    • 方式一:自定义配置类
        @Configuration
        public class WebMvcConfig {
            /*
             <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
                <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
                <property name="prefix" value="/WEB-INF/jsp/"/>
                <property name="suffix" value=".jsp"/>
             </bean>
             */
            @Bean
            public ViewResolver viewResolver(){
                InternalResourceViewResolver viewResolver=new InternalResourceViewResolver();
                viewResolver.setViewClass(JstlView.class);
                viewResolver.setPrefix("/WEB-INF/jsp/");
                viewResolver.setSuffix(".jsp");
                return viewResolver;
            }
        }
      
    • 方式二:外部化配置(eg: resources/application.properties)
        spring.mvc.view.prefix = /WEB-INF/jsp/
        spring.mvc.view.suffix = .jsp
      
  4. Controller:

    • 方式一:直接使用Model#addAttribute
        @Controller
        public class HelloController {
            @RequestMapping(value={"/"},method=RequestMethod.GET)
            public String index(@RequestHeader("Accept-Language") String acceptLanguage
                    ,@CookieValue("JSESSIONID") String jsessionId
                    ,Model model) {
                List<String> users=new ArrayList<String>();
                users.add("Tom");
                users.add("Susan");
                users.add("Jack");
                model.addAttribute("users", users);
                model.addAttribute("message", "Where are you?");
                model.addAttribute("acceptLanguage",acceptLanguage);
                model.addAttribute("jsessionId",jsessionId);
                System.out.println(model);
                return "index";
            }
        }
      
    • 方式二:使用@ModelAttribute
        @Controller
        public class HelloController {
            @RequestMapping(value={"/"},method=RequestMethod.GET)
            public String index(Model model){
                List<String> users=new ArrayList<String>();
                users.add("Tom");
                users.add("Susan");
                users.add("Jack");
                model.addAttribute("users", users);
                return "index";
            }
            @ModelAttribute("message")
            public String message(){
                return "Where are you?";
            }
            @ModelAttribute("acceptLanguage")
            public String acceptLanguage(@RequestHeader("Accept-Language") String acceptLanguage){
                return acceptLanguage;
            }
            @ModelAttribute("jsessionId")
            public String jsessionId(@CookieValue("JSESSIONID") String jsessionId){
                return jsessionId;
            }
        }
      
    • 方式三:使用@ControllerAdvice
        @Controller
        public class HelloController {
            @RequestMapping(value={"/"},method=RequestMethod.GET)
            public String index(Model model){
                return "index";
            }
            @RequestMapping(value="/{num}",method=RequestMethod.GET)
            public String doExTest(@PathVariable int num,Model model){
                model.addAttribute("message","number:"+num);
                return "index";
            }
        }
        @ControllerAdvice(assignableTypes=HelloController.class)
        public class HelloControllerAdvice {
            @ModelAttribute("message")
            public String message(){
                return "Where are you?";
            }
            @ModelAttribute("acceptLanguage")
            public String acceptLanguage(@RequestHeader("Accept-Language") String acceptLanguage){
                return acceptLanguage;
            }
            @ModelAttribute("jsessionId")
            public String jsessionId(@CookieValue("JSESSIONID") String jsessionId){
                return jsessionId;
            }
            @ExceptionHandler(Throwable.class)
            public ResponseEntity<String> onException(Throwable ex){
                return ResponseEntity.ok("handle exception:"+ex.getMessage());
            }
        }
      
  5. Main

     @SpringBootApplication
     public class WebMvcViewApp {
         public static void main(String[] args) {
             SpringApplication.run(WebMvcViewApp.class,args);
         }
     }
    
  6. Visit: http://localhost:8080/

     <Hello>
         <message>Where are you?</message>
         <language>zh-CN,zh;q=0.9,en;q=0.8</language>
         <jsessionId>node08vobe92o7xci1sb3hf7pn3dm56.node0</jsessionId>
         <users>
             <user>Tom</user>
             <user>Susan</user>
             <user>Jack</user>
         </users>
     </Hello>
    

模版视图之Thymeleaf

Key:

  • 视图解析器ViewResolver:ThymeleafViewResolver
  • 视图View:ThymeleaflView
  • 模板引擎ITemplateEngine : SpringTemplateEngine/SpringWebFluxTemplateEngine
  • 外部化配置:ThymeleafProperties

示例(SpringBoot WebMvc):

  1. pom.xml

     <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
         </dependency>
         <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-thymeleaf</artifactId>
         </dependency>
    
  2. Thymeleaf Template: resources/templates/thymeleaf/hello.html

     <p th:text="${message}">!!!</p>
    
  3. Configure: resources/application.properties

     # ThymeleafProperties
     spring.thymeleaf.prefix=classpath:/templates/thymeleaf/
     spring.thymeleaf.suffix=.html
     spring.thymeleaf.cache = false
    
  4. Controller

     @Controller
     public class HelloController {
         @GetMapping("/hello")
         public String hello() {
             model.addAttribute("message", "Hello World");
             return "hello";
         }
     }
    
  5. Main

     @SpringBootApplication
     public class WebMvcViewApp {
         public static void main(String[] args) {
             SpringApplication.run(WebMvcViewApp.class,args);
         }
     }
    
  6. Visit: http://localhost:8080/hello

Thymeleaf模版处理流程:

官网 | Doc | Quict Start

  1. 资源定位(模板来源 ):

    • 文件资源: File
    • ClassPath资源: ClassLoader
    • 统一资源: URL
    • Web资源: ServletContext
    • Spring 资源: ResourceLoader & Resource
  2. 渲染上下文(变量来源 ):

    • Spring Web MVC: Model
    • Servlet: Attribute
    • Thyemeaf: Context
  3. 模板引擎(模板渲染)

    • Thymeleaf模板引擎:interface ITemplateEngine
      • Thymeleaf 原生实现: TemplateEngine
      • Spring 实现: SpringTemplateEngine
      • Spring WebFlux 实现: SpringWebFluxTemplateEngine
  4. 示例:使用 Thymeleaf API 渲染内容

     public class ThymeleafTemplateEngineStarter {
         public static void main(String[] args) throws IOException {
             // 1. 资源定位,获取模板内容
             //String content = "<p th:text=\"${message}\">!!!</p>";
    
             // 从 classpath:/templates/thymeleaf/hello.html 读取内容
             ResourceLoader resourceLoader = new DefaultResourceLoader();
             Resource resource = resourceLoader.getResource("classpath:/templates/thymeleaf/hello.html");
             File templateFile = resource.getFile();
             FileInputStream inputStream = new FileInputStream(templateFile);
             ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
             IOUtils.copy(inputStream, outputStream);
             inputStream.close();
             String content = outputStream.toString("UTF-8");
    
             // 2. 创建渲染上下文
             Context context = new Context();
             context.setVariable("message", "Hello,World");
    
             // 3. 使用模板引擎进行渲染
             SpringTemplateEngine templateEngine = new SpringTemplateEngine();
             String result = templateEngine.process(content, context);
             System.out.println(result);
         }
     }
    

    resources/templates/thymeleaf/hello.html

     <p th:text="${message}">!!!</p>
    

多视图处理

示例:多视图处理器并存

  • 视图处理器:
    • ContentNegotiatingViewResolver
      • ThymeleafViewResolver
      • InternalResourceViewResolver
    • ThymeleafViewResolver
    • InternalResourceViewResolver
  • 注意:
    • ViewResolver Order
    • ViewResolver 模板资源查找
  • 总结:

    • 按照ViewResolver Order,返回第一个可以resolve的View去render(eg:下面的两个Debug示例都只能有一个正确返回)
    • 找不到Resource时就抛出异常
  • pom.xml

     <dependencies>
         <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
         </dependency>
    
         <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-thymeleaf</artifactId>
         </dependency>
    
         <!-- jstl -->
         <dependency>
          <groupId>javax.servlet</groupId>
          <artifactId>jstl</artifactId>
         </dependency>
         <!-- 对jsp的支持的依赖 -->
         <dependency>
           <groupId>org.apache.tomcat.embed</groupId>
           <artifactId>tomcat-embed-jasper</artifactId>
           <scope>provided</scope>
         </dependency>
     </dependencies>
    
     <build>
         <plugins>
             <plugin>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-maven-plugin</artifactId>
             </plugin>
         </plugins>
     </build>
    
  • Template:

    • thymeleaf: resources/templates/thymeleaf/hello.html
        <p th:text="${message}">!!!</p>
      
    • jsp: webapp/WEB-INF/jsp/index.jsp
        <jsp:root 
        xmlns:jsp="http://java.sun.com/JSP/Page" 
        xmlns:c="http://java.sun.com/jsp/jstl/core"
        version="2.0">
            <jsp:directive.page isELIgnored="false"/>
            <Hello>
                 <message>${message}</message>
            </Hello>
        </jsp:root>
      
  • configure: resources/application.properties:

     # ThymeleafProperties
     spring.thymeleaf.prefix=classpath:/templates/thymeleaf/
     spring.thymeleaf.suffix=.html
     spring.thymeleaf.cache = false
    
     # WebMvcProperties
     spring.mvc.view.prefix = /WEB-INF/jsp/
     spring.mvc.view.suffix = .jsp
    
  • Controller

     @Controller
     public class HelloController {
    
         @GetMapping("/")
         public String index() {
             return "index";
         }
         @GetMapping("/hello")
         public String hello() {
             return "hello";
         }
         @ModelAttribute("message")
         public String message(){
             return "Where are you?";
         }
     }
    
  • Main

     @SpringBootApplication
     public class WebMvcViewApp {
         public static void main(String[] args) {
             SpringApplication.run(WebMvcViewApp.class,args);
         }
     }
    
  • 默认ThymeleafViewResolver的Order在InternalResourceViewResolver之前

    • Debug得到:
      • Ordered ViewResolver List:
        • ContentNegotiatingViewResolver
          • BeanNameViewResolver
          • ThymeleafViewResolver
          • ViewResolverComposite
          • InternalResourceViewResolver
        • BeanNameViewResolver
        • ThymeleafViewResolver
        • ViewResolverComposite
        • InternalResourceViewResolver
      • Verify:
  • 将InternalResourceViewResolver(JstlView)顺序提到ThymeleafViewResolver(ThymeleafView)之前

    • 注释掉resources/application.properties中WebMvcProperties相关的配置

        # ThymeleafProperties
        spring.thymeleaf.prefix=classpath:/templates/thymeleaf/
        spring.thymeleaf.suffix=.html
        spring.thymeleaf.cache = false
      
        # WebMvcProperties
        # spring.mvc.view.prefix = /WEB-INF/jsp/
        # spring.mvc.view.suffix = .jsp
      
    • 自定义一个Configuration类,装载新配置的InternalResourceViewResolver
        @Configuration
        public class WebMvcConfig {
            @Bean
            public InternalResourceViewResolver viewResolver() {
                InternalResourceViewResolver viewResolver=new InternalResourceViewResolver();
                viewResolver.setPrefix("/WEB-INF/jsp/");
                viewResolver.setSuffix(".jsp");
                // ThymeleafAutoConfiguration: ThymeleafViewResolver Ordered.LOWEST_PRECEDENCE - 5
                viewResolver.setOrder(Ordered.LOWEST_PRECEDENCE-10);
                return viewResolver;
            } 
        }
      
    • Debug得到:
  • 注:

    • 由于WebMvcProperties不提供order配置,所以无法通过外部化配置修改ViewResolver的顺序
    • 由于上面使用了public InternalResourceViewResolver viewResolver()装载了一个name为viewResolver,class为InternalResourceViewResolver的Bean,所以就不会再装载ContentNegotiatingViewResolver和默认的InternalResourceViewResolver了(具体可参考WebMvcAutoConfiguration中的相关配置)

        @Configuration
        @ConditionalOnWebApplication(type = Type.SERVLET)
        @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
        @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
        @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
        @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class,
                ValidationAutoConfiguration.class })
        public class WebMvcAutoConfiguration {
            // ...
      
            @Configuration
            @Import(EnableWebMvcConfiguration.class)
            @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
            @Order(0)
            public static class WebMvcAutoConfigurationAdapter
                    implements WebMvcConfigurer, ResourceLoaderAware {
                // ... 
      
                @Bean
                @ConditionalOnMissingBean
                public InternalResourceViewResolver defaultViewResolver() {
                    InternalResourceViewResolver resolver = new InternalResourceViewResolver();
                    resolver.setPrefix(this.mvcProperties.getView().getPrefix());
                    resolver.setSuffix(this.mvcProperties.getView().getSuffix());
                    return resolver;
                }
      
                @Bean
                @ConditionalOnBean(ViewResolver.class)
                @ConditionalOnMissingBean(name = "viewResolver", value = ContentNegotiatingViewResolver.class)
                public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
                    ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
                    resolver.setContentNegotiationManager(
                            beanFactory.getBean(ContentNegotiationManager.class));
                    // ContentNegotiatingViewResolver uses all the other view resolvers to locate
                    // a view so it should have a high precedence
                    resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
                    return resolver;
                }
            }
        }
      

内容协商视图解析器

ContentNegotiatingViewResolver (implements ViewResolver):

  1. 优先级:最高(order=Ordered.HIGHEST_PRECEDENCE)
  2. 成员:
    • 包含List<ViewResolver>列表: 会包含除自己以外的所有装载的ViewResolver(按顺序)
    • 包含内容协商管理工厂ContentNegotiationManagerFactoryBean: 生成内容协商管理器ContentNegotiationManager
    • 包含内容协商管理器ContentNegotiationManager: 管理内容协商策略List<ContentNegotiationStrategy>(用于获取HTTP Request的MediaType列表)
  3. 方法:
    • @Override resolveViewName->View
      • getMediaTypes: 获取与produce mediaType兼容的HTTP Request MediaType列表
      • getCandidateViews: 获取所有可能的View (ViewResolver#resolveViewName视图解析得到的View)
      • getBestView: 从上面获取的View中选取最佳匹配的View
        • HTTP Request MediaType <-> ViewResolver View contentType : 第一个匹配的那个View
        • 注:ViewResolver 顺序Request的MediaType匹配规则(Accept 头策略,请求参数策略,...)
  4. 配置:
    • 自定义配置类:@Configuration + implements WebMvcConfigurer + @Override configureContentNegotiation(ContentNegotiationConfigurer configurer)
    • 外部化配置:WebMvcProperties.Contentnegotiation

示例: 多视图处理器内容协商

  1. 上面Case,修改自定义配置类:

     @Configuration
     public class WebMvcConfig {        
         // 1. 修改bean name为 myViewResolver
         // 不与ContentNegotiatingViewResolver装载条件冲突,则系统会自动加载ContentNegotiatingViewResolver
         // ContentNegotiatingViewResolver装载条件:
         // @ConditionalOnBean(ViewResolver.class)
         // @ConditionalOnMissingBean(name = "viewResolver", value = ContentNegotiatingViewResolver.class),
         // 2. set viewResolver content-type (to distinguish with other viewResolvers')
         @Bean
         public InternalResourceViewResolver myViewResolver() {
             InternalResourceViewResolver viewResolver=new InternalResourceViewResolver();
             viewResolver.setPrefix("/WEB-INF/jsp/");
             viewResolver.setSuffix(".jsp");
             // ThymeleafAutoConfiguration: ThymeleafViewResolver Ordered.LOWEST_PRECEDENCE - 5
             viewResolver.setOrder(Ordered.LOWEST_PRECEDENCE-10);
             // Set ViewResolver Content-Type
             viewResolver.setContentType("text/xml;charset=UTF-8");
             return viewResolver;
         } 
     }
    
  2. 配置:

    • 方式一: 外部化配置
        # WebMvcProperties contentnegotiation
        # ?format=pdf
        spring.mvc.contentnegotiation.favorParameter = true
        # /users.pdf
        spring.mvc.contentnegotiation.favorPathExtension = true
      
    • 方式二: 自定义Configuration类
        @Configuration
        public class WebMvcConfig implements WebMvcConfigurer {
            //...
            @Override
            public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
                configurer.favorParameter(true)
                        .favorPathExtension(true);
            }
        }
      
  3. Debug得到:

    • Ordered ViewResolver List:
      • ContentNegotiatingViewResolver
        • InternalResourceViewResolver ( Content-Type: text/xml;charset=UTF-8 )
        • BeanNameViewResolver
        • ThymeleafViewResolver ( Content-Type: text/html;charset=UTF-8 )
        • ViewResolverComposite
      • InternalResourceViewResolver
      • BeanNameViewResolver
      • ThymeleafViewResolver
      • ViewResolverComposite
    • Verify:

REST

REST = RESTful = Representational State Transfer,is one way of providing interoperability between computer systems on the Internet.

  1. 架构约束

    • 统一接口(Uniform interface)
      • 资源识别(Identification of resources)
        • URI(Uniform Resource Identifier )
      • 资源操作(Manipulation of resources through representations)
        • HTTP verbs:GET、PUT、POST、DELETE
      • 自描述消息(Self-descriptive messages)
        • Content-Type
        • MIME-Type
        • Media Type: application/javascript、text/html
      • 超媒体(HATEOAS)
        • Hypermedia As The Engine Of Application State
    • C/S架构(Client-Server)
    • 无状态(Stateless)
    • 可缓存(Cacheable)
    • 分层系统(Layered System)
    • 按需代码(Code on demand)(可选)
  2. WebMvc Rest支持

    • 控制器:@Controller,@RestController (= @Controller+@ResponseBody)
    • 映射:@RequestMapping,@XxxMapping(eg: @GetMapping,@PostMapping,@PutMapping,@DeleteMapping,@PatchMapping)
    • 请求:@RequestParam,@PathVariable,@CookieValue,@RequestBody,@RequestHeader,RequestEntity(header+body)
    • 响应:@ResponseBody,ResponseEntity(header+body),ResponseCookie
    • 拦截:@ControllerAdvice,@RestControllerAdvice,interface HandlerInterceptor
  3. 核心组件

    • HandlerMethod: 被@RequestMapping/@XxxMapping标注的方法
    • RequestMappingHandlerMapping#getHandler: 找到map RequestMappingInfoHandlerMethod
      • RequestMappingInfo: 存储解析出的@RequestMapping信息
        • methods : @RequestMapping#method
        • params : @RequestMapping#params
        • headers : @RequestMapping#headers
        • consumes : @RequestMapping#consumes 请求头 Content-Type 媒体类型映射
        • produces : @RequestMapping#produces 响应头 Content-Type 媒体类型映射
    • RequestMappingHandlerAdapter#handle: HandlerMethod的invoke and handle
      • HandlerMethodArgumentResolver#resolveArgument 处理方法参数解析器(解析HTTP请求内容为HandlerMethod的参数)
        • RequestResponseBodyMethodProcessor: 支持标注@RequestBodymethod的arguments resolve
          • HttpMessageConverter#read : HTTP消息转换器,read: 反序列化HttpRequest
      • HandlerMethodReturnValueHandler#handleReturnValue 处理方法返回值解析器(解析HandlerMethod返回值为HTTP响应内容)
        • RequestResponseBodyMethodProcessor: 支持标注@ResponseBodymethod的returnValue handle
          • ContentNegotiationManager#resolveMediaTypes : 内容协商管理器,解析请求的媒体类型,返回合法的MediaTypes
          • HttpMessageConverter#write : HTTP消息转换器,write: 序列化HttpResponse
          • 注:ModelAndViewContainer#setRequestHandled:true -> ModelAndView null -> no viewResolver#resolveViewName and View#render call !
  4. MediaType 媒体类型

    • 请求的媒体类型: ContentNegotiationManager#resolveMediaTypes
      • 解析出HTTP Request中的媒体类型 (eg: text/html,application/json)
      • 使用ContentNegotiationStrategy#resolveMediaTypes,有各种解析策略,例如:
        • 固定 MediaType : FixedContentNegotiationStrategy
        • "Accept" 请求头: HeaderContentNegotiationStrategy
        • 请求参数: ParameterContentNegotiationStrategy
        • 路径扩展名: PathExtensionContentNegotiationStrategy
      • 解析成功,返回合法 MediaType 列表
      • 解析失败,返回单元素 MediaType.ALL(*/*) 媒体类型列表
    • 可消费的媒体类型: @RequestMapping#consumes
      • 请求头 Content-Type的媒体类型
      • 若请求头中的Content-Type和consumes中配置的不兼容,则该HandlerMethod不会被匹配到执行
    • 可生成的媒体类型: @RequestMapping#produces:
      • 响应头 Content-Type 的媒体类型
      • 若配置了,则使用配置的MediaType列表
      • 若未配置,则使用已注册的 HttpMessageConverter列表支持的MediaType列表
      • 可用来匹配支持的HttpMessageConverter以序列化生成响应内容和生成响应头的Content-Type内容
        • 找到与请求的媒体类型兼容的MediaType列表,使用第一个匹配支持的HttpMessageConverter进行序列化,生成响应内容;
        • 若未找到匹配的,则抛出415 HttpMediaTypeNotAcceptableException;

示例:REST

  1. pom.xml
     <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-web</artifactId>
     </dependency>
    
  2. Controller

     @RestController
     public class UserController {
    
         @GetMapping(value="/say")
         public String say(@RequestParam(required = false) String message) {
             return "Say:" + message;
         }
    
         @GetMapping(value="/saylimit",consumes="application/json",produces="text/html;charset=UTF-8")
         public String saylimit(@RequestParam(required = false) String message) {
             return "Saylimit:" + message;
         }
    
         @GetMapping("/users")
         public Object listUsers() {
             List<User> users=new ArrayList<User>();
             users.add(new User(1,"Tom"));
             users.add(new User(2,"Lucy"));
             users.add(new User(3,"Jack"));
             return users;
         }
    
         @PostMapping("/users")
         public Object addUser(@RequestBody User user) {
             System.out.println("addUser:"+user);
             return user;
         }
     }
    
  3. main
     @SpringBootApplication
     public class WebMvcRestApp {
         public static void main( String[] args ){
             SpringApplication.run(WebMvcRestApp.class, args);
         }
     }
    
  4. Verify

    • Get /say?message=123

        $ curl -i http://localhost:8080/say?message=123
        HTTP/1.1 200
        Content-Type: text/plain;charset=UTF-8
        Content-Length: 7
        Date: Thu, 13 Dec 2018 07:43:44 GMT
      
        Say:123
      
        $ curl -i -H "Content-Type:application/json" http://localhost:8080/say?message=123
      
        HTTP/1.1 200
        Content-Type: text/plain;charset=UTF-8
        Content-Length: 7
        Date: Thu, 13 Dec 2018 08:44:02 GMT
      
        Say:123
      
    • Get /saylimit?message=123

        $ curl -i http://localhost:8080/saylimit?message=123
        HTTP/1.1 415
        Content-Type: application/json;charset=UTF-8
        Transfer-Encoding: chunked
        Date: Thu, 13 Dec 2018 08:28:49 GMT
      
        {"timestamp":"2018-12-13T08:42:21.574+0000","status":415,"error":"Unsupported Media Type","message":"Content type '' not supported","path":"/saylimit"}
      
        $ curl -i -H "Content-Type:application/json" http://localhost:8080/saylimit?message=123
        HTTP/1.1 200
        Content-Type: text/html;charset=UTF-8
        Content-Length: 12
        Date: Thu, 13 Dec 2018 08:43:05 GMT
      
        Saylimit:123
      
    • Get /users

        $ curl -i http://localhost:8080/users
        HTTP/1.1 200
        Content-Type: application/json;charset=UTF-8
        Transfer-Encoding: chunked
        Date: Thu, 13 Dec 2018 07:45:47 GMT
      
        [{"id":1,"name":"Tom"},{"id":2,"name":"Lucy"},{"id":3,"name":"Jack"}]
      
    • Post /users

        $ curl -i -X POST -d "id=22&name=Ketty" http://localhost:8080/users
        HTTP/1.1 415
        Content-Type: application/json;charset=UTF-8
        Transfer-Encoding: chunked
        Date: Thu, 13 Dec 2018 07:47:13 GMT
      
        {"timestamp":"2018-12-13T07:47:13.352+0000","status":415,"error":"Unsupported Media Type","message":"Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported","path":"/users"}
      
        $ curl -i -X POST -H "Content-Type: application/json" -d '{"id":22, "name":"Ketty"}' http://localhost:8080/users
        HTTP/1.1 200
        Content-Type: application/json;charset=UTF-8
        Transfer-Encoding: chunked
        Date: Thu, 13 Dec 2018 07:51:14 GMT
      
        {"id":22,"name":"Ketty"}
      

扩展示例:实现 Content-Typetext/properties 媒体类型的请求与响应,中间转换使用Properties对象

实现:

  • 方式一:依赖REST的@RequestBody@ResponseBody(会使用RequestResponseBodyMethodProcessor->HttpMessageConverter#read/write)
    • 自定义HttpMessageConverter实现类来support text/properties媒体类型的 read & write
      • read: Request -> Properties
      • write: Properties -> Response
    • @Configuration + implements WebMvcConfigurer+ @Override extendMessageConverters -> converters#add/set
  • 方式二;不依赖REST的@RequestBody@ResponseBody
    • 自定义HandlerMethodArgumentResolver实现类,@Override resolveArgument 实现text/properties 媒体类型的请求解析为方法参数的Properties对象
    • 自定义HandlerMethodReturnValueHandler实现类,@Override handleReturnValue 实现Properties类型方法返回值转化为text/properties媒体类型响应内容
    • @Configuration + implements WebMvcConfigurer+ @Override addArgumentResolvers & @Override addReturnValueHandlers
    • 注:上面自定义的实现类可复用上面自定义HttpMessageConverter实现类的read/write方法

示例(方式一):

  1. 自定义HttpMessageConverter:

     public class PropertiesHttpMessageConverter extends AbstractGenericHttpMessageConverter<Properties>{
    
         public PropertiesHttpMessageConverter() {
             // 设置支持的 MediaType
             super(new MediaType("text", "properties"));
         }
    
         @Override
         public Properties read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
                 throws IOException, HttpMessageNotReadableException {
             return readInternal(null, inputMessage);
         }
    
         @Override
         protected void writeInternal(Properties properties, Type type, HttpOutputMessage outputMessage)
                 throws IOException, HttpMessageNotWritableException {
    
             MediaType mediaType = outputMessage.getHeaders().getContentType();
             Charset charset = mediaType.getCharset();
             charset = (charset == null) ? Charset.forName("UTF-8") : charset;
             Writer writer = new OutputStreamWriter(outputMessage.getBody(), charset);
             properties.store(writer,"From PropertiesHttpMessageConverter");
         }
    
         @Override
         protected Properties readInternal(Class<? extends Properties> clazz, HttpInputMessage inputMessage)
                 throws IOException, HttpMessageNotReadableException {
    
             MediaType mediaType = inputMessage.getHeaders().getContentType();
             Charset charset = mediaType.getCharset();
             charset = (charset == null) ? Charset.forName("UTF-8") : charset;
             InputStreamReader reader = new InputStreamReader(inputMessage.getBody(),charset);
             Properties properties = new Properties();
             properties.load(reader);
             return properties;
         }
     }
    
  2. Configure: WebMvcConfigurer#extendMessageConverters
     @Configuration
     public class WebMvcConfig implements WebMvcConfigurer {    
         @Override
         public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
             converters.add(new PropertiesHttpMessageConverter());
         }
     }
    
  3. Controller

     @RestController
     public class UserController {
         //...
    
         @PostMapping(value="/props",consumes = "text/properties;charset=UTF-8")    // Content-Type 过滤媒体类型
         public Object addProp(@RequestBody Properties prop) {
             System.out.println("addProp:"+prop);
             return prop;
         }
     }
    
  4. main
     @SpringBootApplication
     public class WebMvcRestApp {
         public static void main( String[] args ){
             SpringApplication.run(WebMvcRestApp.class, args);
         }
     }
    
  5. Verify

     $ curl -i X POST -H "Content-Type: text/properties" -d 'id:22' http://localhost:8080/props
     HTTP/1.1 200
     Content-Type: application/json;charset=UTF-8
     Transfer-Encoding: chunked
     Date: Thu, 13 Dec 2018 09:13:34 GMT
    
     {"id":"22"}
    

跨域

  • 注解驱动 @CrossOrigin
  • 代码驱动 WebMvcConfigurer#addCorsMappings
  • 过滤 CorsFilter

示例:

  1. pom.xml

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

     @Controller
     public class HelloController {
         @GetMapping("/")
         public String hello() {
             return "index";
         }
     }
    
     @RestController
     public class UserController {
    
         //@CrossOrigin("*")
         @GetMapping(value="/say")
         public String say(@RequestParam(required = false) String message) {
             return "Say:" + message;
         }
     }
    
  3. Configure

     @Configuration
     public class WebMvcConfig implements WebMvcConfigurer {    
         public void addCorsMappings(CorsRegistry registry) {
             registry.addMapping("/**").allowedOrigins("*");
         }
     }
    
  4. resources/templates/index.html

     <!DOCTYPE html>
     <html xmlns:th="http://www.thymeleaf.org">
     <head>
         <title>CORS 示例</title>
         <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
         <script src="https://code.jquery.com/jquery-3.3.1.min.js" ></script>
     </head>
     <body>
     <div id="message"></div>
     </body>
     <script>
     $(document).ready(function(){
          // Ajax 跨域 GET 请求
          $.get( "http://api.rest.org:8080/say?message=Hello", function( data ) {
             alert( data );
             $( "#message" ).html( data );
          });
     });
     </script>
     </html>
    
  5. Visit http://localhost:8080/

WebMVC 源码分析

SpringBoot自动装配

Key:

  1. 配置类:
    • DispatcherServlet : DispatcherServletAutoConfiguration
    • WebMvc : WebMvcAutoConfiguration (替换@EnableWebMvc)
    • Servlet容器 : ServletWebServerFactoryAutoConfiguration
  2. 装载顺序:
    • 绝对顺序: @AutoConfigureOrder
    • 相对顺序: @AutoConfigureAfter
  3. 装配条件
    • Web 类型判断 @ConditionalOnWebApplication(type = Type.SERVLET)
    • API 判断 @ConditionalOnClass
    • Bean 判断 @ConditionalOnMissingBean,@ConditionalOnBean
  4. 外部化配置
    • Web MVC 配置: WebMvcProperties
    • 资源配置: ResourceProperties

部分源码:

  1. META-INF/spring.factories:(jar:spring-boot-autoconfig)

     # Auto Configure
     org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
     org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\
     org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\
     org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
     #...
    
  2. ServletWebServerFactoryAutoConfiguration,DispatcherServletAutoConfiguration,WebMvcAutoConfiguration 配置类:

     // Servlet容器 配置类
     @Configuration
     @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
     @ConditionalOnClass(ServletRequest.class)
     @ConditionalOnWebApplication(type = Type.SERVLET)
     @EnableConfigurationProperties(ServerProperties.class)
     @Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
             ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
             ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
             ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
     public class ServletWebServerFactoryAutoConfiguration {
         //...
     }
    
     // DispatcherServlet 配置类
     @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
     @Configuration
     @ConditionalOnWebApplication(type = Type.SERVLET)
     @ConditionalOnClass(DispatcherServlet.class)
     @AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
     @EnableConfigurationProperties(ServerProperties.class)
     public class DispatcherServletAutoConfiguration {
         //...
     }
    
     // WebMvc 配置类
     @Configuration
     @ConditionalOnWebApplication(type = Type.SERVLET)
     @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
     @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)        // 若已经有了WebMvcConfigurer.class则不会装载WebMvcAutoConfiguration
     @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
     @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class,
             ValidationAutoConfiguration.class })
     public class WebMvcAutoConfiguration {
         //...
         @Configuration
         @Import(EnableWebMvcConfiguration.class)
         @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
         @Order(0)
         public static class WebMvcAutoConfigurationAdapter
                 implements WebMvcConfigurer, ResourceLoaderAware {
             //...
         }
     }
    
  3. 注:

    • 使用springboot自动装配,可不再使用web.xml配置
    • @EnableWebMvc会装载WebMvcConfigurationSupport,所以使用了@EnableWebMvc后,WebMvcAutoConfiguration就不注入了(即WebMvc模块自动装载会失效)

        // EnableWebMvc会装载Class:`WebMvcConfigurationSupport`
        @Retention(RetentionPolicy.RUNTIME)
        @Target(ElementType.TYPE)
        @Documented
        @Import(DelegatingWebMvcConfiguration.class)
        public @interface EnableWebMvc {
        }
      
        @Configuration
        public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
            //...
        }
      

装载ThymeleafViewResolver

加入spring-boot-starter-thymeleaf依赖包后,SpringBoot会自动装载ThymeleafViewResolver

部分源码:

  1. META-INF/spring.factories(jar: spring-boot-autoconfig)

     # Auto Configure
     org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
     org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\
     #...
    
     # Template availability providers
     org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\
     org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider,\
     org.springframework.boot.autoconfigure.mustache.MustacheTemplateAvailabilityProvider,\
     org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider,\
     org.springframework.boot.autoconfigure.thymeleaf.ThymeleafTemplateAvailabilityProvider,\
     org.springframework.boot.autoconfigure.web.servlet.JspTemplateAvailabilityProvider
    
  2. ThymeleafAutoConfiguration

     @Configuration
     @EnableConfigurationProperties(ThymeleafProperties.class)
     @ConditionalOnClass(TemplateMode.class)
     @AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
     public class ThymeleafAutoConfiguration {
         @Configuration
         @ConditionalOnMissingBean(name = "defaultTemplateResolver")
         static class DefaultTemplateResolverConfiguration {
             // ...
         }
    
         @Configuration
         protected static class ThymeleafDefaultConfiguration {
             // ...
         }
    
         @Configuration
         @ConditionalOnWebApplication(type = Type.SERVLET)
         @ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
         static class ThymeleafWebMvcConfiguration {
             // ...
             @Configuration
             static class ThymeleafViewResolverConfiguration {
                 private final ThymeleafProperties properties;
                 private final SpringTemplateEngine templateEngine;
                 @Bean
                 @ConditionalOnMissingBean(name = "thymeleafViewResolver")
                 public ThymeleafViewResolver thymeleafViewResolver() {
                     ThymeleafViewResolver resolver = new ThymeleafViewResolver();
                     resolver.setTemplateEngine(this.templateEngine);
                     resolver.setCharacterEncoding(this.properties.getEncoding().name());
                     resolver.setContentType(
                             appendCharset(this.properties.getServlet().getContentType(),
                                     resolver.getCharacterEncoding()));
                     resolver.setExcludedViewNames(this.properties.getExcludedViewNames());
                     resolver.setViewNames(this.properties.getViewNames());
                     // This resolver acts as a fallback resolver (e.g. like a
                     // InternalResourceViewResolver) so it needs to have low precedence
                     resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 5);
                     resolver.setCache(this.properties.isCache());
                     return resolver;
                 }
                 // ...
             }
         }
    
         @Configuration
         @ConditionalOnWebApplication(type = Type.REACTIVE)
         @ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
         static class ThymeleafReactiveConfiguration {
             // ...
         }
    
         @Configuration
         @ConditionalOnWebApplication(type = Type.REACTIVE)
         @ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
         static class ThymeleafWebFluxConfiguration {
             // ...
         }
    
         //...
     }
    
  3. ConfigurationProperties: ThymeleafProperties

     @ConfigurationProperties(prefix = "spring.thymeleaf")
     public class ThymeleafProperties {
    
         private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
         public static final String DEFAULT_PREFIX = "classpath:/templates/";
         public static final String DEFAULT_SUFFIX = ".html";
         private String prefix = DEFAULT_PREFIX;
         private String suffix = DEFAULT_SUFFIX;
         private boolean cache = true;
         //...
    
         public static class Servlet {
             private MimeType contentType = MimeType.valueOf("text/html");
             // get/setContentType
         }
     }
    
  4. ThymeleafTemplateAvailabilityProvider

     public class ThymeleafTemplateAvailabilityProvider
             implements TemplateAvailabilityProvider {
         @Override
         public boolean isTemplateAvailable(String view, Environment environment,
                 ClassLoader classLoader, ResourceLoader resourceLoader) {
             if (ClassUtils.isPresent("org.thymeleaf.spring5.SpringTemplateEngine",
                     classLoader)) {
                 String prefix = environment.getProperty("spring.thymeleaf.prefix",
                         ThymeleafProperties.DEFAULT_PREFIX);
                 String suffix = environment.getProperty("spring.thymeleaf.suffix",
                         ThymeleafProperties.DEFAULT_SUFFIX);
                 return resourceLoader.getResource(prefix + view + suffix).exists();
             }
             return false;
         }
     }
    

ContentNegotiatingViewResolver

ContentNegotiatingViewResolver:

  • 成员:
    • Order:
      • Ordered.HIGHEST_PRECEDENCE 最高优先级
    • 视图解析器列表List<ViewResolver>: 会包含除自己以外的所有装载的ViewResolver(按顺序)
      • ViewResolver#resolveViewName(viewName,local) -> View
    • 内容协商管理工厂ContentNegotiationManagerFactoryBean: 用于生成内容协商管理器ContentNegotiationManager
      • ContentNegotiationManagerFactoryBean#build() -> ContentNegotiationManager
    • 内容协商管理器ContentNegotiationManager: 管理内容协商策略ContentNegotiationStrategy(用于获取HTTP Request的MediaType列表)
      • List<ContentNegotiationStrategy> , eg:
        • 固定 MediaType : FixedContentNegotiationStrategy
        • "Accept" 请求头: HeaderContentNegotiationStrategy
        • 请求参数: ParameterContentNegotiationStrategy
        • 路径扩展名: PathExtensionContentNegotiationStrategy
      • ContentNegotiationManager#resolveMediaTypes(request) -> List
      • ContentNegotiationManager#resolveFileExtensions(mediaType) -> List
  • Override 方法:resolveViewName
    • getMediaTypes: 获取与produce兼容的HTTP Request MediaType (使用ContentNegotiationManagerContentNegotiationStrategy获取HTTP Request MediaType
    • getCandidateViews: 获取所有可能的View (ViewResolver视图解析得到View)
    • getBestView: 从上面获取的View中选取最佳匹配的View
      • HTTP Request MediaType <-> ViewResolver View contentType : 第一个匹配的那个View
      • 注:ViewResolver 顺序Request的MediaType(Accept 头策略,请求参数策略,...)

部分源码:

Key:

  • Member vars:

    • order: Ordered.HIGHEST_PRECEDENCE
    • ContentNegotiationManager
    • ContentNegotiationManagerFactoryBean
    • List<ViewResolver> viewResolvers
    • List<View> defaultViews
  • @Override resolveViewName

    • List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
      • List<MediaType> acceptableMediaTypes = this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
      • List<MediaType> producibleMediaTypes = new ArrayList<>(request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE));
      • foreach acceptableMediaTypes -> foreach producibleMediaTypes -> acceptable.isCompatibleWith(producible) -> add
      • MediaType.sortBySpecificityAndQuality
    • List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
      • foreach this.viewResolvers
        • candidateViews.add(viewResolver.resolveViewName(viewName, locale))
        • extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
        • foreach extensions -> candidateViews.add(viewResolver.resolveViewName(viewName + '.' + extension, locale))
      • candidateViews.addAll(this.defaultViews)
    • View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
      • foreach candidateViews -> instanceof SmartView && isRedirectView -> return candidateView
      • foreach requestedMediaType:requestedMediaTypes
        • foreach candidateView:candidateViews
          • candidateContentType = MediaType.parseMediaType(candidateView.getContentType())
          • requestMediaType.isCompatibleWith(candidateContentType) -> return candidateView
    • return bestView
  1. ContentNegotiatingViewResolver#resolveViewName -> View

    • getMediaTypes
    • getCandidateViews
    • getBestView

      public class ContentNegotiatingViewResolver 
        extends WebApplicationObjectSupport 
        implements ViewResolver, Ordered, InitializingBean {
      
        private int order = Ordered.HIGHEST_PRECEDENCE;
        @Nullable
        private ContentNegotiationManager contentNegotiationManager;
        private final ContentNegotiationManagerFactoryBean cnmFactoryBean = new ContentNegotiationManagerFactoryBean();
        @Nullable
        private List<View> defaultViews;
        @Nullable
        private List<ViewResolver> viewResolvers;
      
        @Override
        protected void initServletContext(ServletContext servletContext) {
            Collection<ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();
            if (this.viewResolvers == null) {
                this.viewResolvers = new ArrayList<>(matchingBeans.size());
                for (ViewResolver viewResolver : matchingBeans) {
                    if (this != viewResolver) {
                        this.viewResolvers.add(viewResolver);
                    }
                }
            }
            else {
                for (int i = 0; i < this.viewResolvers.size(); i++) {
                    ViewResolver vr = this.viewResolvers.get(i);
                    if (matchingBeans.contains(vr)) {
                        continue;
                    }
                    String name = vr.getClass().getName() + i;
                    obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(vr, name);
                }
      
            }
            AnnotationAwareOrderComparator.sort(this.viewResolvers);    // sort!
            this.cnmFactoryBean.setServletContext(servletContext);
        }
      
        @Override
        public void afterPropertiesSet() {
            if (this.contentNegotiationManager == null) {
                this.contentNegotiationManager = this.cnmFactoryBean.build();
            }
        }
      
        @Override
        @Nullable
        public View resolveViewName(String viewName, Locale locale) throws Exception {
            RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
            Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
            List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
            if (requestedMediaTypes != null) {
                List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
                View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
                if (bestView != null) {
                    return bestView;
                }
            }
            if (this.useNotAcceptableStatusCode) {
                if (logger.isDebugEnabled()) {
                    logger.debug("No acceptable view found; returning 406 (Not Acceptable) status code");
                }
                return NOT_ACCEPTABLE_VIEW;
            }
            else {
                logger.debug("No acceptable view found; returning null");
                return null;
            }
        }
      
        @Nullable
        protected List<MediaType> getMediaTypes(HttpServletRequest request) {
            try {
                ServletWebRequest webRequest = new ServletWebRequest(request);
                List<MediaType> acceptableMediaTypes = this.contentNegotiationManager.resolveMediaTypes(webRequest);
                List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request);
                Set<MediaType> compatibleMediaTypes = new LinkedHashSet<>();
                for (MediaType acceptable : acceptableMediaTypes) {
                    for (MediaType producible : producibleMediaTypes) {
                        if (acceptable.isCompatibleWith(producible)) {
                            compatibleMediaTypes.add(getMostSpecificMediaType(acceptable, producible));
                        }
                    }
                }
                List<MediaType> selectedMediaTypes = new ArrayList<>(compatibleMediaTypes);
                MediaType.sortBySpecificityAndQuality(selectedMediaTypes);
                return selectedMediaTypes;
            }
            catch (HttpMediaTypeNotAcceptableException ex) {
                return null;
            }
        }
      
        @SuppressWarnings("unchecked")
        private List<MediaType> getProducibleMediaTypes(HttpServletRequest request) {
            Set<MediaType> mediaTypes = (Set<MediaType>)
                    request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
            if (!CollectionUtils.isEmpty(mediaTypes)) {
                return new ArrayList<>(mediaTypes);
            }
            else {
                return Collections.singletonList(MediaType.ALL);
            }
        }
      
        private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
                throws Exception {
            List<View> candidateViews = new ArrayList<>();
            if (this.viewResolvers != null) {
                for (ViewResolver viewResolver : this.viewResolvers) {
                    View view = viewResolver.resolveViewName(viewName, locale);
                    if (view != null) {
                        candidateViews.add(view);
                    }
                    for (MediaType requestedMediaType : requestedMediaTypes) {
                        List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
                        for (String extension : extensions) {
                            String viewNameWithExtension = viewName + '.' + extension;
                            view = viewResolver.resolveViewName(viewNameWithExtension, locale);
                            if (view != null) {
                                candidateViews.add(view);
                            }
                        }
                    }
                }
            }
            if (!CollectionUtils.isEmpty(this.defaultViews)) {
                candidateViews.addAll(this.defaultViews);
            }
            return candidateViews;
        }
      
        @Nullable
        private View getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs) {
            for (View candidateView : candidateViews) {
                if (candidateView instanceof SmartView) {
                    SmartView smartView = (SmartView) candidateView;
                    if (smartView.isRedirectView()) {
                        if (logger.isDebugEnabled()) {
                            logger.debug("Returning redirect view [" + candidateView + "]");
                        }
                        return candidateView;
                    }
                }
            }
            for (MediaType mediaType : requestedMediaTypes) {
                for (View candidateView : candidateViews) {
                    if (StringUtils.hasText(candidateView.getContentType())) {
                        MediaType candidateContentType = MediaType.parseMediaType(candidateView.getContentType());
                        if (mediaType.isCompatibleWith(candidateContentType)) {
                            if (logger.isDebugEnabled()) {
                                logger.debug("Returning [" + candidateView + "] based on requested media type '" +
                                        mediaType + "'");
                            }
                            attrs.setAttribute(View.SELECTED_CONTENT_TYPE, mediaType, RequestAttributes.SCOPE_REQUEST);
                            return candidateView;
                        }
                    }
                }
            }
            return null;
        }
      
        //...
      }
      
  2. ContentNegotiationManagerFactoryBean#build -> new ContentNegotiationManager(strategies)

     public class ContentNegotiationManagerFactoryBean
             implements FactoryBean<ContentNegotiationManager>, ServletContextAware, InitializingBean {
         // ...
    
         public ContentNegotiationManager build() {
             List<ContentNegotiationStrategy> strategies = new ArrayList<>();
    
             if (this.strategies != null) {
                 strategies.addAll(this.strategies);
             }
             else {
                 if (this.favorPathExtension) {
                     PathExtensionContentNegotiationStrategy strategy;
                     if (this.servletContext != null && !useRegisteredExtensionsOnly()) {
                         strategy = new ServletPathExtensionContentNegotiationStrategy(this.servletContext, this.mediaTypes);
                     }
                     else {
                         strategy = new PathExtensionContentNegotiationStrategy(this.mediaTypes);
                     }
                     strategy.setIgnoreUnknownExtensions(this.ignoreUnknownPathExtensions);
                     if (this.useRegisteredExtensionsOnly != null) {
                         strategy.setUseRegisteredExtensionsOnly(this.useRegisteredExtensionsOnly);
                     }
                     strategies.add(strategy);
                 }
    
                 if (this.favorParameter) {
                     ParameterContentNegotiationStrategy strategy = new ParameterContentNegotiationStrategy(this.mediaTypes);
                     strategy.setParameterName(this.parameterName);
                     if (this.useRegisteredExtensionsOnly != null) {
                         strategy.setUseRegisteredExtensionsOnly(this.useRegisteredExtensionsOnly);
                     }
                     else {
                         strategy.setUseRegisteredExtensionsOnly(true);  // backwards compatibility
                     }
                     strategies.add(strategy);
                 }
    
                 if (!this.ignoreAcceptHeader) {
                     strategies.add(new HeaderContentNegotiationStrategy());
                 }
    
                 if (this.defaultNegotiationStrategy != null) {
                     strategies.add(this.defaultNegotiationStrategy);
                 }
             }
    
             this.contentNegotiationManager = new ContentNegotiationManager(strategies);
             return this.contentNegotiationManager;
         }
     }
    
  3. ContentNegotiationManager

    • resolveMediaTypes: ContentNegotiationStrategy#resolveMediaTypes
    • resolveFileExtensions: MediaTypeFileExtensionResolver#resolveFileExtensions

      public class ContentNegotiationManager implements ContentNegotiationStrategy, MediaTypeFileExtensionResolver {
      
        private final List<ContentNegotiationStrategy> strategies = new ArrayList<>();
        private final Set<MediaTypeFileExtensionResolver> resolvers = new LinkedHashSet<>();
      
        @Override
        public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
            for (ContentNegotiationStrategy strategy : this.strategies) {
                List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
                if (mediaTypes.equals(MEDIA_TYPE_ALL_LIST)) {
                    continue;
                }
                return mediaTypes;
            }
            return MEDIA_TYPE_ALL_LIST;
        }
      
        @Override
        public List<String> resolveFileExtensions(MediaType mediaType) {
            Set<String> result = new LinkedHashSet<>();
            for (MediaTypeFileExtensionResolver resolver : this.resolvers) {
                result.addAll(resolver.resolveFileExtensions(mediaType));
            }
            return new ArrayList<>(result);
        }
      
        // ... 
      }
      
      @FunctionalInterface
      public interface ContentNegotiationStrategy {
        List<MediaType> MEDIA_TYPE_ALL_LIST = Collections.singletonList(MediaType.ALL);
        List<MediaType> resolveMediaTypes(NativeWebRequest webRequest)
                throws HttpMediaTypeNotAcceptableException;
      }
      

DispatcherServlet

webmvc

package org.springframework.web.servlet;

  1. DispatcherServlet#doDispatch(request,response)

    • HandlerExecutionChain { handler(eg:HandlerMethod) & interceptorList }
      • List handlerMappings
      • HandlerMapping#getHandler!=null
    • HandlerAdapter
      • List handlerAdapters
      • HandlerAdapter#support
    • ModelAndView { model:ModelMap,view(eg:viewName) }
      • HandlerAdapter#handle
      • HandlerAdapter#invokeHandlerMethod
    • View
      • List viewResolvers
      • ViewResolver#resolveViewName(ModelAndView#view,local)
    • View#render(ModelAndView#model)
  2. HandlerMapping#getHandler -> HandlerExecutionChain

     /*
     HandlerMapping
         - AbstractHandlerMapping
             + AbstractHandlerMethodMapping<T>
                 - RequestMappingInfoHandlerMapping<RequestMappingInfo>
                     * RequestMappingHandlerMapping
             + AbstractUrlHandlerMapping
                 - AbstractDetectingUrlHandlerMapping
                     * BeanNameUrlHandlerMapping
                 - SimpleUrlHandlerMapping
             + EmptyHandlerMapping
     */
    
    • AbstractHandlerMapping
      • getHandler
        • abstract getHandlerInternal
    • AbstractHandlerMethodMapping
      • afterPropertiesSet -> initHandlerMethods -> detectHandlerMethods
        • abstract getMappingForMethod
        • registerHandlerMethod
      • override getHandlerInternal -> lookupHandlerMethod
        • abstract getMatchingMapping
        • protected handleMatch
        • protected handleNoMatch
    • RequestMappingInfoHandlerMapping
      • override getMatchingMapping
      • override handleMatch
      • override handleNoMatch
    • RequestMappingHandlerMapping
      • override getMappingForMethod -> createRequestMappingInfo
  3. HandlerAdapter#handle -> ModelAndView{modelMap,viewName}

     /*
     HandlerAdapter
         - AbstractHandlerMethodAdapter
             * RequestMappingHandlerAdapter
         - HttpRequestHandlerAdapter
         - SimpleControllerHandlerAdapter
         - SimpleServletHandlerAdapter
    
     HandlerMethodArgumentResolver
         - AbstractMessageConverterMethodArgumentResolver
             + AbstractMessageConverterMethodProcessor
                 * RequestResponseBodyMethodProcessor
                 * HttpEntityMethodProcessor
             + RequestPartMethodArgumentResolver
         - ...
    
     HandlerMethodReturnValueHandler
         - AbstractMessageConverterMethodProcessor
             * RequestResponseBodyMethodProcessor
             * HttpEntityMethodProcessor
         - ...
    
     HttpMessageConverter
         - AbstractHttpMessageConverter<T>
             + AbstractGenericHttpMessageConverter<T>
                 - AbstractJackson2HttpMessageConverter
                     * MappingJackson2HttpMessageConverter
                     * MappingJackson2XmlHttpMessageConverter
                     * ...
                 - AbstractJsonHttpMessageConverter
                     * GsonHttpMessageConverter
                     * JsonbHttpMessageConverter
                 - PropertiesHttpMessageConverter
                 - ResourceRegionHttpMessageConverter
             + ByteArrayHttpMessageConverter
             + ObjectToStringHttpMessageConverter
             + ResourceHttpMessageConverter
             + StringHttpMessageConverter
             + ...
         - ...
     */
    
    • AbstractHandlerMethodAdapter
      • support: expects the handler to be an HandlerMethod
        • abstract supportsInternal
      • handler
        • abstract handleInternal
    • RequestMappingHandlerAdapter
      • afterPropertiesSet
      • override supportsInternal : return true
      • override handleInternal -> invokeHandleMethod -> ServletInvocableHandlerMethod#invokeAndHandle
        • getMethodArgumentValues(HandlerMethodArgumentResolverComposite#resolveArgument)
        • doInvoke
        • handleReturnValue (HandlerMethodReturnValueHandlerComposite#handleReturnValue)
    • RequestResponseBodyMethodProcessor (extends AbstractMessageConverterMethodProcessor)
      • resolve method args for @RequestBody(implements HandlerMethodArgumentResolver)
        • supportsParameter
        • resolveArgument
          • HttpMessageConverter#canRead
          • HttpMessageConverter#read
      • handle return value for @ResponseBody(implements HandlerMethodReturnValueHandler)
        • supportsReturnType
        • handleReturnValue
          • HttpMessageConverter#canWrite
          • HttpMessageConverter#write
  4. ViewResolver#resolveViewName(ModelAndView#viewName) -> View

     /*
     ViewResolver
      - AbstractCachingViewResolver
          + ResourceBundleViewResolver
          + ThymeleafViewResolver
              - AjaxThymeleafViewResolver
          + UrlBasedViewResolver
              - AbstractTemplateViewResolver
                  * FreeMarkerViewResolver
                  * GroovyMarkupViewResolver
              - InternalResourceViewResolver
              - ...
          + XmlViewResolver
      - BeanNameViewResolver
      - ContentNegotiatingViewResolver
      - ViewResolverComposite
     */
    
    • ThymeleafViewResolver (Integer.MAX_VALUE)
    • ContentNegotiatingViewResolver (Ordered.HIGHEST_PRECEDENCE)
    • InternalResourceViewResolver (Ordered.LOWEST_PRECEDENCE)
    • ViewResolverComposite (Ordered.LOWEST_PRECEDENCE)
  5. View#render(ModelAndView#modelMap)

     /*
     View
         - AbstractView
             + AbstractFeedView<T extends WireFeed>
                 * AbstractAtomFeedView
                 * AbstractRssFeedView
             + AbstractJackson2View
                 * MappingJackson2JsonView
                 * MappingJackson2XmlView
             + AbstractPdfView
             + AbstractUrlBasedView
                 - AbstractTemplateView
                     * FreeMarkerView
                     * GroovyMarkupView
                 - InternalResourceView
                     * JstlView
                 - RedirectView
                 - AbstractPdfStamperView
                 - ScriptTemplateView
                 - TilesView
                 - XsltView
             + AbstractXlsView
                 - AbstractXlsxView
                     * AbstractXlsxStreamingView
             + MarshallingView
         - AbstractThymeleafView
             + ThymeleafView
                 - AjaxThymeleafView
                     * FlowAjaxThymeleafView
     */
    
    • JstlView
    • ThymeleafView
    • FreeMarkerView
    • RedirectView
    • MappingJackson2JsonView

部分源码:

  1. DispatcherServlet (总控)

    // 1. DispatcherServlet
    public class DispatcherServlet extends FrameworkServlet {
    
     @Override
     protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception
     => doDispatch(request, response);
    
     protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception
     => {
         // 2.1 HandlerMapping -> HandlerExecutionChain: handler & interceptorList
         // eg: RequestMappingHandlerMapping -> HandlerExecutionChain: handler (HandlerMethod) & interceptorList
         HandlerExecutionChain mappedHandler = getHandler(processedRequest);
         => {
             for (HandlerMapping hm : this.handlerMappings) {
                 HandlerExecutionChain handler = hm.getHandler(request);
                 if (handler != null) {
                     return handler;
                 }
             }
         }
    
         // 2.2 HandlerAdapter
         // eg: RequestMappingHandlerAdapter
         HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
         =>{
             for (HandlerAdapter ha : this.handlerAdapters) {
                 if (ha.supports(handler)) {
                     return ha;
                 }
             }
         }
    
         // 2.3 HandlerAdapter -> invoke handlerMethod -> ModelAndView: ModelMap & View (eg: viewName) & HttpStatus
         ModelAndView mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    
         // 2.4 ViewResolver -> View -> Render & Response
         processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
         =>{
             if(dispatchException!=null){
                 if dispatchException instanceof ModelAndViewDefiningException
                     mv = ((ModelAndViewDefiningException) exception).getModelAndView();
                 else
                     mv = processHandlerException(request, response, handler, exception);
                         /*=> {
                             for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
                                 exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
                                 if (exMv != null) {
                                     break;
                                 }
                             }
                         }*/
             }
             render(mv, request, response);
             =>{
                 String viewName = mv.getViewName();
                 View view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
                     /*=> {
                         for (ViewResolver viewResolver : this.viewResolvers) {
                             View view = viewResolver.resolveViewName(viewName, locale);
                             if (view != null) {
                                 return view;
                             }
                         }
                     }*/
                 view.render(mv.getModelInternal(), request, response);
             }
             mappedHandler.triggerAfterCompletion(request, response, null);
         }
    
         // final{...}
     }
    }
    
  2. HandlerMapping: RequestMappingHandlerMapping

    • 内部相当于维护了一个Map<RequestMappingInfo,HandlerMethod>getHandler时根据当前request找到匹配的HandlerMethod
    • AbstractHandlerMethodMapping (extends AbstractHandlerMapping)
        // 1. afterPropertiesSet -- 会注册所有mapping(requestMapping和各个Class method关系)
        //        -> initHandlerMethods 
        //            -> detectHandlerMethods 
        //                -> call abstract func:getMappingForMethod
        protected void initHandlerMethods() {
            //Looking for request mappings in application context
            String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
                    BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) :
                    obtainApplicationContext().getBeanNamesForType(Object.class));
            for (String beanName : beanNames) {
                if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
                    Class<?> beanType = obtainApplicationContext().getType(beanName);
                    if (beanType != null && isHandler(beanType))
                        detectHandlerMethods(beanName);
                        =>{
                            //detectHandlerMethods(handler):
                            Class<?> handlerType = (handler instanceof String ?
                                    obtainApplicationContext().getType(handler) : handler.getClass());
                            final Class<?> userType = ClassUtils.getUserClass(handlerType);
                            Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
                                    method -> {
                                        return getMappingForMethod(method, userType); // abstract func!
                                    });
                            // request handler methods found on ${userType}.${methods}
                            methods.forEach((method, mapping) -> {
                                Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
                                registerHandlerMethod(handler, invocableMethod, mapping);
                            });
                        }
                }
            }
            handlerMethodsInitialized(getHandlerMethods());
        }
        // 2. getHandlerInternal -- 找到与request匹配的HandleMethod
        //        -> lookupHandlerMethod 
        //            -> call abstract func: getMatchingMapping
        //            -> call protected func: handleMatch
        //            -> call protected func: handleNoMatch
        protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
            String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
            //Looking up handler method for the request path
            this.mappingRegistry.acquireReadLock();
            try {
                HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
                return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
            }
            finally {
                this.mappingRegistry.releaseReadLock();
            }
        }
        protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
            List<Match> matches = new ArrayList<>();
            List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
            if (directPathMatches != null) {
                addMatchingMappings(directPathMatches, matches, request);
                // foreach directPathMatches to get MatchingMaping by call abstract getMatchingMapping:
                /*
                    for:directPathMatches{
                        T match = getMatchingMapping(mapping, request);    // abstract func!
                        if match!=null
                            matches.add(new Match(match, this.mappingRegistry.getMappings().get(mapping)));
                    }                    
                */
            }
            if (matches.isEmpty()) {
                // No choice but to go through all mappings...
                addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
            }
            if (!matches.isEmpty()) {
                Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
                matches.sort(comparator);
                Match bestMatch = matches.get(0);
                if (matches.size() > 1) {
                    if (CorsUtils.isPreFlightRequest(request)) {
                        return PREFLIGHT_AMBIGUOUS_MATCH;
                    }
                    Match secondBestMatch = matches.get(1);
                    if (comparator.compare(bestMatch, secondBestMatch) == 0) {
                        Method m1 = bestMatch.handlerMethod.getMethod();
                        Method m2 = secondBestMatch.handlerMethod.getMethod();
                        throw new IllegalStateException("Ambiguous handler methods mapped for HTTP path '" +
                                request.getRequestURL() + "': {" + m1 + ", " + m2 + "}");
                    }
                }
                handleMatch(bestMatch.mapping, lookupPath, request);    // protected func!
                return bestMatch.handlerMethod;
            }
            else {
                return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);  // protected func!
            }
        }
      
    • RequestMappingInfoHandlerMapping (extends AbstractHandlerMethodMapping)

        // 1. getMatchingMapping
        //        ->RequestMappingInfo#getMatchingCondition
        @Override
        protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info, HttpServletRequest request) {
            return info.getMatchingCondition(request);
            =>{
                // RequestMappingInfo#getMatchingCondition:
                RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
                ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
                HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
                ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);
                ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
                if (methods == null || params == null || headers == null || consumes == null || produces == null) {
                    return null;
                }
                PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(request);
                if (patterns == null) {
                    return null;
                }
                RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request);
                if (custom == null) {
                    return null;
                }
                return new RequestMappingInfo(this.name, patterns,
                        methods, params, headers, consumes, produces, custom.getCondition());
            }
        }
      
        // 2. handleMatch
        @Override
        protected void handleMatch(RequestMappingInfo info, String lookupPath, HttpServletRequest request) {
            request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, lookupPath);
            Set<String> patterns = info.getPatternsCondition().getPatterns();
            //bestPattern,uriVariables,decodedUriVariables -> request.setAttribute(xxx,xxx)
            //...
        }
      
        // 3. handleNoMatch
        @Override
        protected HandlerMethod handleNoMatch(
                Set<RequestMappingInfo> infos, String lookupPath, HttpServletRequest request) throws ServletException{
            PartialMatchHelper helper = new PartialMatchHelper(infos, request);
            helper.hasMethodsMismatch(): throw new HttpRequestMethodNotSupportedException(request.getMethod(), methods);
            helper.hasConsumesMismatch(): throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<>(mediaTypes));
            helper.hasProducesMismatch(): throw new HttpMediaTypeNotAcceptableException(new ArrayList<>(mediaTypes));
            helper.hasParamsMismatch(): throw new UnsatisfiedServletRequestParameterException(conditions, request.getParameterMap());
            return null;
        }
      
    • RequestMappingHandlerMapping (extends RequestMappingInfoHandlerMapping)
        // 1. getMappingForMethod 
        //        -> createRequestMappingInfo:
        protected RequestMappingInfo createRequestMappingInfo(
                    RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) {
            RequestMappingInfo.Builder builder = RequestMappingInfo
                    .paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
                    .methods(requestMapping.method())
                    .params(requestMapping.params())
                    .headers(requestMapping.headers())
                    .consumes(requestMapping.consumes())
                    .produces(requestMapping.produces())
                    .mappingName(requestMapping.name());
            if (customCondition != null)
                builder.customCondition(customCondition);
            return builder.options(this.config).build();
        }
        // 2. class 成员变量: contentNegotiationManager
        private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager();
      
  3. HandlerAdapter: RequestMappingHandlerAdapter

    • RequestMappingHandlerAdapter (extends AbstractHandlerMethodAdapter)

        // 1. constructor : initial messageConverters : List<HttpMessageConverter<?>>
        public RequestMappingHandlerAdapter() {
            StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
            stringHttpMessageConverter.setWriteAcceptCharset(false); // See SPR-7316
            this.messageConverters = new ArrayList<HttpMessageConverter<?>>();
            this.messageConverters.add(new ByteArrayHttpMessageConverter());
            this.messageConverters.add(stringHttpMessageConverter);
            this.messageConverters.add(new SourceHttpMessageConverter<Source>());
            this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
        }
        // 2. afterPropertiesSet: initial argumentResolvers,returnValueHandlers,...
        //        -> this.argumentResolvers : HandlerMethodArgumentResolverComposite
        //        -> this.initBinderArgumentResolvers : HandlerMethodArgumentResolverComposite
        //        -> this.returnValueHandlers : HandlerMethodReturnValueHandlerComposite
        @Override
        public void afterPropertiesSet(){
            initControllerAdviceCache();
            if (this.argumentResolvers == null) {
                List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
                /*
                =>{
                    //resolvers.add(...)
                    //...
                    // use RequestResponseBodyMethodProcessor to resolve @RequestBody method arguments
                    resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
                    //...
                }*/
                this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers)
            }
            if (this.initBinderArgumentResolvers == null) {
                List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
                this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
            }
            if (this.returnValueHandlers == null) {
                List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
                /*
                    =>{
                        //handlers.add(...)
                        //...
                        // use RequestResponseBodyMethodProcessor to handle @ResponseBody return value
                        handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(),
                            this.contentNegotiationManager, this.requestResponseBodyAdvice));
                        //...
                    }
                */
                this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
            }
        }
      
        // 3. handleInternal 
        //        -> invokeHandleMethod
        //            -> ServletInvocableHandlerMethod#invokeAndHandle :
        //                -> getMethodArgumentValues
        //                -> doInvoke
        //                -> handleReturnValue
        @Override
        protected final ModelAndView handleInternal(HttpServletRequest request,
                HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
            // ...
        }
      
    • ServletInvocableHandlerMethod (extends InvocableHandlerMethod)

        // 1. invokeAndHandle
        //        -> getMethodArgumentValues(HandlerMethodArgumentResolverComposite#resolveArgument)
        //        -> doInvoke
        //        -> handleReturnValue (HandlerMethodReturnValueHandler#handleReturnValue)
        public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
                    Object... providedArgs) throws Exception 
        =>{
            // 1. resolveArgument(HandlerMethodArgumentResolver#resolveArgument) 
            //        => args -> handlerMethod -> returnValue
            Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);    
                => {
                    // InvocableHandlerMethod#invokeForRequest:
                    Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
                        /*    
                        InvocableHandlerMethod#getMethodArgumentValues:{
                            MethodParameter[] parameters = getMethodParameters();
                            for:parameters{
                                args[i] = resolveProvidedArgument(parameter, providedArgs)
                                if (args[i] == null && this.argumentResolvers.supportsParameter(parameter))
                                    args[i] = this.argumentResolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
                            }
                        }    
                        */
                    // 2. doInvoke
                    Object returnValue = doInvoke(args);
                    return returnValue;
                }
      
            // 3. handleReturnValue (HandlerMethodReturnValueHandler#handleReturnValue)
            this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
        }
      
        // 2. class 成员变量:argumentResolvers -- use to resolveArgument
        private HandlerMethodArgumentResolverComposite argumentResolvers = new HandlerMethodArgumentResolverComposite();
        /*
            class HandlerMethodArgumentResolverComposite {
                private final List<HandlerMethodArgumentResolver> argumentResolvers = new LinkedList<>();
                @Override
                public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
                    NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception{
                    HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
                    =>{
                        for resolver: this.argumentResolvers{
                            if (resolver.supportsParameter(parameter))
                                return resolver;
                        }
                    }
                    return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
                }
            }
        */
      
        // 3. class 成员变量:returnValueHandlers -- use to handleReturnValue
        private HandlerMethodReturnValueHandlerComposite returnValueHandlers;
        /*
            class HandlerMethodReturnValueHandlerComposite {
                private final List<HandlerMethodReturnValueHandler> returnValueHandlers = new ArrayList<>();
                @Override
                public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
                        ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception{
                    HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
                    =>{
                        for:this.returnValueHandlers{
                            if(handler.supportsReturnType(returnType))
                                return handler;
                        }
                    }
                    handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
                }
            }
        */
      
    • RequestResponseBodyMethodProcessor: resolve args for @RequestBody & handle return value for @ResponseBody
        /**
         * Resolves method arguments annotated with {@code @RequestBody} and handles return
         * values from methods annotated with {@code @ResponseBody} by reading and writing
         * to the body of the request or response with an {@link HttpMessageConverter}.
         */
        public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
            @Override
            public boolean supportsParameter(MethodParameter parameter) {
                return parameter.hasParameterAnnotation(RequestBody.class);
            }
            @Override
            public boolean supportsReturnType(MethodParameter returnType) {
                return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
                        returnType.hasMethodAnnotation(ResponseBody.class));
            }
            @Override
            public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
                    NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception{
                Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
                    /*
                        //AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters:
                        contentType: get from requestHeader
                        for:this.messageConverters{
                            if(converter.canRead(...))
                                return converter.read(...) // HttpMessageConverter#read
                        }
                    */
                return adaptArgumentIfNecessary(arg, parameter);
            }
            @Override
            public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
                    ModelAndViewContainer mavContainer, NativeWebRequest webRequest){
                writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage)
                    /*
                    //AbstractMessageConverterMethodProcessor#writeWithMessageConverters:
                    acceptableMediaTypes & producibleMediaTypes => Compatible && Concrete => mediaTypes
                        => {
                            AcceptableMediaTypes: this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
                            producibleMediaTypes: 
                                mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
                                or:
                                    for: this.messageConverters{
                                        if(converter.canWrite(...))
                                            result.addAll(converter.getSupportedMediaTypes())
                                    }
                        }
                    for: this.messageConverters{
                        if(converter.canWrite(...)){
                            outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,converter.getClass(),
                            inputMessage, outputMessage);
                            addContentDispositionHeader(inputMessage, outputMessage);
                            converter.write(outputValue, selectedMediaType, outputMessage);    // HttpMessageConverter#write
                        }
                    }                    
                    */
            }
        }
      

WebFlux

  • jar: spring-webflux (dependency: spring-web,reactor-core)
  • Reactive思想的一种实现(数据流Data Stream,非阻塞 Non-Blocking,推模式push-based,背压Backpressure
  • 依赖Reactor类库(jar:reactor-core)
    • 依赖reactive-streams
      • 核心组件:Publisher,Subscriber,Subscription,Processor
      • 背压处理
    • 核心组件:Mono,Flux,Scheduler
  • 依赖SpringWeb类库(jar: spring-web)
    • HttpHandler: Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response);
    • WebHandler: Mono<Void> handle(ServerWebExchange exchange);
  • 编程模型:
  • 核心组件(功能大同WebMvc, jar: spring-webflux, package:o.s.w.reactive...):
    • 总控 DispatcherHandler (implements WebHandler)
    • 处理器管理
      • 映射 HandlerMapping (eg: RequestMappingHandlerMapping)
      • 适配 HandlerAdapter (eg: RequestMappingHandlerAdapter)
      • 执行 HandlerResult
    • 异常处理 HandlerResult#exceptionHandler
    • HandlerMethod: @RequestMapping/@XxxMapping标注的方法
      • HandlerMethod参数解析器: HandlerMethodArgumentResolver
      • Handler返回值解析器: HandlerResultHandler
        • ResponseBodyResultHandler
        • ResponseEntityResultHandler
        • ServerResponseResultHandler
        • ViewResolutionResultHandler
          • RequestedContentTypeResolverBuilder
          • RequestedContentTypeResolver
    • 视图解析器 ViewResolver
    • 视图渲染 View, eg:
      • RedirectView
      • FreeMarkerView
      • HttpMessageWriterView
    • 配置
      • WebFluxConfigurer
      • WebFluxConfigurationSupport
    • for Functional Endpoints(函数式端点方式)
      • RouterFunction
          @FunctionalInterface 
          // 标注表示这是一个函数式接口
          // 只能标注在接口上,且该接口只有一个抽象方法(接口默认方法以及声明中覆盖 Object 的公开方法不算)
          // 标注后此接口的实例可被 Lambda 表示式、方法引用或构造器引用创建
          public interface RouterFunction<T extends ServerResponse> {
              Mono<HandlerFunction<T>> route(ServerRequest request);
              // default:
              // and,andOther,andRoute,andNest,filter,accept
          }
        
      • RouterFunctionMapping (implements HandlerMapping)
      • HandlerFunctionAdapter (implements HandlerAdapter)
  • 使用场景:不适合RT(响应)敏感的RPC框架/Web应用,适合请求慢慢执行的场景(把请求丢过来,不care什么时候完成,完成后通知你下就即可),Reactive可以提升吞吐量,但RunTime反而会变得更长,且出现响应超时等问题
  • Spring WebMVC vs. Spring WebFlux
    • 均能使用注解驱动Controller,WebFlux还能使用函数式端点方式
    • 主要不同点在于并发模型和阻塞特性:
      • Spring WebMvc: Servlet应用,通常是阻塞服务,Servlet容器一般使用较大的线程池处理请求
      • Spring WebFlux: Reactive应用,通常是非阻塞服务,服务器可使用少量、固定大小的线程池处理请求
    • 目前WebFlux并未优于WebMvc:
      • 性能上没有明显的速度提升(甚至性能结果稍微更恶劣)
      • 在编程友好性方面,Reactive编程尽管没有新增大量的代码,却使编码(和调试)变得复杂了
      • 缺少文档

Java 并发模型

  • 阻塞/非阻塞 (编程模型)
    IO NIO
    阻塞 非阻塞
    面向流 面向缓冲
    线程被阻塞直到读/写完 无数据可读/写时,该线程可继续做其他事
    一个线程只管理一个输入输出通道 一个线程可以管理多个输入输出通道
    使用场景:少量的连接使用较高的带宽,一次发送大量的数据 使用场景:聊天服务器要管理很多个连接,每次只是发送少量的数据,一个线程就可以管理多个连接,提升系统吞吐量
  • 同步/异步 (线程模型)
  • 组合:
    • 阻塞 & 同步/异步,eg:
      • 迭代器模式(Iterator): 阻塞同步
      • Future#get: blocking & 异步->串行
    • 非阻塞 & 同步/异步,eg:
      • Observer模式(观察者模式,事件/监听者模式): 非阻塞
      • Reactor模式(反应堆模式): 非阻塞同步
      • Proactor模式 : 非阻塞异步

示例:数据加载(阻塞任务) + 同步/异步

  1. 在各个任务间没有数据依赖关系的情况下:
    • 单线程 -> 串行:时间消耗线性累加
    • 多线程 -> 并行:时间消耗取最大者,性能和资源利用率明显地得到提升
  2. 在任务间有依赖时:
    • 依赖任务不论是否在同一线程,都会串行执行(又回到了阻塞模式)
    • 与非依赖的异步任务,还是并行执行
  3. 总结:如果任务间有依赖,或则设置强制等待某个任务结束,并发模型并不会提高执行效率
public class AsyncTest {
    private long startTime;
    public void load(String source,int seconds) {
        try {
            long startTime = System.currentTimeMillis();
            long milliseconds = TimeUnit.SECONDS.toMillis(seconds);
            Thread.sleep(milliseconds);
            long costTime = System.currentTimeMillis() - startTime;
            System.out.printf("[Thread : %s] %s cost :  %d ms \n",Thread.currentThread().getName(), source, costTime);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
    @Before
    public void before() {
        startTime = System.currentTimeMillis();
    }
    @After
    public void after() {
        long costTime = System.currentTimeMillis() - startTime;
        System.out.println("total cost:" + costTime + " ms");
    }

    // 同步:只有一个main线程 => 串行执行
    // total cost: 1.x + 2.x  + 3.x > 6s (有些线程调度切换等花费掉的时间)
    // Detail:
    // [Thread : main] loadConfigurations cost :  1005 ms 
    // [Thread : main] loadUsers cost :  2005 ms 
    // [Thread : main] loadOrders cost :  3005 ms 
    // total cost:6016 ms
    @Test
    public void serialLoadTest(){ 
        load("loadConfigurations",1);
        load("loadUsers",2);
        load("loadOrders",3);
    }

    // 异步: 多线程 => 并行执行
    // main线程通过轮询来检查是否都完成了,各个子任务间无依赖,也不会阻塞main线程
    // total cost: max(1.x, 2.x, 3.x)  = 3.x > 3s
    // Detail:
    // [Thread : pool-1-thread-1] loadConfigurations cost :  1000 ms 
    // [Thread : pool-1-thread-2] loadUsers cost :  2002 ms 
    // [Thread : pool-1-thread-3] loadOrders cost :  3005 ms 
    // total cost:3063 ms
    @Test
    public void parallelLoadTest() {    
        ExecutorService executorService = Executors.newFixedThreadPool(3); // 创建线程池
        CompletionService<String> completionService = new ExecutorCompletionService<String>(executorService);
        completionService.submit(()->{load("loadConfigurations",1);}, null);      //  耗时 >= 1s
        completionService.submit(()->{load("loadUsers",2);}, null);               //  耗时 >= 2s
        completionService.submit(()->{load("loadOrders",3);}, null);              //  耗时 >= 3s
        int count = 0;
        while (count < 3) { // 等待三个任务完成
            if (completionService.poll() != null) {
                count++;
            }
        }
        executorService.shutdown();
    }

    // 异步:多线程 + Future#get() => 并行->串行执行, main thead 阻塞
    // 多个任务提交后,返回的多个Future逐一调用get()方法时,将会依次blocking main thread(等待直到当前任务执行完成),从而任务的执行从并行变为串行执行
    // total cost: 1.x + 2.x + 3.x > 6s 
    // Detail:
    // [Thread : pool-1-thread-1] loadConfigurations cost :  1001 ms 
    // [Thread : pool-1-thread-2] loadUsers cost :  2005 ms 
    // [Thread : pool-1-thread-3] loadConfigurations cost :  3006 ms 
    // total cost:6077 ms
    @Test
    public void futureBlockingLoadTest() {
       ExecutorService executorService = Executors.newFixedThreadPool(3); // 创建线程池
       runCompletely(executorService.submit(()->{load("loadConfigurations",1);}));
       runCompletely(executorService.submit(()->{load("loadUsers",2);}));
       runCompletely(executorService.submit(()->{load("loadConfigurations",3);}));
       executorService.shutdown();
    }
    private void runCompletely(Future<?> future) {
        try {
            future.get();
        } catch (Exception e) {
        }
    }

    // 异步:多线程  => main 和 sub-thread并行,sub-thread串行执行任务且完成时触发回调
    // Detail:
    // Hello
    // [Thread : ForkJoinPool.commonPool-worker-1] loadConfigurations cost :  1004 ms 
    // [Thread : ForkJoinPool.commonPool-worker-1] loadUsers cost :  2004 ms 
    // [Thread : ForkJoinPool.commonPool-worker-1] loadOrders cost :  3005 ms 
    // [Thread :ForkJoinPool.commonPool-worker-1] Complete 6072ms
    @Test
    public void chainLoadTest() throws InterruptedException {
        // main -> submit -> ...
        // sub-thread : F1 -> F2 -> F3
        CompletableFuture
            .runAsync(()->{load("loadConfigurations",1);})
            .thenRun(()->{load("loadUsers",2);})
            .thenRun(()->{load("loadOrders",3);})
            .whenComplete((result, throwable) -> { // 完成时回调
                long costTime = System.currentTimeMillis() - startTime;
                System.out.println("[Thread :" + Thread.currentThread().getName() + "] Complete "+costTime+"ms");
            })
            .exceptionally(throwable->{
                System.out.println("[Thread :" + Thread.currentThread().getName() + "] Exception");
                return null;
            })
            //.join()    // 等待完成
            ; 

        System.out.println("Hello");
        Thread.sleep(8000);
    }

    // Java GUI 事件/监听者模式(即观察者模式)的并发模型可为同步或异步
    // 通过回调`Callbacks`实现非阻塞
    // 注意:`Callbacks Hell`, 监听的维度很多时则对应的Callback实现也会很多
    @Test
    public void callbackTest() throws IOException {
        JFrame jFrame = new JFrame("GUI 示例");
        jFrame.setBounds(500, 300, 400, 300);
        LayoutManager layoutManager = new BorderLayout(400, 300);
        jFrame.setLayout(layoutManager);
        jFrame.addMouseListener(new MouseAdapter() { // callback 1
         @Override
         public void mouseClicked(MouseEvent e) {
             System.out.printf("[线程 : %s] 鼠标点击,坐标(X : %d, Y : %d)\n",
                     Thread.currentThread().getName(), e.getX(), e.getY());
         }
        });
        jFrame.addWindowListener(new WindowAdapter() {  // callback 2
         @Override
         public void windowClosing(WindowEvent e) {
             System.out.printf("[线程 : %s] 清除 jFrame... \n", Thread.currentThread().getName());
             jFrame.dispose(); // 清除 jFrame
         }

         @Override
         public void windowClosed(WindowEvent e) {
             System.out.printf("[线程 : %s] 退出程序... \n", Thread.currentThread().getName());
             System.exit(0); // 退出程序
         }
        });
        System.out.println("当前线程:" + Thread.currentThread().getName());
        jFrame.setVisible(true);

        System.in.read();
    }
}

Reactive

  1. Reactive 思想

    • 非阻塞下同步/异步执行 -- 多功能
      • 阻塞导致性能瓶颈和浪费资源,但非阻塞也不一定提升性能
      • 增加线程可能会引起资源竞争和并发问题
    • 数据在生产与消费间的平衡(背压)
  2. Reactive Programming (编程模型)

    • 观察者模式(observer pattern)的延伸
    • 处理流式数据(streams: sequenced data/events)
    • 非阻塞下的同步/异步执行 (no-blocking)
    • 推拉相结合(push-based and pull-based)
    • 响应数据传播时的变化 (responsive)
    • 结合背压(Backpressure)技术处理数据生产与消费的平衡问题
    • Observable vs Iterable: An Observable(RxJava) is the asynchronous/push “dual” to the synchronous/pull Iterable
      • Observer 观察者:被动通知(非阻塞回调方式),推动模式(eg: 服务器端推送到客户端)
      • Iterator 迭代器:主动触发(循环遍历),拉动模式(eg: 客户端主动for循环)
        event Iterable (pull) Observable (push)
        data T next() onNext(T)
        discover error throws Exception onError(Exception)
        complete !hasNext() onCompleted()
    • 注:观察者模式(即事件/监听者模式)的并发模型可为同步或异步,eg:
      • Spring (同步/异步)事件/监听器:
        • 事件: ApplicationEvent
        • 事件监听器: ApplicationListener
        • 事件广播器: ApplicationEventMulticaster
        • 事件发布器: ApplicationEventPublisher
      • Servlet 同步事件/监听器:
        • 事件: ServletContextEvent
        • 事件监听器: ServletContextListener
      • Servlet 异步事件/监听器:
        • 事件: AsyncEvent
        • 事件监听器: AsyncListener
  3. Reactive 实现框架

    • RxJava (Reactive Extensions)
    • Reactor (Spring/SpringBoot WebFlux使用此Reactive类库)
    • Flow API (Java 9)
    • 注:Java 原生技术Stream的限制Stream属于迭代器模式,拉模式)
      • 有限操作符
      • 不支持背压
      • 不支持订阅
  4. 场景

    • 管理流式数据在异步边界的交换(reactive-streams)
      • govern the exchange of stream data
      • across an asynchronous boundary
    • 通常并非让应用运行更快速,而是要利用较少的资源提升伸缩性(SpringFramework)
      • generally do not make applications run faster
      • scale with a small, fixed number of threads and less memory
    • 更好可读性 more readable (ReactiveX)
    • 结构性,高层次并发抽象 High level abstraction (Reactor)

Reactor

  • jar: reactor-core (dependency jar: reactive-streams)
  • 核心API:
    • Mono : 0~1的非阻塞结果(类似Optional,点对点模式),实现 reactive-streams Publisher
    • Flux : 0~N的非阻塞序列(类似Stream,发布/订阅者模式),实现 reactive-streams Publisher
    • Scheduler : Reactor调度线程池
      线程池 方法 内部名称 线程名称 线程数量 线程idel时间
      单复用线程 Schedulers.single() single single 1 Long Live
      弹性线程池 Schedulers.elastic() elastic elastic-evictor-{num} 无限制(unbounded) 60s
      并行线程池 Schedulers.parallel() parallel parallel-{num} 处理器数量 60s
      • 底层实现:ScheduledThreadPoolExecutor
      • 查看当前线程:Schedulers.immediate() ( 等价于 Thread.currentThread() )
  • reactive-streams

    • 定义

      Reactive Streams is a standard and specification for Stream-oriented libraries for the JVM that

      • process a potentially unbounded number of elements
      • in sequence,
      • asynchronously passing elements between components,
      • with mandatory non-blocking backpressure.
    • 核心API ( package: org.reactivestreams)
      • Publisher :数据发布者(上游)
          public interface Publisher<T> {
              public void subscribe(Subscriber<? super T> s);
          }
        
      • Subscriber :数据订阅者(下游)
          public interface Subscriber<T> {
              public void onSubscribe(Subscription s);    // 当下游订阅时
              public void onNext(T t);                    // 当下游接收数据时
              public void onError(Throwable t);            // 当数据流(Data Streams)执行错误时
              public void onComplete();                    // 当数据流(Data Streams)执行完成时
          }
        
      • Subscription :订阅信号 (Subscriber to Publisher -- 背压)
          public interface Subscription {
              public void request(long n);    // 限定请求上游元素的数量
              public void cancel();            // 请求停止发送数据并且清除资源
          }
        
      • ProcessorPublisherSubscriber综合体(中游,承上启下)
          public interface Processor<T, R> extends Subscriber<T>, Publisher<R> {
          }
        
    • 背压处理(backpressure handle):
      • publisher side maintains a higher rate than the subscriber
      • mediate between threads to be bounded (传播上游信号,eg: request process at most n elements; halt sending )
      • 下游Subscriber工作在无边界unbounded大小的数据流水线,当上游Publisher提供数据的速率快于下游Subscriber的消费数据速率时,下游Subscriber将通过传播信号request到上游Publisher: 请求限制数据的数量(limit number)或通知上游停止数据生产(halt)
  • 示例:Flux

    1. pom.xml
       <dependency>
           <groupId>io.projectreactor</groupId>
           <artifactId>reactor-core</artifactId>
       </dependency>
      
    2. lambde编程方式

      
       private static void println(Object object) {
           String threadName = Thread.currentThread().getName();
           System.out.println("[Thread:" + threadName + "] " + object);
       }
      
       // 同步
       @Test
       public void fluxSyncFunctinalProgramingTest() {
           println("Run fluxSyncFunctinalProgramingTest ...");
           Flux.just("A", "B", "C")             // Sequence: A -> B -> C
               .map(value -> "+" + value)         // "A" -> "+A" 
               .subscribe(
                       ReactorTest::println,     // 数据消费 = onNext(T)
                       ReactorTest::println,     // 异常处理 = onError(Throwable)
                       () -> {                 // 完成回调 = onComplete()
                           println("Complete!");
                       },
                       subscription -> {         // 背压操作 onSubscribe(Subscription)
                           subscription.request(Integer.MAX_VALUE); // 请求的元素数量
                           subscription.cancel();     // 取消上游传输数据到下游
                       }
               );
           /*
            Summary:
            subscription.request(0);    => Exception (n must >0)
            subscription.request(2);    => main: +A -> +B
            subscription.request(5);    => main: +A -> +B -> +C -> Complete!
            subscription.request(2); subscription.cancel(); => main: +A -> +B
            subscription.request(5); subscription.cancel(); => main: +A -> +B -> +C -> Complete!
            subscription.cancel(); ... => none
            */
       }
      
         // 异步
       @Test
       public void fluxAsyncFunctionalProgramingTest() throws InterruptedException {
           println("Run fluxAsyncFunctionalProgramingTest ...");
           Flux.just("A", "B", "C")
               .publishOn(Schedulers.elastic())     // 线程池切换
               .map(value -> "+" + value)
               .subscribe(
                       ReactorTest::println,
                       ReactorTest::println,
                       () -> {
                           println("Complete!");
                       },
                       subscription -> {
                           subscription.request(5);
                        // subscription.cancel();
                       }
               );
           /*
            Summary:
            subscription.request(2); => elastic-2: +A -> +B
            subscription.request(5); => elastic-2: +A -> +B -> +C -> Complete!
            subscription.request(2);subscription.cancel(); => none
           */            
       }
      
    3. 传统编程方式
       @Test
       public void fluxResponseProgramingTest() {
           println("Run fluxResponseProgramingTest ...");
           Flux.just("A", "B", "C")
               //.publishOn(Schedulers.elastic()) 
               .map(value -> "+" + value)
               .subscribe(new Subscriber<String>() {
                   private Subscription subscription;
                   private int count = 0;
                   @Override
                   public void onSubscribe(Subscription s) {
                       subscription = s;
                       subscription.request(1);
                   }
                   @Override
                   public void onNext(String s) {
                       if (count == 2) {
                           throw new RuntimeException("故意抛异常!");
                       }
                       println(s);
                       count++;
                       subscription.request(1);
                   }
                   @Override
                   public void onError(Throwable t) {
                       println(t.getMessage());
                   }
                   @Override
                   public void onComplete() {
                       println("Complete");
                   }
               });            
           /*
            Summary:
            1. sync => main: +A -> +B -> Exception
            2. async: .publishOn(Schedulers.elastic()) => main: +A -> +B 
            */
       }
      

WebFlux

示例:

  1. 注解驱动,函数式端点驱动

     @SpringBootApplication
     @RestController
     public class WebFluxApp {
         public static void main(String[] args) {
             SpringApplication.run(WebFluxApp.class,args);
         }
    
         private static void println(String message) {
             System.out.println("[" + Thread.currentThread().getName() + "] : " + message);
         }
    
         @GetMapping("/mvc")
         public String mvc() {
             println("mvc");
             return "MVC";
         }
    
         @GetMapping("/mono")
         public Mono<String> mono() {
             println("mono");
             return Mono.just("Mono");
         }
    
         @Bean
         public RouterFunction<ServerResponse> hello(){
             // curl -i localhost:8080/hello
             // curl -i -H "Content-Type:application/json" localhost:8080/hello2
             // curl -i localhost:8080/hello3
             return RouterFunctions
                     .route(RequestPredicates.GET("/hello")
                             ,serverRequest->{
                                 return ServerResponse.ok().body(Mono.just("Hello World!"),String.class);
                             })
                     .andRoute(RequestPredicates.GET("/hello2").and(RequestPredicates.contentType(MediaType.APPLICATION_JSON))
                             ,this::hello2Handler)
                     .andRoute(RequestPredicates.GET("/hello3"), this::hello3Handler);
         }
    
         public Mono<ServerResponse> hello2Handler(ServerRequest request){
             Mono<String> helloMono=Mono.just("Hello World 2");
             return ServerResponse.ok().body(helloMono,String.class);
         }
    
         public Mono<ServerResponse> hello3Handler(ServerRequest request){
             Flux<String> helloFlux=Flux.just("A","B","C");
             return ServerResponse.ok().body(helloFlux,String.class);
         }
     }
    
  2. 非阻塞调用阻塞

     @Configuration
     public class RouterFunctionConfiguration {
          @Bean
         //@Autowired
         public RouterFunction<ServerResponse> listUsers(UserRepository userRepository){
             return RouterFunctions.route(RequestPredicates.GET("/users")
                     ,request -> {
                         Collection<User> users = userRepository.list();    // 阻塞
                         Flux<User> userFlux = Flux.fromIterable(users); // 非阻塞
                         return ServerResponse.ok().body(userFlux,User.class);
                     });
         }
     }
    
     @RestController
     public class UserController {
         private final UserRepository userRepository;
         @Autowired
         public UserController(UserRepository userRepository) {
             this.userRepository = userRepository;
         }
         @PostMapping("/users")
         public User save(@RequestParam String name){
             User user = new User();
             user.setName(name);
             if(userRepository.save(user)){
                 System.out.printf("Save %s Successful! \n",user);
             }
             return user;
         }
     }
    
     @Repository
     public class UserRepository {
         private final ConcurrentMap<Integer,User> users = new ConcurrentHashMap<Integer,User>();
         private final static AtomicInteger idGenerator = new AtomicInteger();
         public boolean save(User user){
             Integer id = idGenerator.incrementAndGet();
             user.setId(id);
             return users.put(id,user)==null;
         }
         public Collection<User> list(){
             return users.values();
         }
     }
    
     // Domain:
     // User(Integer id,String name)
     // ...
    
     // main:
     @SpringBootApplication
     public class App {
         public static void main(String[] args) {
             SpringApplication.run(App.class,args);
         }
         /* Verify:
          *  curl -i -X POST localhost:8080/users?name=Tom
          *  curl -i -X POST localhost:8080/users?name=Lucy
          *  curl -i localhost:8080/users
          * */
     }
    

Reference