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
三大特性
自动装配组件: 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,\ ...
- 激活:
嵌入式Web容器:
- Web Servlet:
Tomcat
,Jetty
,Undertow
- Web Reactive:
Netty Web Server
- Web Servlet:
生产准备特性:
- 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
)
- 指标: 内建Metrics,自定义Metrics (visit:
- dependency:
Web
传统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注解:
- Servlet 3.0 规范:加入异步Servlet
- Servlet 3.1 规范:加入非阻塞Servlet
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
- HandlerMethod参数解析器:
- 视图解析器
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
- jar:
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);
- 编程模型:
- 注解式驱动 Annotated Controllers: 同WebMvc使用的注解
- 函数式端点 Functional Endpoints : 使用
RouterFunction
(功能上代替Contoller+HandleMethod)
- 核心组件(功能大同WebMvc, jar:
spring-webflux
, package:o.s.w.reactive...
):- 总控
DispatcherHandler
(implementsWebHandler
) - 处理器管理
- 映射
HandlerMapping
(eg: RequestMappingHandlerMapping) - 适配
HandlerAdapter
(eg: RequestMappingHandlerAdapter) - 执行
HandlerResult
- 映射
- 异常处理
HandlerResult#exceptionHandler
- HandlerMethod:
@RequestMapping
/@XxxMapping
标注的方法- HandlerMethod参数解析器:
HandlerMethodArgumentResolver
- Handler返回值解析器:
HandlerResultHandler
ResponseBodyResultHandler
ResponseEntityResultHandler
ServerResponseResultHandler
ViewResolutionResultHandler
RequestedContentTypeResolverBuilder
RequestedContentTypeResolver
- HandlerMethod参数解析器:
- 视图解析器
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编程尽管没有新增大量的代码,却使编码(和调试)变得复杂了
- 缺少文档
- jar:
基本使用
Hello World(使用嵌入式容器)
创建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等配置信息
- 方式三:使用IDEA的Spring Initializr工具创建(Community版本不支持)
- 方式一: 使用 start spring
配置依赖包(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>
- basic
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!"; } }
运行/发布
- 方式一: 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
- visit
http://localhost:8080
to verify -> should show "Hello World!" in the page
Hello World(使用独立容器)
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>
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!"; } }
打包运行
cd myProject mvn -Dmaven.test.skip -U clean package # 将target下生成的war包丢到Servlet容器运行(eg:Tomcat)
也可使用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
嵌入式容器
- package:
war
/jar
- dependency jar:
spring-boot-starter-web
(default includingspring-boot-starter-tomcat
,也可exclude default,then include other embed servlet container) - coding:
@SpringBootAplication
+main()
run
main 或者java -jar xxx
运行可执行包 或者mvn spring-boot:run
- package:
独立容器
- 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
- package:
eg: 整合上面两种运行方式
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>
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. 热加载
共点:编译/部署项目不重启服务器,都基于Java的类加载器实现 (在容器启动时,启动一后台线程,定时监测class文件的时间戳,判断是否变化了)
热部署:在服务器运行时重新部署项目 —— 多用于生产环境 (会释放内存,比热加载更加干净彻底)
热加载:在运行时重新加载改变的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 -->
单元测试
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); } } }
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!"))) ; } }
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
配置文件:
resources/application.properties
或resources/application.yml
注解:
@Value
, eg:@Value(${content})
@ConfigurationProperties
,eg:@ConfigurationProperties(prefix = "hello")
示例1:@Value
,@ConfigurationProperties
使用
配置文件
resources/application.yml
say: prefix: "Hi" target: "Tom" times: 5 content: "Test: ${say.prefix} ${say.target}"
获取注入配置的属性值
// 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; } }
visit
http://localhost:8080/testSay
to verify,shoud see: "Test: Hi Tom -- SayProperties [prefix=Hi, target=Tom, times=5]"
示例2:多环境配置(不同环境使用不同配置)
resource/application-dev.yml
server: port: 8080 servlet: context-path: /hello say: prefix: "Hi" target: "DevEnv" times: 5
resource/application-prod.yml
server: port: 8090 servlet: context-path: /hello say: prefix: "Hi" target: "ProdEnv" times: 5
resource/application.yml
spring: profiles: active: dev content: "Test: ${say.prefix} ${say.target}"
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 ProgrammingOOP
面向对象 Object Oriented ProgrammingPOP
面向过程 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());
}
}
装配
Spring装配技术:
- 模式注解装配:
@Component
,@Repository
,@Service
,@Controller
,@Configuration
- Enable模块装配:
@Enable模块
- 条件装配:
@Profile
,@Conditional
- 工厂加载机制:
SpringFactoriesLoader
+META-INF/spring.factories
- 模式注解装配:
SpringBoot自动装配:
- 依赖Spring装配技术,基于约定大于配置的原则,实现自动装配
- 实现步骤:
- 激活自动装配
@EnableAutoConfiguration
- 实现装配
XxxAutoConfiguration
- 配置
META-INF/spring.factories
- 激活自动装配
Spring 装配技术
模式注解装配
模式注解(Stereotype Annotations):
an annotation that is used to declare the role that a component plays within the application.
模式注解装配:
@ComponentScan
/<context:component-scan>
扫描并找到
@Component
或者其派生Annotation(eg:@Repository
,@Service
,@Controller
,@Configuration
) 标注的Class,将它们注册为Spring Bean使用示例:
- 使用
<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模块
(模块:具备相同领域的功能组件集合, 组合所形成一个独立的单元)
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
服务熔断模块 实现方式:
注解驱动方式,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 }; }
示例:自定义一个
@EnableHelloWorld
并使用方式一:
@EnableHelloWorld
ImportConfiguration 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
ImportImportSelector implements Class
that used to returnConfiguration 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+)
- 注解方式: 使用
@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(); } }
@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
(要装载的类可通过注解@Order
或implements Ordered
来配置顺序) - 使用示例:
SpringApplication#getSpringFactoriesInstances
SpringBoot自动装配
依赖Spring装配技术,基于约定大于配置的原则,实现自动装配
实现步骤:
- 实现装配配置类
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) }
- 实现装配配置类
源码分析: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 {}; }
示例:自定义一个配置类,实现自动装配
自定义一个配置类
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(); } }
- 方式一:
- 在
resources/META-INF/spring.factories
中配置自定义的AutoConfiguration类org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.cj.autoconfig.auto.HelloWorldAutoConfiguration
激活自动装配,并测试
@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
)
使用
SpringApplication.run
ConfigurableApplicationContext ctx=SpringApplication.run(ApplicationConfiguration.class, args);
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);
new SpringApplicationBuilder
ConfigurableApplicationContext context = new SpringApplicationBuilder(ApplicationConfiguration.class) .web(WebApplicationType.NONE) // NONE,SERVLET,REACTIVE .profiles("Java8") .run(args);
示例:
最简式:
@SpringBootApplication public class DemoSpringApplication { public static void main(String[] args) { SpringApplication.run(DemoSpringApplication.class,args) } }
可配置式:
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 推断
WebApplicationType
:根据当前应用ClassPath 中是否存在相关实现类来推断WebApplication的类型WebApplicationType.REACTIVE
: Web ReactiveWebApplicationType.SERVLET
: Web ServletWebApplicationType.NONE
: 非Web
- 1.2 加载
ApplicationContextInitializer
上下文初始器: 利用 Spring 工厂加载机制,实现类实例化,并排序对象集合 - 1.3 加载
ApplicationListener
事件监听器: 利用 Spring 工厂加载机制,实现类实例化,并排序对象集合 - 1.4 推断引导类(
Main Class
): 根据 Main 线程执行堆栈判断实际的引导类
- 1.1 推断
运行阶段 (run函数)
- 2.1 加载并运行
SpringApplicationRunListeners
运行监听器,监听SpringBoot/Spring事件 - 2.2 创建
Environment
: 根据准备阶段推断的WebApplicationType
创建对应的ConfigurableEnvironment
实例- Web Reactive:
StandardEnvironment
- Web Servlet:
StandardServletEnvironment
- 非 Web:
StandardEnvironment
- Web Reactive:
- 2.3 创建Spring应用上下文:根据准备阶段推断的
WebApplicationType
创建对应的ConfigurableApplicationContext
实例- Web Reactive:
AnnotationConfigReactiveWebServerApplicationContext
- Web Servlet:
AnnotationConfigServletWebServerApplicationContext
- 非 Web:
AnnotationConfigApplicationContext
- Web Reactive:
- 2.4 加载
SpringBootExceptionReporter
故障分析报告(利用 Spring 工厂加载机制),供以后失败时使用 - 2.5 加载Bean到应用上下文:加载Annotation/XML配置的Bean给Spring管理
- 2.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
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
- eg: SpringBoot实现类
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)
;
总结 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
自定义
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; } }
配置
META-INF/spring.factories
# ApplicationContextInitializer org.springframework.context.ApplicationContextInitializer=\ com.cj.application.initializer.HelloApplicationInitializer,\ com.cj.application.initializer.WorldApplicationInitializer
测试
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
自定义
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; } }
- 配置
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
测试
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.
加载外部配置资源的内容到程序变量中用于动态控制:
外部化配置资源(保存到
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}.properties
outside pkg jarapplication-{profile}.properties
packaged inside jarapplication.properties
outside pkg jarapplication.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); });
- 常见的加载项顺序:
绑定到变量
- XML方式:
<bean>
-><property name="xxx" value="${...}" />
- Annotation方式:
@Value
,@ConfigurationProperties
- XML方式:
加载外部化配置(XML方式)
示例:
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(); } }
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
- Key:
Annotation方式
@Value
@ConfigurationProperties
- 扩展:
@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
- prepareEnvironment:
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; }
implements
BeanFactoryAware
@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
不行
可考虑扩展以下类,并依据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
)
示例
示例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 技术(规范)的一部分
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 应用 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 Servlet 组件的生命周期
Servlet
- init(ServletConfig)
- service(ServletRequest,ServletResponse)
- destroy()
Filter
- init(FilterConfig)
- doFilter(ServletRequest,ServletResponse,FilterChain)
- destroy()
Listener
- contextInitialized(ServletContextEvent)
- contextDestroyed(ServletContextEvent)
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 Servlet容器
Servlet容器 启动 初始化器 独立Servlet容器 独立Servlet容器 -> 启动 App Servlet SPI(3.0): ServletContainerInitializer
; Spring 适配:SpringServletContainerInitializer
+@HandlesTypes(WebApplicationInitializer.class)
嵌入式Servlet容器 App -> 启动嵌入式Servlet容器 SpringBoot: ServletContextInitializer
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容器初始化器
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应用就变成了一个即插即用的组件,不用依据应用环境定义一套新的配置 - 参考:
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 注解驱动
- AbstractDispatcherServletInitializer 编程驱动
- AbstractReactiveWebInitializer
- AbstractSecurityWebApplicationInitializer
- SpringBootServletInitializer (jar: spring-boot)
- AbstractContextLoaderInitializer
Spring WebMvc 示例:
Key:
- 使用xml配置WebMvc相关组件
- 使用
web.xml
或者自定义的WebApplicationInitializer
实现类注册DispatcherServlet
- 自定义一个Servlet,并通过
@WebServlet
注册 - 使用独立Servlet启动运行
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>
/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>
- /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
- 使用
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组件( SpringSpringServletContainerInitializer
->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[]{"/"}; } }
- 使用
自定义一个
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); } }
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) :
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 interfaceServletContainerInitializer
)来实现ServletContext
的initialization.
- 为减少风险,嵌入式容器(eg: embed jetty/tomcat/...) 不能使用Servlet interface
ServletContextInitializer
实现类- RegistrationBean
- DynamicRegistrationBean
- AbstractFilterRegistrationBean
- DelegatingFilterProxyRegistrationBean
- FilterRegistrationBean
: @WebFilter
- ServletRegistrationBean
: @WebServlet
- AbstractFilterRegistrationBean
- ServletListenerRegistrationBean
: @WebListener
- DynamicRegistrationBean
- RegistrationBean
@ServletComponentScan
- 嵌入式容器无法自动识别并处理Servlet注解
@WebServlet
/@WebFilter
/@WebListener
- SpringBoot提供
@ServletComponentScan
来扫描识别这些Servlet注解 - 处理过程:扫描 package ->
@WebServlet
/@WebFilter
/@WebListener
->RegistrationBean
BeanDefinition ->RegistrationBean
Bean: 嵌入式容器调用ServletContextInitializer#onStartup(ServletContext)
时装配识别出的Servlet组件
- 嵌入式容器无法自动识别并处理Servlet注解
部分源码:
@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)
总结: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
- 在自定义Servlet上添加
- 方式三:使用
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/
-> OKcurl -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,initServletBeanFrameworkServlet
: final initServletBean -> initWebApplicationContext -> onRefresh; service -> abstract doServiceDispatchServlet
: 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); }
- init:
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+)
技术:
DeferredResult
(spring-web)Callable
(java)CompletionStage
(java)
示例:
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
- visit:
WebMVC
核心组件 (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
支持标注@RequestMapping
的method
4 HandlerAdapter
handle -> ModelAndView(modelMap+viewName) invoke Handler
-> resolveArgument + doInvoke + handleReturnValue ->ModelAndView(modelMap+viewName)
eg: 1. RequestMappingHandlerAdapter
支持标注@RequestMapping
的method
invoke and handleeg: 2. RequestResponseBodyMethodProcessor
(implementsHandlerMethodArgumentResolver
)支持标注@RequestBody
的method
的arguments resolveeg: 3. RequestResponseBodyMethodProcessor
(implementsHandlerMethodReturnValueHandler
)支持标注@ResponseBody
的method
的returnValue handle5 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常用注解 (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
- 控制器:
视图处理
- 视图解析
ViewResolver#resolveViewName
->View
- Order
- return Matched View
- Resource Location
- 视图渲染
View#render
- Context
- TemplateEngine
- 视图解析
内容协商处理
- 解析器
ContentNegotiatingViewResolver
(resolveViewName) - 工厂
ContentNegotiationManagerFactoryBean
(构建ContentNegotiationManager) - 管理器
ContentNegotiationManager
(管理ContentNegotiationStrategy) - 策略
ContentNegotiationStrategy
(resolveMediaTypes) - 配置
ContentNegotiationConfigurer
- 解析器
框架:
Spring WebMvc
- WebMvc总控DispatcherServlet注册:
web.xml
/WebApplicationInitializer
- WebMvc其他组件配置:
.xml
/@Configuration
- 独立Servlet容器启动运行
- WebMvc总控DispatcherServlet注册:
SpringBoot WebMvc
(依赖SpringWebMvc
,只是增加了自动化装配和配置部分)- 自动装配
@EnableAutoConfiguration
:- DispatcherServletAutoConfiguration: 装配WebMvc总控DispatcherServlet
- ServletWebServerFactoryAutoConfiguration: 装配Servlet容器
- WebMvcAutoConfiguration: 装配WebMvc其他组件
- 配置: 自定义配置类装配 / 外部化配置
- 自定义类
@Configuration
(可implementsWebMvcConfigurer
方便添加其他配置)+@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):
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>
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>
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
- 方式一:自定义配置类
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()); } }
- 方式一:直接使用
Main
@SpringBootApplication public class WebMvcViewApp { public static void main(String[] args) { SpringApplication.run(WebMvcViewApp.class,args); } }
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):
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>
Thymeleaf Template: resources/templates/thymeleaf/hello.html
<p th:text="${message}">!!!</p>
Configure: resources/application.properties
# ThymeleafProperties spring.thymeleaf.prefix=classpath:/templates/thymeleaf/ spring.thymeleaf.suffix=.html spring.thymeleaf.cache = false
Controller
@Controller public class HelloController { @GetMapping("/hello") public String hello() { model.addAttribute("message", "Hello World"); return "hello"; } }
Main
@SpringBootApplication public class WebMvcViewApp { public static void main(String[] args) { SpringApplication.run(WebMvcViewApp.class,args); } }
Visit:
http://localhost:8080/hello
Thymeleaf模版处理流程:
官网 | Doc | Quict Start
资源定位(模板来源 ):
- 文件资源: File
- ClassPath资源: ClassLoader
- 统一资源: URL
- Web资源: ServletContext
- Spring 资源: ResourceLoader & Resource
渲染上下文(变量来源 ):
- Spring Web MVC:
Model
- Servlet:
Attribute
- Thyemeaf:
Context
- Spring Web MVC:
模板引擎(模板渲染)
- Thymeleaf模板引擎:
interface ITemplateEngine
- Thymeleaf 原生实现: TemplateEngine
- Spring 实现: SpringTemplateEngine
- Spring WebFlux 实现: SpringWebFluxTemplateEngine
- Thymeleaf模板引擎:
示例:使用 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
- ContentNegotiatingViewResolver
- 注意:
- 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>
- thymeleaf: resources/templates/thymeleaf/hello.html
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
- ContentNegotiatingViewResolver
- Verify:
- Visit: http://localhost:8080/ => Error(500:Error resolving template "index", template might not exist or might not be accessible by any of the configured Template Resolvers)
- Visit: http://localhost:8080/hello => Success
- Ordered ViewResolver List:
- Debug得到:
将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得到:
- Ordered ViewResolver List:
- InternalResourceViewResolver
- BeanNameViewResolver
- ViewResolverComposite
- ThymeleafViewResolver
- Verify:
- Visit: http://localhost:8080/ => Success
- Visit: http://localhost:8080/hello => Error(404)
- Ordered ViewResolver List:
注:
- 由于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
):
- 优先级:最高(order=Ordered.HIGHEST_PRECEDENCE)
- 成员:
- 包含
List<ViewResolver>
列表: 会包含除自己以外的所有装载的ViewResolver
(按顺序) - 包含内容协商管理工厂
ContentNegotiationManagerFactoryBean
: 生成内容协商管理器ContentNegotiationManager
- 包含内容协商管理器
ContentNegotiationManager
: 管理内容协商策略List<ContentNegotiationStrategy>
(用于获取HTTP Request的MediaType
列表)
- 包含
- 方法:
@Override resolveViewName->View
getMediaTypes
: 获取与produce mediaType兼容的HTTP RequestMediaType
列表getCandidateViews
: 获取所有可能的View
(ViewResolver#resolveViewName
视图解析得到的View
)getBestView
: 从上面获取的View中选取最佳匹配的View
- HTTP Request
MediaType
<-> ViewResolver ViewcontentType
: 第一个匹配的那个View - 注:
ViewResolver 顺序
和Request的MediaType
匹配规则(Accept 头策略,请求参数策略,...)
- HTTP Request
- 配置:
- 自定义配置类:
@Configuration
+implements WebMvcConfigurer
+@Override configureContentNegotiation(ContentNegotiationConfigurer configurer)
- 外部化配置:
WebMvcProperties.Contentnegotiation
- 自定义配置类:
示例: 多视图处理器内容协商
上面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; } }
配置:
- 方式一: 外部化配置
# 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); } }
- 方式一: 外部化配置
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
- ContentNegotiatingViewResolver
- Verify:
- http://localhost:8080/?format=xml => Success (InternalResourceViewResolver resolved,show jsp page)
- http://localhost:8080/ Accept:text/xml => Success (match InternalResourceViewResolver resolved, show jsp page)
- http://localhost:8080/hello => Success (ThymeleafViewResolver resolved, show thymeleaf page)
- Ordered ViewResolver List:
REST
REST = RESTful = Representational State Transfer,is one way of providing interoperability between computer systems on the Internet.
架构约束
- 统一接口(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
- 资源识别(Identification of resources)
- C/S架构(Client-Server)
- 无状态(Stateless)
- 可缓存(Cacheable)
- 分层系统(Layered System)
- 按需代码(Code on demand)(可选)
- 统一接口(Uniform interface)
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
核心组件
HandlerMethod
: 被@RequestMapping/@XxxMapping
标注的方法RequestMappingHandlerMapping#getHandler
: 找到mapRequestMappingInfo
的HandlerMethod
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 handleHandlerMethodArgumentResolver#resolveArgument
处理方法参数解析器(解析HTTP请求内容为HandlerMethod
的参数)RequestResponseBodyMethodProcessor
: 支持标注@RequestBody
的method
的arguments resolveHttpMessageConverter#read
: HTTP消息转换器,read: 反序列化HttpRequest
HandlerMethodReturnValueHandler#handleReturnValue
处理方法返回值解析器(解析HandlerMethod
返回值为HTTP响应内容)RequestResponseBodyMethodProcessor
: 支持标注@ResponseBody
的method
的returnValue handleContentNegotiationManager#resolveMediaTypes
: 内容协商管理器,解析请求的媒体类型,返回合法的MediaTypesHttpMessageConverter#write
: HTTP消息转换器,write: 序列化HttpResponse- 注:ModelAndViewContainer#setRequestHandled:true -> ModelAndView null -> no viewResolver#resolveViewName and View#render call !
MediaType 媒体类型
- 请求的媒体类型:
ContentNegotiationManager#resolveMediaTypes
- 解析出HTTP Request中的媒体类型 (eg:
text/html
,application/json
) - 使用
ContentNegotiationStrategy#resolveMediaTypes
,有各种解析策略,例如:- 固定 MediaType : FixedContentNegotiationStrategy
- "Accept" 请求头: HeaderContentNegotiationStrategy
- 请求参数: ParameterContentNegotiationStrategy
- 路径扩展名: PathExtensionContentNegotiationStrategy
- 解析成功,返回合法 MediaType 列表
- 解析失败,返回单元素
MediaType.ALL
(*/*
) 媒体类型列表
- 解析出HTTP Request中的媒体类型 (eg:
- 可消费的媒体类型:
@RequestMapping#consumes
- 请求头
Content-Type
的媒体类型 - 若请求头中的
Content-Type
和consumes中配置的不兼容,则该HandlerMethod不会被匹配到执行
- 请求头
- 可生成的媒体类型:
@RequestMapping#produces
:- 响应头
Content-Type
的媒体类型 - 若配置了,则使用配置的MediaType列表
- 若未配置,则使用已注册的 HttpMessageConverter列表支持的MediaType列表
- 可用来匹配支持的
HttpMessageConverter
以序列化生成响应内容和生成响应头的Content-Type
内容- 找到与请求的媒体类型兼容的MediaType列表,使用第一个匹配支持的HttpMessageConverter进行序列化,生成响应内容;
- 若未找到匹配的,则抛出415 HttpMediaTypeNotAcceptableException;
- 响应头
- 请求的媒体类型:
示例:REST
- pom.xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
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; } }
- main
@SpringBootApplication public class WebMvcRestApp { public static void main( String[] args ){ SpringApplication.run(WebMvcRestApp.class, args); } }
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-Type
为 text/properties
媒体类型的请求与响应,中间转换使用Properties
对象
实现:
- 方式一:依赖REST的
@RequestBody
和@ResponseBody
(会使用RequestResponseBodyMethodProcessor
->HttpMessageConverter#read/write
)- 自定义
HttpMessageConverter
实现类来supporttext/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
方法
- 自定义
示例(方式一):
自定义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; } }
- Configure:
WebMvcConfigurer#extendMessageConverters
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(new PropertiesHttpMessageConverter()); } }
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; } }
- main
@SpringBootApplication public class WebMvcRestApp { public static void main( String[] args ){ SpringApplication.run(WebMvcRestApp.class, args); } }
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
示例:
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>
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; } }
Configure
@Configuration public class WebMvcConfig implements WebMvcConfigurer { public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**").allowedOrigins("*"); } }
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>
Visit http://localhost:8080/
WebMVC 源码分析
SpringBoot自动装配
Key:
- 配置类:
- DispatcherServlet :
DispatcherServletAutoConfiguration
- WebMvc :
WebMvcAutoConfiguration
(替换@EnableWebMvc
) - Servlet容器 :
ServletWebServerFactoryAutoConfiguration
- DispatcherServlet :
- 装载顺序:
- 绝对顺序:
@AutoConfigureOrder
- 相对顺序:
@AutoConfigureAfter
- 绝对顺序:
- 装配条件
- Web 类型判断
@ConditionalOnWebApplication(type = Type.SERVLET)
- API 判断
@ConditionalOnClass
- Bean 判断
@ConditionalOnMissingBean
,@ConditionalOnBean
- Web 类型判断
- 外部化配置
- Web MVC 配置:
WebMvcProperties
- 资源配置:
ResourceProperties
- Web MVC 配置:
部分源码:
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,\ #...
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 { //... } }
注:
- 使用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 { //... }
- 使用springboot自动装配,可不再使用
装载ThymeleafViewResolver
加入spring-boot-starter-thymeleaf
依赖包后,SpringBoot会自动装载ThymeleafViewResolver
部分源码:
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
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 { // ... } //... }
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 } }
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)
-> ListContentNegotiationManager#resolveFileExtensions(mediaType)
-> List
- Order:
- Override 方法:
resolveViewName
getMediaTypes
: 获取与produce兼容的HTTP RequestMediaType
(使用ContentNegotiationManager
的ContentNegotiationStrategy
获取HTTP RequestMediaType
)getCandidateViews
: 获取所有可能的View (ViewResolver视图解析得到View)getBestView
: 从上面获取的View中选取最佳匹配的View- HTTP Request
MediaType
<-> ViewResolver ViewcontentType
: 第一个匹配的那个View - 注:
ViewResolver 顺序
和Request的MediaType
(Accept 头策略,请求参数策略,...)
- HTTP Request
部分源码:
Key:
Member vars:
order
: Ordered.HIGHEST_PRECEDENCEContentNegotiationManager
ContentNegotiationManagerFactoryBean
List<ViewResolver>
viewResolversList<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)
- foreach this.viewResolvers
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
- foreach candidateView:candidateViews
- return
bestView
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; } //... }
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; } }
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; }
- resolveMediaTypes:
DispatcherServlet
package org.springframework.web.servlet;
DispatcherServlet#doDispatch(request,response)
- HandlerExecutionChain { handler(eg:HandlerMethod) & interceptorList }
- List
handlerMappings - HandlerMapping#getHandler!=null
- List
- HandlerAdapter
- List
handlerAdapters - HandlerAdapter#support
- List
- ModelAndView { model:ModelMap,view(eg:viewName) }
- HandlerAdapter#handle
- HandlerAdapter#invokeHandlerMethod
- View
- List
viewResolvers - ViewResolver#resolveViewName(ModelAndView#view,local)
- List
- View#render(ModelAndView#model)
- HandlerExecutionChain { handler(eg:HandlerMethod) & interceptorList }
HandlerMapping#getHandler -> HandlerExecutionChain
/* HandlerMapping - AbstractHandlerMapping + AbstractHandlerMethodMapping<T> - RequestMappingInfoHandlerMapping<RequestMappingInfo> * RequestMappingHandlerMapping + AbstractUrlHandlerMapping - AbstractDetectingUrlHandlerMapping * BeanNameUrlHandlerMapping - SimpleUrlHandlerMapping + EmptyHandlerMapping */
- AbstractHandlerMapping
- getHandler
- abstract getHandlerInternal
- getHandler
- AbstractHandlerMethodMapping
- afterPropertiesSet -> initHandlerMethods -> detectHandlerMethods
- abstract getMappingForMethod
- registerHandlerMethod
- override getHandlerInternal -> lookupHandlerMethod
- abstract getMatchingMapping
- protected handleMatch
- protected handleNoMatch
- afterPropertiesSet -> initHandlerMethods -> detectHandlerMethods
- RequestMappingInfoHandlerMapping
- override getMatchingMapping
- override handleMatch
- override handleNoMatch
- RequestMappingHandlerMapping
- override getMappingForMethod -> createRequestMappingInfo
- AbstractHandlerMapping
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
- support: expects the handler to be an
- 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
- resolve method args for
- AbstractHandlerMethodAdapter
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)
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
部分源码:
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{...} } }
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();
- 内部相当于维护了一个
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);
- 编程模型:
- 注解式驱动 Annotated Controllers: 同WebMvc使用的注解
- 函数式端点 Functional Endpoints : 使用
RouterFunction
- 核心组件(功能大同WebMvc, jar:
spring-webflux
, package:o.s.w.reactive...
):- 总控
DispatcherHandler
(implementsWebHandler
) - 处理器管理
- 映射
HandlerMapping
(eg: RequestMappingHandlerMapping) - 适配
HandlerAdapter
(eg: RequestMappingHandlerAdapter) - 执行
HandlerResult
- 映射
- 异常处理
HandlerResult#exceptionHandler
- HandlerMethod:
@RequestMapping
/@XxxMapping
标注的方法- HandlerMethod参数解析器:
HandlerMethodArgumentResolver
- Handler返回值解析器:
HandlerResultHandler
ResponseBodyResultHandler
ResponseEntityResultHandler
ServerResponseResultHandler
ViewResolutionResultHandler
RequestedContentTypeResolverBuilder
RequestedContentTypeResolver
- HandlerMethod参数解析器:
- 视图解析器
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模式 : 非阻塞异步
- 阻塞 & 同步/异步,eg:
示例:数据加载(阻塞任务) + 同步/异步
- 在各个任务间没有数据依赖关系的情况下:
- 单线程 -> 串行:时间消耗线性累加
- 多线程 -> 并行:时间消耗取最大者,性能和资源利用率明显地得到提升
- 在任务间有依赖时:
- 依赖任务不论是否在同一线程,都会串行执行(又回到了阻塞模式)
- 与非依赖的异步任务,还是并行执行
- 总结:如果任务间有依赖,或则设置强制等待某个任务结束,并发模型并不会提高执行效率
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
Reactive 思想
- 非阻塞下同步/异步执行 -- 多功能
- 阻塞导致性能瓶颈和浪费资源,但非阻塞也不一定提升性能
- 增加线程可能会引起资源竞争和并发问题
- 数据在生产与消费间的平衡(背压)
- 非阻塞下同步/异步执行 -- 多功能
Reactive Programming (编程模型)
- 观察者模式(observer pattern)的延伸
- 处理流式数据(streams: sequenced data/events)
- 非阻塞下的同步/异步执行 (no-blocking)
- 推拉相结合(push-based and pull-based)
- 响应数据传播时的变化 (responsive)
- 结合背压(Backpressure)技术处理数据生产与消费的平衡问题
Observable
vsIterable
: 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
- Spring (同步/异步)事件/监听器:
Reactive 实现框架
- RxJava (Reactive Extensions)
- Reactor (Spring/SpringBoot WebFlux使用此Reactive类库)
- Flow API (Java 9)
- 注:Java 原生技术Stream的限制Stream属于迭代器模式,拉模式)
- 有限操作符
- 不支持背压
- 不支持订阅
场景
- 管理流式数据在异步边界的交换(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)
- 管理流式数据在异步边界的交换(reactive-streams)
Reactor
- jar:
reactor-core
(dependency jar:reactive-streams
) - 核心API:
Mono
: 0~1的非阻塞结果(类似Optional
,点对点模式),实现 reactive-streamsPublisher
Flux
: 0~N的非阻塞序列(类似Stream
,发布/订阅者模式),实现 reactive-streamsPublisher
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(); // 请求停止发送数据并且清除资源 }
Processor
:Publisher
和Subscriber
综合体(中游,承上启下)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
- pom.xml
<dependency> <groupId>io.projectreactor</groupId> <artifactId>reactor-core</artifactId> </dependency>
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 */ }
- 传统编程方式
@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 */ }
- pom.xml
WebFlux
示例:
注解驱动,函数式端点驱动
@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); } }
非阻塞调用阻塞
@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 * */ }