SpringBoot3 – 基础配置原理查询
自动配置原理
流程
- 1.导入starter(以starter-json为例):导入了开发场景
- 1.场景启动器导入了相关场景的所有依赖:starter-json、starter-tomcat、springmvc
- 2.每个场景启动器都引入了一个spring-boot-starter,核心场景启动器
- 3.核心场景启动器引入了spring-boot-autoconfigure包
- 4.spring-boot-autoconfigure 里面囊括了所有场景的所有配置
- 5.只要这个包下的所有类都能生交,那么相当于SpringBoot官方写好的整合功能就生效了
- 6.SpringBoot默认却扫描不到 spring-boot-autoconfigure 下写好的所有配置类(这些配置类给我们做了整合操作)
- 2.主程序: @SpringBootApplication
- 1.@SpringBootApplication 由三个注解组成 @SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan
- 2.SpringBoot默认只能扫描自己主程序所在的包及其子包,扫描不到spring-boot-autoconfigure 里面官方写好的配置类
- 3.@EnableAutoConfiguration:SpringBoot 开启自动配置的核心
- @EnableAutoConfiguration 中包含了一个注册 @Import(AutoConfigurationImportSelector.class) 提供功能:批量给容器导入组件
- SpringBoot启动会默认加载142个配置类
- 这142个配置类来自于 spring-boot-autoconfigure 包下的“META-INF/spring/org.springframeword.boot.autoconfigure.AutoConfiguration.imports”文件里的xxxAutoConfiguration 类导入进来(自动配置类)
- 虽然导入了142个自动配置类,但是不一定都会生效
- 4.按需生效:
- 并不是这142个自动配置类都能生效
- 每一个自动配置类,都有条件注解“@conditionalOnXXX”,只有条件成立,才能生效
- 3.XxxAutoConfiguration 自动配置类的作用
- 1.给容器中使用 @Bean 放一堆组件
- 2.每个自动配置类中都可通有 @EnableConfigurationProperties(prefix="xxx") 注册,用于记录配置文件中的配置值封装到 xxxProperties 属性类中
- 3.把服务器的所有配置都以“prefix的xxx”开头。配置都封装到了属性类中。
- 4.给容器中放的所有组件的一些核心参数,都来自于 xxxProperties.
- 只需要改配置文件的值,核心组件的底层参数都能修改
YAML文件
yaml 语法
- 1.使用 - 来定义数组:
-
person: likes: - a - b - c
- 2.也可以使用 [] 来定义数组
-
person: likes: ["a", "b", "c"]
- 3.使用 {} 来定义 map 数据
-
person: likes: a: {name: "a", age: 18} b: {name: "b", age: 20}
- 4.也可以使用换行定义map数据
-
person: likes: a: name: "a" age: 18 b: name: "b" age: 20
- 5.使用 | 来定义大文本,会保留换行符格式
-
person: likes: | 123 456 678
- 6.使用 > 来定义大文本,会把换行符压缩成空格
-
person: likes: > 123 456 678
- 7.单引号引着的文本,不会使 \n 做转义
-
person: likes: '123 \n 456' # 不会转义 \n
- 8.使用双引号可以使 \n 做转义
-
person: likes: "123 \n 456" # \n 将转为换行符
- 9.使用 --- 来定义区分不同的yaml文档
-
person: likes: "123 \n 456" # \n 将转为换行符 # 使用 --- 来区别在同一个文件中定义两个文档的内容 --- person: likes:
- 10.传入日期时间类的数据时使用以下格式:
-
person: brithday: 2024/01/01 12:12:12
日志配置
- Spring使用commons-logging作为内部日志,但底层日志实现是开放的。可对接其他日志框架。
- spring5 以后 common-logging 被spring直接自己写了
- 支持jul,log4j2,logback。SpringBoot提供了默认的控制台输出配置,也可以配置输出为文件。
- logback是默认使用的。
- 虽然日志框架很多,但是我们不用担心,使用SpringBoot的默认配置就能工作的很好。
日志格式
默认输出格式:
- 时间和日期:毫秒级精度
- 日志级别:ERROR、WARN、INFO、DEBUG、TRACE
- 进程 ID
- ---: 消息分割符
- 线程名:使用 [] 包含
- Logger 名:通常是产生日志的类名
- 消息:日志记录的内容
注意:logback没有FATAL级别,对应的是ERROR
默认值:参照:spring-boot包additional-spring-configuration-metadata.json文件
默认输出格式值:%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd'T'HH:mm:ss.SSSXXX}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}
手动输出日志
我们可以在我们写的类中输出对应的日志,可以通过以下方式取得一个日志工厂,生成日志对象,再进行输出日志
Logger logger = LoggerFactory.getLogger(getClass());
logger.info("")
同时SpringBoot中已对日志做了注解操作,我们只需要引入注解 @Slf4j 后,就能使用日志对象 log
@Slf4j
log.info("")
日志级别
- 由低到高 ALL,TRACE, DEBUG, INFO, WARN, ERROR,FATAL,OFF
-
- 只会打印指定级别及以上级别的日志
- ALL:打印所有日志
- TRACE:追踪框架详细流程日志,一般不使用
- DEBUG:开发调试细节日志
- INFO:关键、感兴趣信息日志
- WARN:警告但不是错误的信息日志,比如:版本过时
- ERROR:业务错误日志,比如出现各种异常
- FATAL:致命错误日志,比如jvm系统崩溃
- OFF:关闭所有日志记录
-
- 不指定级别的所有类,都使用root指定的级别作为默认级别
- SpringBoot日志默认级别是 INFO
可以使用配置项对SpringBoot配置日志级别,在不配置情况下,Boot 默认所有日志使用 root 的默认级别,root 范围是指整个程序包都使用该日志级别
logging.level.root = debug
也可以对程序的范围做不同的日志级别:
logging.level.cn.unsoft.controller = debug
日志设置还可以把多个包设为一个组:
logging.level.group.xxx = cn.unsoft.xx1, cn.unsoft.xx2
logging.level.xxx = info
log还可以通过占位符输出参数
log.info("参数1: {}, 参数2: {}", prop1, prop2)
指定日志文件的保存路径
logging.file.path = xxx
指定日志文件的文件名
logging.file.name = xxx
注意: 如果不写的话,默认只输出到控制台,不写文件。
日志文件输出
我们可以使用以下配置项来配置日志文件:
logging.file.name = logfile.log
但是对于日志来说,每天增加的日志,会使得日志文件变得非常大,我们可以使用日志文件大小限制来区分文件大小
- logging.logback.rollingpolicy.file-name-pattern : 日志存档的文件名格式(默认做: ${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz)
- logging.logback.rollingpolicy.clean-history-on-start: 应用启动时是否清除以前的存档(默认 false)
- logging.logback.rollingpolicy.max-file-size: 存档前,每个日志文件的最大大小(默认 10MB)
- logging.logback.rollingpolicy.total-size-cap: 日志文件被删除之前,可以容纳的最大大小(默认做0B).设置1GB则磁盘存储超过1GB日志后就会删除旧日志文件
- logging.logback.rollingpolicy.max-history: 日志文件保存的最大天数(默认7)
自定义配置日志系统
如果我们希望使用自定义的logback配置,可以在resource文件夹中加入文件 logback.xml 并在其中进行配置即可生效
Spring 对日志文件的命名有约定
- Logback:
- 可以使用 logback-spring.xml ,logback-spring.groovy 该文件可以直接让spring完全控制日志初始化
- 可以使用 logback.xml 和 logback.groovy 该文件属于游离状态,不能被spring完全控制日志初始化
- Log4j2:
- log4j2-spring.xml 或者 log4j2.xml 文件
- JDK(Java Util Logging):
- logging.properties
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern><![CDATA[%n[%d{yyyy-MM-dd HH:mm:ss.SSS}] [level: %p] [Thread: %t] [ Class:%c >> Method: %M:%L ]%n%p:%m%n]]></pattern>
</encoder>
</appender>
<appender name="LOG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern><![CDATA[%n[%d{yyyy-MM-dd HH:mm:ss.SSS}] [level: %p] [Thread: %t] [ Class:%c >> Method: %M:%L ]%n%p:%m%n]]></pattern>
</encoder>
<file>logs/sports-web.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/sports-web.-%d{yyyyMMdd}.%i.log</fileNamePattern>
<!-- 每天一个日志文件,当天的日志文件超过10MB时,生成新的日志文件,当天的日志文件数量超过totalSizeCap/maxFileSize,日志文件就会被回滚覆盖。 -->
<maxFileSize>10MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
</appender>
<logger name="com.sports" level="DEBUG" additivity="false">
<appender-ref ref="STDOUT"/>
<appender-ref ref="LOG_FILE"/>
<!--<appender-ref ref="myAppender"/>-->
</logger>
<root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="LOG_FILE"/>
<!--<appender-ref ref="mqAppender"/>-->
</root>
</configuration>
在默认情况下,SpringBoot使用的是 logging
但如果我们希望使用 Log4j2,我们可以排除Spring引入的Logging,然后我们自己去引入Log4j2
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
Web 开发能力
WebMvcConfigurer 接口
自动配置
- 1.SpringMVC 的所有配置 spring.mvc
- 2.Web场景通用配置 spring.web
- 3.文件上传配置 spring.servlet.multipart
- 4.服务器的配置 server
默认效果
- 1.包含了 ContentNegotiationViewResolver 和 BeanNameViewResolver 组件,方便视图解析
- 2.默认的静态资源处理机制,静态资源放在 static 文件夹下即可直接访问
- 3.自动注册了 Converter, GenericConverter, Formatter 组件,适配常见数据类型转换和格式化需求
- 4.支持 HttpMessageConverters, 可以方便返回 json 等数据类型
- 5.注册 MessageCocdesResolve, 方便国际化及错误消息处理
- 6.支持 静态 index.html
- 7.自动使用 ConfigurableWebBindingInitializer, 实现消息处理、数据绑定、类型转化等功能
注意:
- 1.如果想保持boot mvc的默认配置,并且自定义更多的mvc配置,如:interceptors, formatters,view controllers等。可以使用@Configuration注解添加一个WebMvcConfigurer类型的配置类,并不要标注 @EnableWebMvc
- 2.如果想保持boot mvc的默认配置,但要自定义核心组件实例,比如:
- RequestMappingHandlerMapping
- RequestMappingHandlerAdapter
- ExceptionHandlerExceptionResolver
- 给容器中放一个WebMvcRegistrations组件即可
- 3.如果想全面接管Spring MVC,@Configuration标注一个配置类,并加上@EnableWebMvc注解,实现WebMvcConfigurer 接口
方式 |
用法 |
效果 |
|
全自动 |
直接编写控制器逻辑 |
全部使用自动配置默认效果 |
|
手自一体 |
|
不要标注 |
保留自动配置效果 |
全手动 |
|
标注 |
禁用自动配置效果 |
给容器中写一个配置类@Configuration
实现 WebMvcConfigurer
但是不要标注 @EnableWebMvc
注解,实现手自一体的效果。
静态资源
生效条件:
@AutoConfiguration(after = { DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class }) //在这些自动配置之后
@ConditionalOnWebApplication(type = Type.SERVLET) //如果是web应用就生效,类型SERVLET、REACTIVE 响应式web
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) //容器中没有这个Bean,才生效。默认就是没有
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)//优先级
@ImportRuntimeHints(WebResourcesRuntimeHints.class)
public class WebMvcAutoConfiguration {
}
其中我们讲这个接口中的处理静态资源规则:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
//1、
addResourceHandler(registry, this.mvcProperties.getWebjarsPathPattern(),
"classpath:/META-INF/resources/webjars/");
addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations());
if (this.servletContext != null) {
ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION);
registration.addResourceLocations(resource);
}
});
}
addResourceHandler 的两次增加
- 第一次增加是增加请求地址
/webjars/**
时,会查找资源目录下的classpath:/META-INF/resources/webjars/
,其作用是用在webjars上,webjars就是一个提供打包好的静态资源的各种包,详情:https://www.webjars.org/ - 第二次增加的是增加用户任意请求
/**
的静态资源时寻找的地址,Spring给我们预留了4个目录,这4个目录分别如下:- classpath:/META-INF/resources/
- classpath:/resources/
- classpath:/static/
- classpath:/public/
静态资源加载:
private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, Consumer<ResourceHandlerRegistration> customizer) {
if (!registry.hasMappingForPattern(pattern)) {
ResourceHandlerRegistration registration = registry.addResourceHandler(new String[]{pattern});
customizer.accept(registration);
registration.setCachePeriod(this.getSeconds(this.resourceProperties.getCache().getPeriod()));
registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl());
registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());
this.customizeResourceHandlerRegistration(registration);
}
}
setCachePeriod 是指设置静态资源缓存机制周期; 多久不用找服务器要新的。 默认没有,以s为单位
setCacheControl 是指HTTP缓存控制;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Caching
setUseLastModified 是指浏览器向服务器询问资源最后修改时间,在缓存中,浏览器不知道该服务器什么时候会改变资源,所以服务器会给浏览器一个资源的最后修改时间,浏览器在请求之前会向服务器获取该修改时间,如果修改时间并没有改变,说明服务器的资源也没有改变,这时浏览器就直接使用缓存数据而不再向服务器请求获取资源了。
欢迎页
在Boot 中,提供了一个地址映射,会对用户任意请求中找到那4个静态资源目录中寻找“index.html” 文件,作为首页的访问;
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, this.getWelcomePage(), this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(this.getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(this.getCorsConfigurations());
return welcomePageHandlerMapping;
}
favicon 图标
在Boot 中,也可以在那4个静态资源目录中放入favicon图标以使浏览器出现favicon图标功能。
路径匹配
Spring 5.3 之后加入了更多的请求路径匹配的实现策略
以前只支持 AntPathMatcher 策略,现在提供了 PathPatternParser 策略。并且可以让我们指定到底使用那种策略
Ant风格路径用法
Ant 风格的路径模式语法有以下规则:
- * 表示任意数量的字符
- ? 表示任意一个字符
- ** 表示任意数量的目录
- {} 表示一个命名的模式占位符,就是接收外部输入的值传入变量
- [] 表示字符集合,例如 [a-z] 表示小写字母
举例:
- *html 匹配任意处称,扩展名为 .html 的文件
- /folder1/*/*.java 匹配在folder1目录下的任意两级目录下的.java文件
- /folder1/**/*.jsp 匹配在folder2目录下的任意深度目录下的.jsp文件
- /{type}/{id}.html 匹配任意文件名为 {id}.html, 在任意命名的{type}目录下的文件
注意:Ant风格的路径模式语法中的特殊字符需要转义,如:
- 要匹配文件路径中的星号,则需要转义为 \\*
- 要匹配文件路径中的问号,则需要转义为 \\?
PathPatternParser 风格路径用法
PathPatternParser 是Spring新版的路径匹配方案,在新版的SpringBoot下,默认使用了新版的 PathPatternParser
- PathPatternParser 在jmh基准测试下,有6~8倍吞吐量的提升,降低30%~40%空间分配率
- PathPatternParser 兼容 AntPathMatcher 语法,并支持更多类型的路径模式
- PathPatternParser 中的“/**/”多段匹配仅允许在URI的末尾使用
举个例子:
@GetMapping("/a*/b?/{p:[a-f]+}/**")
public Result test(){
return null;
}
在上面的匹配模式中,我们可以看出以下几个点:
- a* 该段可以以a打头,后面任意字符
- b? 该段可以以b打头,后面跟上1个字符
- {p:[a-f]+} 该段将记录用户输入的参数传入到p变量中,但限制用户只能输入a~f之间的字符,字符数至少一个
- ** 该段则是表示打后的任意目录段和字符
注意:在 AntPathMatcher 匹配环境下,/**/ 是可以配置在URI中间的,如下表示:
@GetMapping("/a*/b?/**/{p:[a-f]+}/**")
public Result test(){
return null;
}
但在新版的 PathPatternParser 下,/**/ 不再支持,仅能使用在URI的末尾部分,若在 PathPatternParser 版本下匹配上面的URI,会在启动SpringBoot时就会报错即出。
要使用旧版的 AntPathMatcher 匹配模式,可以更改下面配置项:
spring.mvc.pathmatch.matching-strategy = ant_path_matcher
内容协商
当我们有多个平台,每个平台希望服务器返回属于自己的协议的数据,这时可以请求时提交Accept:类型的方式向服务器提交期望数据格式
多端内容适配
SpringBoot 多端内容适配
- 客户端向服务端发送请求,携带HTTP标准的Accept请求头,默认开启。
- Accept: application/json text/xml text/yaml
- 服务端根据客户端请求头期望的数据类型进行动态返回
- 基于请求参数内容协商(需要开启),如果开启,默认提交的字段名:format
- 发送请求 GET /projects/spring?format=json
- 匹配到 @GetMapping("/projects/spring")
- 根据参数协商,优选返兕json类型的数据【需要开启参数匹配设置】
- 发送请求 GET /projects/spring?format=xml 优先返回 xml 类型数据
开启内容协商
如果我们希望可以通过提交数据格式要求让服务器即时改变返回的数据格式,我们就需要先开启内容协商,同时我们还需要对数据转换所需的依赖引导进来
1.在SpringBoot中默认引入了json数据转换依赖,但并没有引入 xml 数据转换依赖,如果我们希望服务器能使用数据转换为xml后再输出的话,我们可以引入下面依赖包
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
2.引入xml数据转换依赖后,我们需要对model实体类做数据转换,标记其实稽类是可以被转为xml格式的
@JacksonXmlRootElement // 可以写出为xml文档
@Data
public class Person {
private Long id;
private String userName;
private String email;
private Integer age;
}
到这一步,基本可以通过 Accept 定义输出格式来让服务器输出对应格式的数据了
3.开启内容协商
开启内容协商,是指我们不使用 Accept 的方式,而是在 URI 上直接提交想要的数据格式,如 /projects?format=xml
我们需要在配置中开启内容协商
# 开启基于请求参数的内容协商功能。 默认参数名:format。 默认此功能不开启
spring.mvc.contentnegotiation.favor-parameter=true
# 指定内容协商时使用的参数名。默认是 format,修改后就变成 type=xml
spring.mvc.contentnegotiation.parameter-name=type
数据格式转换
HttpMessageConverter 原理(了解)
@ResponseBody 其实是使用 HttpMessageConverter 来做数据转换的,所以我们可以通过定制HttpMessageConverter来实现多端内容协商
- 1. 如果controller方法的返回值标注了 @ResponseBody 注解
- 1.1. 请求进来先来到DispatcherServlet的doDispatch()进行处理
- 1.2. 找到一个 HandlerAdapter 适配器。利用适配器执行目标方法
- 1.3. RequestMappingHandlerAdapter来执行,调用invokeHandlerMethod()来执行目标方法
- 1.4. 目标方法执行之前,准备好两个东西
- 1.4.1. HandlerMethodArgumentResolver:参数解析器,确定目标方法每个参数值
- 1.4.2. HandlerMethodReturnValueHandler:返回值处理器,确定目标方法的返回值改怎么处理
- 1.5. RequestMappingHandlerAdapter 里面的invokeAndHandle()真正执行目标方法
- 1.6. 目标方法执行完成,会返回返回值对象
- 1.7. 找到一个合适的返回值处理器 HandlerMethodReturnValueHandler
- 1.8. 最终找到 RequestResponseBodyMethodProcessor能处理 标注了 @ResponseBody注解的方法
- 1.9. RequestResponseBodyMethodProcessor 调用writeWithMessageConverters ,利用MessageConverter把返回值写出去
- 2. HttpMessageConverter 会先进行内容协商
- 2.1. 遍历所有的MessageConverter看谁支持这种内容类型的数据
- 2.2. 默认MessageConverter有以下
- 2.4. 最终因为要json所以MappingJackson2HttpMessageConverter支持写出json
- 2.5. jackson用ObjectMapper把对象写出去
上面解释:@ResponseBody
由HttpMessageConverter
处理
Spring 默认的 HttpMessageConverter
Spring 中默认包含了8个消息转换器
- EnableWebMvcConfiguration通过 addDefaultHttpMessageConverters添加了默认的MessageConverter;如下:
- ByteArrayHttpMessageConverter: 支持字节数据读写
- StringHttpMessageConverter: 支持字符串读写
- ResourceHttpMessageConverter:支持资源读写
- ResourceRegionHttpMessageConverter: 支持分区资源写出
- AllEncompassingFormHttpMessageConverter:支持表单xml/json读写
- MappingJackson2HttpMessageConverter: 支持请求响应体Json读写
系统提供默认的MessageConverter 功能有限,仅用于json或者普通返回数据。额外增加新的内容协商功能,必须增加新的HttpMessageConverter
自定义HttpMessageConverter
假如我们现在需要在SpringBoot 中增加输出为 Yaml 格式的数据,这时我们就可以通过自定义增加一个种用于转换 HttpMessageConverter 消息转换器,当我们浏览器的Accept中要求传递yaml格式,或在内容协商中指定yaml类型时,Spring 就会自动通过识别输出数据类型要求来寻找对应的 HttpMessageConverter 来处理。
第一步:导入用于转换为 Yaml 格式的依赖(本次使用了 jackson 的Yaml 转换器)
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
第二步:数据类型在Spring中是以一个Map方式存储的,所以我们需要在配置中增加属于我们自定义的类型名和识别名称(类型名yaml,识别名 application/yaml)
打后我们就可以在内容协商中指定输出内容 /projects?format=yaml
又或者在Accept 中指定 Accept: application/yaml
#新增一种媒体类型
spring.mvc.contentnegotiation.media-types.yaml=application/yaml
第三步:通过实现 HttpMessageConverter 的接口,来定义其数据输入和输出
// AbstractHttpMessageConverter 是 HttpMessageConverter 的抽象类,我们同样可以通过继承 HttpMessageConverter 来实现
public class MyYamlHttpMessageConverter extends AbstractHttpMessageConverter<Object> {
// 在这里定义用于转换YAML数据的 objectMapper 对象,由jackson 提供的转换对象
private ObjectMapper objectMapper = null;
public MyYamlHttpMessageConverter(){
// 告诉SpringBoot这个MessageConverter支持哪种媒体类型 //媒体类型
// 我们在第二步定义了 数据类型,但需要让数据类型与我们的转换器做一个绑定关系,让Spring知道,当使用 yaml 时就是用我们的转换器
super(new MediaType("text", "yaml", Charset.forName("UTF-8")));
YAMLFactory factory = new YAMLFactory()
.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER); // 关闭 yaml 的特性 --- 的文档分隔符
this.objectMapper = new ObjectMapper(factory);
}
@Override
protected boolean supports(Class<?> clazz) {
// 只要是对象类型,不是基本类型
return true;
}
@Override // @RequestBody 时会使用的,即当浏览器传递数据过来时,如何把数据转化为对象
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
@Override //@ResponseBody 即把对象如何转换为指定格式时的方法。
protected void writeInternal(Object methodReturnValue, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
//try-with写法,自动关流
try(OutputStream os = outputMessage.getBody()){
this.objectMapper.writeValue(os,methodReturnValue);
}
}
}
第四步:增加HttpMessageConverter 处理器到IoC容器中
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override // 配置一个能把对象转为yaml的messageConverter
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new MyYamlHttpMessageConverter());
}
};
}
模板引擎
由于 SpringBoot 使用了嵌入式 Servlet 容器。所以 JSP 默认是不能使用的。如果需要服务端页面渲染,优先考虑使用 模板引擎。
模板引擎页面默认放在 src/main/resources/templates
Thymeleaf
Thymeleaf官网:https://www.thymeleaf.org/
整合 Thymeleaf
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
自动配置原理
- 开启了 org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration 自动配置
- 属性绑定在 ThymeleafProperties 中,对应配置文件 spring.thymeleaf 内容
- 所有的模板页面默认在 classpath:/templates文件夹下
- 默认效果
- 所有的模板页面在 classpath:/templates/下面找
- 找后缀名为
.html
的页面 -
核心用法
th:xxx:动态渲染指定的 html 标签属性值、或者th指令(遍历、判断等)
- th:text:标签体内文本值渲染
- th:utext:不会转义,显示为html原本的样子。
- th:属性:标签指定属性渲染
- th:attr:标签任意属性渲染
- th:ifth:each...:其他th指令
示例:
<p th:text="${content}">原内容</p>
<a th:href="${url}">登录</a>
<img src="../../images/gtvglogo.png"
th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
表达式:用来动态取值
- ${}:变量取值;使用model共享给页面的值都直接用${}
- @{}:url路径;
- #{}:国际化消息
- ~{}:片段引用
- *{}:变量选择:需要配合th:object绑定对象
系统工具&内置对象:详细文档
- param:请求参数对象
- session:session对象
- application:application对象
- #execInfo:模板执行信息
- #messages:国际化消息
- #uris:uri/url工具
- #conversions:类型转换工具
- #dates:日期工具,是java.util.Date对象的工具类
- #calendars:类似#dates,只不过是java.util.Calendar对象的工具类
- #temporals: JDK8+ java.time API 工具类
- #numbers:数字操作工具
- #strings:字符串操作
- #objects:对象操作
- #bools:bool操作
- #arrays:array工具
- #lists:list工具
- #sets:set工具
- #maps:map工具
- #aggregates:集合聚合工具(sum、avg)
- #ids:id生成工具
语法示例
表达式:
- 变量取值:${...}
- url 取值:@{...}
- 国际化消息:#{...}
- 变量选择:*{...}
- 片段引用: ~{...}
常见:
- 文本: 'one text','another one!',...
- 数字: 0,34,3.0,12.3,...
- 布尔:true、false
- null: null
- 变量名: one,sometext,main...
文本操作:
- 拼串: +
- 文本替换:| The name is ${name} |
布尔操作:
- 二进制运算: and,or
- 取反:!,not
比较运算:
- 比较:>,<,<=,>=(gt,lt,ge,le)
- 等值运算:==,!=(eq,ne)
条件运算:
- if-then: (if)?(then)
- if-then-else: (if)?(then):(else)
- default: (value)?:(defaultValue)
特殊语法:
- 无操作:_
所有以上都可以嵌套组合
'User is of type ' + (${user.isAdmin()} ? 'Administrator' : (${user.type} ?: 'Unknown'))
属性设置
- th:href="@{/product/list}"
- th:attr="class=${active}"
- th:attr="src=@{/images/gtvglogo.png},title=${logo},alt=#{logo}"
- th:checked="${user.active}"
<p th:text="${content}">原内容</p>
<a th:href="${url}">登录</a>
<img src="../../images/gtvglogo.png"
th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
遍历
语法: th:each="元素名,迭代状态 : ${集合}"
<tr th:each="prod : ${prods}">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
<tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
iterStat 有以下属性:
- index:当前遍历元素的索引,从0开始
- count:当前遍历元素的索引,从1开始
- size:需要遍历元素的总数量
- current:当前正在遍历的元素对象
- even/odd:是否偶数/奇数行
- first:是否第一个元素
- last:是否最后一个元素
判断
th:if
-
<a href="comments.html" th:href="@{/product/comments(prodId=${prod.id})}" th:if="${not #lists.isEmpty(prod.comments)}" >view</a
th:switch
-
<div th:switch="${user.role}"> <p th:case="'admin'">User is an administrator</p> <p th:case="#{roles.manager}">User is a manager</p> <p th:case="*">User is some other thing</p> </div>
属性优先级
优先级 |
功能 |
|
1 |
片段包含 |
th:insert th:replace |
2 |
遍历 |
th:each |
3 |
判断 |
th:if th:unless th:switch th:case |
4 |
定义本地变量 |
th:object th:with |
5 |
通用方式属性修改 |
th:attr th:attrprepend th:attrappend |
6 |
指定属性修改 |
th:value th:href th:src ... |
7 |
文本值 |
th:text th:utext |
8 |
片段指定 |
th:fragment |
9 |
片段移除 |
th:remove |
行内写法
可以把变量写在元素包裹内
语法:
[[...]] or [(...)]
示例
<p>Hello, [[${session.user.name}]]!</p>
变量选择
使用 th:object 关键字绑定一个变量,那么其包裹里的元素,可以直接使用该变量的值,使用 *{} 来读取绑定元素的值
<div th:object="${session.user}">
<p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
<p>Surname: <span th:text="*{lastName}">Pepper</span>.</p>
<p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
</div>
等同于
<div>
<p>Name: <span th:text="${session.user.firstName}">Sebastian</span>.</p>
<p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p>
<p>Nationality: <span th:text="${session.user.nationality}">Saturn</span>.</p>
</div
模板布局
如果我们希望把某些经常用到的html结构抽取到一个文件中,然后在其它页面上重复利用,可以使用模板布局
- 定义模板指令: th:fragment
- 引用模板:~{模板文件名 :: 模板名称}
- 插入模板:th:insert、th:replace
- th:insert 是指可在当前标签内加入该模板结构
- th:replac 是指直把让该模板结构替换掉当前标签
<!-- 在其它地方创建一个用于存储复用结构的html 如 common.html 并使用 th:fragment 指令定义这个模板,模板名为 copy-->
<footer th:fragment="copy">© 2011 The Good Thymes Virtual Grocery</footer>
<!-- 在其它html 文件中若想使用 common.html 中的 copy 模板,则可以定义一个 div 然后使用 th:insert 或 th:replace 来读取模板 -->
<body>
<!-- 使用模板格式 ~{模板html文件名 :: 模板名称} -->
<!-- 使用 th:insert 使得 div 中包裹模板 -->
<div th:insert="~{footer :: copy}"></div>
<!-- 使用 th:replace 直接使用模板替换掉整个 div 标签 -->
<div th:replace="~{footer :: copy}"></div>
</body>
<body>
结果:
<body>
<div>
<footer>© 2011 The Good Thymes Virtual Grocery</footer>
</div>
<footer>© 2011 The Good Thymes Virtual Grocery</footer>
</body>
</body>
国际化(Thymeleaf)
国际化的自动配置参照 MessageSourceAutoConfiguration
实现步骤:
- 1.Spring Boot 在类路径根下查找 message.properties 资源绑定文件。、
- 2.多语言可以定义多个消息文件,命名为 message_区域代码.properties.如
- a.message.properties -> 默认环境(即未知语言环境或未定义国际化的语言环境)
- b.message_zh_CN.properties -> 中文环境
- c.message_en_US.properties -> 英文环境
- 3.在程序中可以自动注入 MessageSource 组件,获得国际化的配置项值
- 4.在页面中可以使用表达式 #{} 获取国际化的配置项值
国际化配置
按照自动配置文件 MessageSourceAutoConfiguration 中可以看到一些预配置项
# 国际化文件定义
spring.message.basename = message
# 文件编码格式
spring.message.encoding = utf-8
在代码中获取国际化字段值
@Autowired //国际化取消息用的组件
MessageSource messageSource;
@GetMapping("/haha")
public String haha(HttpServletRequest request){
Locale locale = request.getLocale();
//利用代码的方式获取国际化配置文件中指定的配置项的值
String login = messageSource.getMessage("login", null, locale);
return login;
}
错误处理
错误处理在自动配置都在 ErrorMvcAutoConfiguration 中。一共两大核心机制
- 1.SpringBoot 会自动适应处理错误,响应页面或JSON数据
- 2.SpringMVC的错误处理机制依然保留,MVC处理不了,才会交给Boot进行处理
错误处理流程原理(了解)
发生错误以后,转发给/error路径,SpringBoot在底层写好一个 BasicErrorController的组件,专门处理这个请求
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE) //返回HTML
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping //返回 ResponseEntity, JSON
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
错误页面是这么解析到的
//1、解析错误的自定义视图地址
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
//2、如果解析不到错误页面的地址,默认的错误页就是 error
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
容器中专门有一个错误视图解析器
@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext, this.resources);
}
SpringBoot解析自定义错误页的默认规则
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
private ModelAndView resolve(String viewName, Map<String, Object> model) {
String errorViewName = "error/" + viewName;
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {
return new ModelAndView(errorViewName, model);
}
return resolveResource(errorViewName, model);
}
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
for (String location : this.resources.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
}
}
return null;
}
容器中有一个默认的名为 error 的 view; 提供了默认白页功能
@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
return this.defaultErrorView;
}
封装了JSON格式的错误信息
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
规则:
- 如果发生了500、404、503、403 这些错误
- 如果有模板引擎,默认在 classpath:/templates/error/精确码.html
- 如果没有模板引擎,在静态资源文件夹下找 精确码.html
- 如果匹配不到精确码.html这些精确的错误页,就去找5xx.html,4xx.html模糊匹配
- 如果有模板引擎,默认在 classpath:/templates/error/5xx.html
- 如果没有模板引擎,在静态资源文件夹下找 5xx.html
- 如果模板引擎路径templates下有 error.html页面,就直接渲染
最佳实战
前后分离
- 后台发生的所有错误,@ControllerAdvice + @ExceptionHandler进行统一异常处理。
服务端页面渲染
- 不可预知的一些,HTTP码表示的服务器或客户端错误
- 给classpath:/templates/error/下面,放常用精确的错误码页面。500.html,404.html
- 给classpath:/templates/error/下面,放通用模糊匹配的错误码页面。 5xx.html,4xx.html
- 发生业务错误
- 核心业务,每一种错误,都应该代码控制,跳转到自己定制的错误页。
- 通用业务,classpath:/templates/error.html页面,显示错误信息。
嵌入式容器
Servlet容器:管理、运行Servlet组件(Servlet、Filter、Listener)的环境,一般指服务器
容器的自动配置(了解)
SpringBoot 默认嵌入Tomcat作为Servlet容器。
自动配置类是ServletWebServerFactoryAutoConfiguration,EmbeddedWebServerFactoryCustomizerAutoConfiguration
自动配置类开始分析功能。`xxxxAutoConfiguration`
自动配置类代码如下:它通过 ServerProperties 类来接收配置参数
@AutoConfiguration
@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 {
}
- ServletWebServerFactoryAutoConfiguration 自动配置了嵌入式容器场景
- 绑定了ServerProperties配置类,所有和服务器有关的配置 server
- ServletWebServerFactoryAutoConfiguration 导入了 嵌入式的三大服务器 Tomcat、Jetty、Undertow
- 导入 Tomcat、Jetty、Undertow 都有条件注解。系统中有这个类才行(也就是导了包)
- 默认 Tomcat配置生效。给容器中放 TomcatServletWebServerFactory
- 都给容器中 ServletWebServerFactory放了一个 web服务器工厂(造web服务器的)
- web服务器工厂 都有一个功能,getWebServer获取web服务器
- TomcatServletWebServerFactory 创建了 tomcat。
- ServletWebServerFactory 什么时候会创建 webServer出来。
- ServletWebServerApplicationContextioc容器,启动的时候会调用创建web服务器
- Spring容器刷新(启动)的时候,会预留一个时机,刷新子容器。onRefresh()
- refresh() 容器刷新 十二大步的刷新子容器会调用 onRefresh();
-
@Override protected void onRefresh() { super.onRefresh(); try { createWebServer(); } catch (Throwable ex) { throw new ApplicationContextException("Unable to start web server", ex); } }
Web场景的Spring容器启动,在onRefresh的时候,会调用创建web服务器的方法。
Web服务器的创建是通过WebServerFactory搞定的。容器中又会根据导了什么包条件注解,启动相关的 服务器配置,默认EmbeddedTomcat会给容器中放一个 TomcatServletWebServerFactory,导致项目启动,自动创建出Tomcat。
自定义容器
<properties>
<servlet-api.version>3.1.0</servlet-api.version>
</properties>
<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>
<!-- 使用 jetty 依赖导入即可使用 jetty 服务器作为web容器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
通过给容器中放一个ServletWebServerFactory,来禁用掉SpringBoot默认放的服务器工厂,实现自定义嵌入任意服务器
全面接管SpringMVC
SpringBoot 默认配置好了 SpringMVC 的所有常用特性。
如果我们需要全面接管SpringMVC的所有配置并禁用默认配置,仅需要编写一个WebMvcConfigurer配置类,并标注 @EnableWebMvc 即可
全手动模式
- @EnableWebMvc : 禁用默认配置
- WebMvcConfigurer组件:定义MVC的底层行为
WebMvcAutoConfiguration 自动配置了哪些规则(了解)
SpringMVC自动配置场景给我们配置了如下所有默认行为
- WebMvcAutoConfigurationweb场景的自动配置类
- 支持RESTful的filter:HiddenHttpMethodFilter
- 支持非POST请求,请求体携带数据:FormContentFilter
- 导入EnableWebMvcConfiguration:
- RequestMappingHandlerAdapter
- WelcomePageHandlerMapping: 欢迎页功能支持(模板引擎目录、静态资源目录放index.html),项目访问/ 就默认展示这个页面.
- RequestMappingHandlerMapping:找每个请求由谁处理的映射关系
- ExceptionHandlerExceptionResolver:默认的异常解析器
- LocaleResolver:国际化解析器
- ThemeResolver:主题解析器
- FlashMapManager:临时数据共享
- FormattingConversionService: 数据格式化 、类型转化
- Validator: 数据校验JSR303提供的数据校验功能
- WebBindingInitializer:请求参数的封装与绑定
- ContentNegotiationManager:内容协商管理器
- WebMvcAutoConfigurationAdapter配置生效,它是一个WebMvcConfigurer,定义mvc底层组件
- 定义好 WebMvcConfigurer 底层组件默认功能;所有功能详见列表
- 视图解析器:InternalResourceViewResolver
- 视图解析器:BeanNameViewResolver,视图名(controller方法的返回值字符串)就是组件名
- 内容协商解析器:ContentNegotiatingViewResolver
- 请求上下文过滤器:RequestContextFilter: 任意位置直接获取当前请求
- 静态资源链规则
- ProblemDetailsExceptionHandler:错误详情(新特性,用于定义Boot自身报出的错误的处理器)
- SpringMVC内部场景异常被它捕获:
- 定义了MVC默认的底层行为: WebMvcConfigurer
WebMvcConfigurer 所包含的功能
提供方法 |
核心参数 |
功能 |
默认 |
addFormatters |
FormatterRegistry |
格式化器:支持属性上@NumberFormat和@DatetimeFormat的数据类型转换 |
GenericConversionService |
getValidator |
无 |
数据校验:校验 Controller 上使用@Valid标注的参数合法性。需要导入starter-validator |
无 |
addInterceptors |
InterceptorRegistry |
拦截器:拦截收到的所有请求 |
无 |
configureContentNegotiation |
ContentNegotiationConfigurer |
内容协商:支持多种数据格式返回。需要配合支持这种类型的HttpMessageConverter |
支持 json |
configureMessageConverters |
List<HttpMessageConverter<?>> |
消息转换器:标注@ResponseBody的返回值会利用MessageConverter直接写出去 |
8 个,支持byte,string,multipart,resource,json |
addViewControllers |
ViewControllerRegistry |
视图映射:直接将请求路径与物理视图映射。用于无 java 业务逻辑的直接视图页渲染 |
无 |
configureViewResolvers |
ViewResolverRegistry |
视图解析器:逻辑视图转为物理视图 |
ViewResolverComposite |
addResourceHandlers |
ResourceHandlerRegistry |
静态资源处理:静态资源路径映射、缓存控制 |
ResourceHandlerRegistry |
configureDefaultServletHandling |
DefaultServletHandlerConfigurer |
默认 Servlet:可以覆盖 Tomcat 的DefaultServlet。让DispatcherServlet拦截/ |
无 |
configurePathMatch |
PathMatchConfigurer |
路径匹配:自定义 URL 路径匹配。可以自动为所有路径加上指定前缀,比如 /api |
无 |
configureAsyncSupport |
AsyncSupportConfigurer |
异步支持: |
TaskExecutionAutoConfiguration |
addCorsMappings |
CorsRegistry |
跨域: |
无 |
addArgumentResolvers |
List<HandlerMethodArgumentResolver> |
参数解析器: |
mvc 默认提供 |
addReturnValueHandlers |
List<HandlerMethodReturnValueHandler> |
返回值解析器: |
mvc 默认提供 |
configureHandlerExceptionResolvers |
List<HandlerExceptionResolver> |
异常处理器: |
默认 3 个 |
getMessageCodesResolver |
无 |
消息码解析器:国际化使用 |
无 |
@EnableWebMvc 为什么能禁用默认行为
@EnableWebMvc给容器中导入 DelegatingWebMvcConfiguration组件,他是 WebMvcConfigurationSupport
WebMvcAutoConfiguration有一个核心的条件注解, @ConditionalOnMissingBean(WebMvcConfigurationSupport.class),容器中没有WebMvcConfigurationSupport,WebMvcAutoConfiguration才生效.
@EnableWebMvc 导入 WebMvcConfigurationSupport 导致 WebMvcAutoConfiguration 失效。导致禁用了默认行为
Problemdetails
Problemdetails 是 SpringBoot3 提供的新特性,默认不开启,需要配置
spring.mvc.problemdetails.enabled = true # 开启 Problemdetails
RFC 7807: https://www.rfc-editor.org/rfc/rfc7807
错误信息返回新格式
ProblemDetailsExceptionHandler 是一个 @ControllerAdvice集中处理系统异常
@Configuration(proxyBeanMethods = false)
//配置过一个属性 spring.mvc.problemdetails.enabled=true
@ConditionalOnProperty(prefix = "spring.mvc.problemdetails", name = "enabled", havingValue = "true")
static class ProblemDetailsErrorHandlingConfiguration {
@Bean
@ConditionalOnMissingBean(ResponseEntityExceptionHandler.class)
ProblemDetailsExceptionHandler problemDetailsExceptionHandler() {
return new ProblemDetailsExceptionHandler();
}
}
处理以下异常。如果系统出现以下异常,会被SpringBoot支持以 RFC 7807规范方式返回错误数据
@ExceptionHandler({
HttpRequestMethodNotSupportedException.class, //请求方式不支持
HttpMediaTypeNotSupportedException.class,
HttpMediaTypeNotAcceptableException.class,
MissingPathVariableException.class,
MissingServletRequestParameterException.class,
MissingServletRequestPartException.class,
ServletRequestBindingException.class,
MethodArgumentNotValidException.class,
NoHandlerFoundException.class,
AsyncRequestTimeoutException.class,
ErrorResponseException.class,
ConversionNotSupportedException.class,
TypeMismatchException.class,
HttpMessageNotReadableException.class,
HttpMessageNotWritableException.class,
BindException.class
})
默认响应错误的json。状态码 405
{
"timestamp": "2023-04-18T11:13:05.515+00:00",
"status": 405,
"error": "Method Not Allowed",
"trace": "org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'POST' is not supported\r\n\tat org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.handleNoMatch(RequestMappingInfoHandlerMapping.java:265)\r\n\tat org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.lookupHandlerMethod(AbstractHandlerMethodMapping.java:441)\r\n\tat org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.getHandlerInternal(AbstractHandlerMethodMapping.java:382)\r\n\tat org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.getHandlerInternal(RequestMappingInfoHandlerMapping.java:126)\r\n\tat org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.getHandlerInternal(RequestMappingInfoHandlerMapping.java:68)\r\n\tat org.springframework.web.servlet.handler.AbstractHandlerMapping.getHandler(AbstractHandlerMapping.java:505)\r\n\tat org.springframework.web.servlet.DispatcherServlet.getHandler(DispatcherServlet.java:1275)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1057)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:974)\r\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1011)\r\n\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914)\r\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:563)\r\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)\r\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:631)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:205)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\r\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\r\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\r\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\r\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\r\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:166)\r\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)\r\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:493)\r\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)\r\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)\r\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)\r\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:341)\r\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:390)\r\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)\r\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:894)\r\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741)\r\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)\r\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)\r\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)\r\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\r\n\tat java.base/java.lang.Thread.run(Thread.java:833)\r\n",
"message": "Method 'POST' is not supported.",
"path": "/list"
}
开启ProblemDetails返回, 使用新的MediaType,返回如下格式的报错信息
Content-Type: application/problem json+ 额外扩展返回
{
"type": "about:blank",
"title": "Method Not Allowed",
"status": 405,
"detail": "Method 'POST' is not supported.",
"instance": "/list"
}
数据访问
数据库自动配置
JDBC-starter 的自动配置场境
- mybatis-spring-boot-starter导入 spring-boot-starter-jdbc,jdbc是操作数据库的场景
- Jdbc场景的几个自动配置
- org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
- 数据源的自动配置
- 所有和数据源有关的配置都绑定在DataSourceProperties
- 默认使用 HikariDataSource
- org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration
- 给容器中放了JdbcTemplate操作数据库
- org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration
- org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration
- 基于XA二阶提交协议的分布式事务数据源
- org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration
- 支持事务
- org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
- 具有的底层能力:数据源、JdbcTemplate、事务
- mybatis-spring-boot-starter导入 mybatis-spring-boot-autoconfigure(mybatis的自动配置包)
- 默认加载两个自动配置类:
- org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration
- org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
- 必须在数据源配置好之后才配置
- 给容器中SqlSessionFactory组件。创建和数据库的一次会话
- 给容器中SqlSessionTemplate组件。操作数据库
- MyBatis的所有配置绑定在MybatisProperties
- 每个Mapper接口的代理对象是怎么创建放到容器中。详见@MapperScan原理:
- 利用@Import(MapperScannerRegistrar.class)批量给容器中注册组件。解析指定的包路径里面的每一个类,为每一个Mapper接口类,创建Bean定义信息,注册到容器中。
SpringBoot 基础特性
SpringApplication - Banner 开屏Logo
Spring的开屏Logo是可以自定义的,我们可以使用配置设置自定义的Logo
# 自定义banner字符
spring.banner.location=classpath:/banner.txt
# 关闭banner
spring.main.banner-mode=off
# 代码中关闭Banner,在SpringApplication.run 中设置
SpringApplication app = new SpringApplication(class)
// 设置Spring应用的各种属性,也可在这里配置
app.setBannerMode(Banner.Mode.Off)
app.setXXXX()
// 注意:配置文件优先级更高
// 让程序运行起来
app.run(args)
SpringApplication - Builder 构建模式启动
Spring支持使用流式API来启动系统
new SpringApplicationBuilder()
.main(ManagerApplication.class)
.environment(null)
.bannerMode(Banner.Mode.OFF)
.banner(null)
.run(args);
Profiles
Spring 针对系统的【开发】、【测试】、【生产】等环境下的场境使用情况,提出一种环境隔离能力,在不同的环境下,会使用不同的环境配置能力
区分环境
如果我们不定义任何环境情况下,Spring提供了默认环境
@Profiles({"default"})
如果我们需要激活我们定义的环境,我们需要在配置文件中加入激活环境设置
spring.profiles.active=dev,test
在系统运行时通过命令行定义环境
java -jar xxx.jar --spring.profiles.active=prod
指定默认环境
Spring中默认的环境是 "default",当然我们也可以把默认环境给修改掉
spring.profiles.default = dev
此时默认环境会被定义为 dev 环境
设置包含指定多个环境(不管激活哪个环境,这些环境都会生效的环境)
spring.profiles.include= dev,test
比如,有一些组件,不管在测试还是在生产环境中,都必须要生效的,如Mybatis环境,log环境等
对多个环境进行分组
如果每个环境都需要同时被激活,要一个个激活是比较麻烦的,这时我们可以把某些环境组合成一个组,直接一次启动一个环境组
spring.profiles.group.haha= dev,test
# 激活haha组
spring.profiles.active = haha
指定环境
在组件中定义注解 @Profiles定义生效环境,定义后,只有激活了当前环境,这个组件才会被加入到IoC中
@Profiles({"dev", "test"})
@component()
public class Person {}
如果组件不配置 @Profiles 则表示任何环境下都会加载
@Profiles({"dev", "test"})
@Bean
public Cat cat(){
}
配置文件激活
spring.profiles.active 除了能激活代码中配置了 @Profiles 注解的组件,同时也是激活配置文件的方法,通过增加 application-{profiles}.yaml 命名文件的配置,可以使用spring.profiles.active激活
注意:profiles 配置文件中不能再配置 spring.profiles.active 之类的配置项了。
外部化配置
在已打包jar的应中,我们要如何修改里面的配置。
SpringBoot允许将配置外部化,以便可以在不同的环境中使用相同的应用程序代码
我们可以使用各种外部配置源,包括Java ProPerties 文件,Yaml文件,环境变量和命令行参数
@Value可以获取做,也可以用@ConfigurationProperties将所有属性绑定到java project 中
以下是SpringBoot属性源加载顺序,后面的优先级比前面的高
- 1.默认属性(通过SpringApplication.setDefaultProperties指定的)
- 2.@PropertySource("classpath:/xxx.yml")指定加载的配置(需要写在@Configuration类上才可生效)
- 3.配置文件(application.properties和yml等)
- 4.RandomValuePropertySource支持的random.*配置(如: @Value("${random.int}"))
- 5.OS环境变量
- 6.Java 系统属性(System.getProperties())
- 7.JNDI属性(来自java:comp/env)
- 8.ServletContext 初始化参数
- 9.ServletConfig 初始化参数
- 10.SPRING_APPLICATION_JSON属性(内置在环境变量或系统属性中的JSON)
- 11.命令行参数
- 12.测试属性(@SpringBootTest进行测试时指定的属性)
- 13.测试类 @TestPropertySource注解
- 14.Devtools 设置的全局属性。($HOME/.config/spring-boot)
- 1.jar 包内的 application.properties /yml
- 2.jar包内的 application-{profiles}.properties /yml
- 3.jar包外的 application.properties /yml
- 4.jar包外的 application={profiles}.properties /yml
注意:如果 .properties 和 .yml 同时存在,则 .properties优先
所有参数均可由命令行传入,使用 --参数项=参数值,将会被添加到环境变量中,并优先于配置文件。
比姐 java -jar app.jar --name="Spring" 可以使用 @Value(${name})获取
外部配置
SpringBoot应用启动时会自动寻找 application.properties和yaml位置,进行加载。顺序如何
越后优先级越高
- 内部
- 1.类根路径 的 properties / yml
- 2.类下 resource/config 包中的 properties / yml
- 外部
- 1.当前路径的 properties / yml
- 2.当前下 /config 子目录: 如 /config 中的 properties / yml
- 3. /config 目录的直接子目录: 如 /config/haha中的 properties / yml
配置优先级顺序图
导入配置
使用spring.config.import可以导入额外配置
spring.config.import=my.properties
# my.properties 中配置的属性
aaa = bbb
# 在application 配置中属性,这里生效
aaa = ccc
注意:导入配置优先级低于配置文件的优先级
属性占位符
配置文件中可以使用 ${name:default}形式取出之前配置过的值。
app.name=MyApp
app.description=${app.name} is a Spring Boot application written by ${username:默认值}
单元测试
SpringBoot 提供一系列测试工具集及注解方便我们进行测试。
spring-boot-test提供核心测试能力,spring-boot-test-autoconfigure 提供测试的一些自动配置。
我们只需要导入spring-boot-starter-test 即可整合测试
JUnit5
整合JUnit 5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
测试中使用到的注解
JUnit5的注解与JUnit4的注解有所变化
https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations
- @Test :表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
- @ParameterizedTest :表示方法是参数化测试,下方会有详细介绍
- @RepeatedTest :表示方法可重复执行,下方会有详细介绍
- @DisplayName :为测试类或者测试方法设置展示名称
- @BeforeEach :表示在每个单元测试之前执行
- @AfterEach :表示在每个单元测试之后执行
- @BeforeAll :表示在所有单元测试之前执行
- @AfterAll :表示在所有单元测试之后执行
- @Tag :表示单元测试类别,类似于JUnit4中的@Categories
- @Disabled :表示测试类或测试方法不执行,类似于JUnit4中的@Ignore
- @Disabled :表示测试类或测试方法不执行,类似于JUnit4中的@Ignore
- @ExtendWith :为测试类或测试方法提供扩展类引用
断言
方法 |
说明 |
assertEquals |
判断两个对象或两个原始类型是否相等 |
assertNotEquals |
判断两个对象或两个原始类型是否不相等 |
assertSame |
判断两个对象引用是否指向同一个对象 |
assertNotSame |
判断两个对象引用是否指向不同的对象 |
assertTrue |
判断给定的布尔值是否为 true |
assertFalse |
判断给定的布尔值是否为 false |
assertNull |
判断给定的对象引用是否为 null |
assertNotNull |
判断给定的对象引用是否不为 null |
assertArrayEquals |
数组断言 |
assertAll |
组合断言 |
assertThrows |
异常断言 |
assertTimeout |
超时断言 |
fail |
快速失败 |
嵌套测试
JUnit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach 和@AfterEach 注解,而且嵌套的层次没有限制。
示例:
@DisplayName("A stack")
class TestingAStackDemo {
Stack<Object> stack;
@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
}
@Nested
@DisplayName("when new")
class WhenNew {
@BeforeEach
void createNewStack() {
stack = new Stack<>();
}
@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, stack::pop);
}
@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, stack::peek);
}
@Nested
@DisplayName("after pushing an element")
class AfterPushing {
String anElement = "an element";
@BeforeEach
void pushAnElement() {
stack.push(anElement);
}
@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}
@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
}
参数化测试
参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。
利用@ValueSource等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。
@ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
@NullSource: 表示为参数化测试提供一个null的入参
@EnumSource: 表示为参数化测试提供一个枚举入参
@CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
@MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)
示例:
@ParameterizedTest
@ValueSource(strings = {"one", "two", "three"})
@DisplayName("参数化测试1,会把上面三个元素都埃个测试一遍")
public void parameterizedTest1(String string) {
System.out.println(string);
Assertions.assertTrue(StringUtils.isNotBlank(string));
}
@ParameterizedTest
@MethodSource("method") //指定方法名
@DisplayName("方法来源参数,会调用 method 方法来获得测试参数,并挨个测试,需要接收一个 Stream 对象")
public void testWithExplicitLocalMethodSource(String name) {
System.out.println(name);
Assertions.assertNotNull(name);
}
static Stream<String> method() {
return Stream.of("apple", "banana");
}
SpringBoot 核心原理
生命周期监听器
SpringBoot的启动整个过程是有生命周期的,我们可以通过定义一个生命周期类,来让Spring在启动的每个阶段给我们报告启动情况
步骤:
- 一、生命周期监听类定义在项目的 classpath:/META-INF/spring.factories 文件中
- 二、配置生命周期的key为 SpringApplicationRunListener 的全类名
- 三、配置生命周期的value为 我们自定义生命周期的类的全类名
配置生命周期运行的 spring.factories 文件
org.springframework.boot.SpringApplicationRunListener=cn.unsoft.spzx.manager.bootstrap.RunListener
配置生命周期监听类
package cn.unsoft.spzx.manager.bootstrap;
import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import java.time.Duration;
public class RunListener implements SpringApplicationRunListener {
@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) {
/**
* 【准备阶段】
* 应用开始,SpringApplication 的 run 方法一调用,只要有了 BootstapContext 就执行
*/
}
@Override
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
/**
* 【准备阶段】
* 环境准备好(把启动参数等绑定到环境变量中),但是ioc还没有创建【只调一次】
*/
}
@Override
public void contextPrepared(ConfigurableApplicationContext context) {
/**
* 【启动阶段】
* ioc容器创建并准备好,但是source(主配置类)没加载,并关闭引导上下文(bootstrapContext)
* 组件都没创建【调一次】
*/
}
@Override
public void contextLoaded(ConfigurableApplicationContext context) {
/**
* 【启动阶段】
* ioc容器加载了,主配置类加载进去了,但是ioc容器还没刷新(我们的bean还没创建)
* 【调一次】
*/
}
@Override
public void started(ConfigurableApplicationContext context, Duration timeTaken) {
/**
* 【启动阶段】
* ioc容器刷新了,所有bean造好了,但是runner还没调用
* 【调一次】
*/
}
@Override
public void ready(ConfigurableApplicationContext context, Duration timeTaken) {
/**
* 【肩动阶段】
* ioc容器刷新了,所有bean造好了,所有runner都调完了
* 如果到了这个阶段,说明所有系统组件都已经完成启动了
*/
}
@Override
public void failed(ConfigurableApplicationContext context, Throwable exception) {
/**
* 对
* environmentPrepared
* contextPrepared
* contextLoaded
* started
* ready
* 进行 try 如果其中之一启动失败,就会调用这个方法
*/
}
}
生命周期流程图
事件触发回调
在SpringBoot 启动时机中,有非常多的生命周期,其中SpringApplicationRunListener只是整个生命周期中比较重要的,但是除了SpringApplicationRunListener这种控制全局生命周期的类以外,还有通过事件机制进行生命周期控制的各类事件和监听器
各种回调监听器如下:
- BootstrapRegistryInitializer : 用于负责感知特定阶段,用于感知引导初始化阶段的
- 它可以定义于 META-INF/spring.factories 文件中
- 执行阶段: 创建引导上下文bootstrapContext的时候触发。
- 也可以在bootstrapContext 中定义 application.addBootstrapRegistryInitializer();
- ApplicationContextInitializer : 用于负责感知特定阶段,用于感知ioc容器初始化
- 它可以定义于 META-INF/spring.factories 文件中
- 也可以在bootstrapContext 中定义 application.addInitializers();
- ApplicationListener : 感知全阶段,基于事件机制,感知事件。一旦到了哪个阶段可以做别的事情
- 它和 SpringApplicationRunListener 不一样的地方在于,ApplicationListener 是接收事件并执行,不是处理全局生命周期的
- SpringApplicationRunListener : 感知全阶段生命周期 + 各种阶段都能自定义操作
- 它可以定义于 META-INF/spring.factories 文件中
- ApplicationRunner : 感知特定阶段,感知应用就绪Ready.如果在其中的程序没有完成,就不会执行ready方法
- 可以在@Bean中定义
- CommandLineRunner : 感知特定阶段,感知应用就绪Ready.如果在其中的程序没有完成,就不会执行ready方法 和 ApplicationRunner 是一样的
- 可以在@Bean中定义
最佳实践:
- 如果项目启动前做事: BootstrapRegistryInitializer 和 ApplicationContextInitializer
- 如果想要在项目启动完成后做事:ApplicationRunner和 CommandLineRunner
- 如果要干涉生命周期做事:SpringApplicationRunListener
- 如果想要用事件机制:ApplicationListener
完整的触发流程
完整角发的9大事件,基于 ApplicationListener 提供的事件回调
- 1.ApplicationStartingEvent:应用启动但未做任何事情, 除过注册listeners and initializers.
- 2.ApplicationEnvironmentPreparedEvent: Environment 准备好,但context 未创建.
- 3.ApplicationContextInitializedEvent: ApplicationContext 准备好,ApplicationContextInitializers 调用,但是任何bean未加载
- 4.ApplicationPreparedEvent: 容器刷新之前,bean定义信息加载
- 5.ApplicationStartedEvent: 容器刷新完成, runner未调用
- ========以下就开始插入了探针机制============
- 6.AvailabilityChangeEvent: LivenessState.CORRECT应用存活; 存活探针,即告诉程序程度了但不确定能不能提供服务
- 7.ApplicationReadyEvent: 任何runner被调用
- 8.AvailabilityChangeEvent:ReadinessState.ACCEPTING_TRAFFIC就绪探针,可以接请求,明确告知程序能提供服务了
- 9.ApplicationFailedEvent :启动出错
应用事件发送顺序如下:
感知应用是否存活了:可能植物状态,虽然活着但是不能处理请求。
应用是否就绪了:能响应请求,说明确实活的比较好。
自定义事件驱动开发
我们可以利用Spring的事件发布与监听技术,自定义发布事件,和监听事件,从而实现业务开发。
首先我们可以通过 Spring 提供的 ApplicationEventPublisherAware 类中能获取 事件对象
我们可以通过编写一个专门用于发布事件的类,当我们需要发布事件时,就调用这个类来发布事件
@Service
public class EventPublisher implements ApplicationEventPublisherAware {
/**
* 底层发送事件用的组件,SpringBoot会通过ApplicationEventPublisherAware接口自动注入给我们
* 事件是广播出去的。所有监听这个事件的监听器都可以收到
*/
ApplicationEventPublisher applicationEventPublisher;
/**
* 调用方法,发布事件
* @param event
*/
public void sendEvent(ApplicationEvent event) {
//调用底层API发送事件
applicationEventPublisher.publishEvent(event);
}
/**
* 会被自动调用,把真正发事件的底层组组件给我们注入进来
* @param applicationEventPublisher event publisher to be used by this object
*/
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
}
定义事件的类型,因为事件在整个Spring中是有非常多的,它的从类是ApplicationEvent,如果我们监听 ApplicationEvent 那么系统上所有的事件都会被接收,所以我们需要定义我们自己的事件类型
public class LoginSuccessEvent extends ApplicationEvent {
public LoginSuccessEvent(Object source) {
super(source);
}
}
事件的监听并处理者,可以通过使用 @EventListener 注解,接收发布的事件
@Service
public class Services{
// 使用 @EventListener 声明该方法是用来接收事件的,所接收的参数则为该事件的类型
// 我们可以定义事件的类型,只接收什么类型的事件
@Order(1)
@EventListener
public void onEvent(LoginSuccessEvent loginSuccessEvent){
System.out.println("===== CouponService ====感知到事件"+loginSuccessEvent);
UserEntity source = (UserEntity) loginSuccessEvent.getSource();
sendCoupon(source.getUsername());
}
public void sendCoupon(String username){
System.out.println(username + " 随机得到了一张优惠券");
}
}
或者使用实现接口的方式定义事件
public class TestServiceImpl implements ApplicationListener<LoginSuccessEvent> {
@Override
public void onApplicationEvent(LoginSuccessEvent event) {
}
}
这时我们只需要调用发布事件,所有的监听者都会收到并处理
EventPublisher.sendEvent(data); // data 为需要传递的参数到事件中,给监听者使用
进阶理解
@SpringBootApplication 注解流程
@SpringBootApplication 注解 是标注整个程序为一个Spring应用程序,其中包含了三个主要的注解
- @SpringBootConfiguration
- 就是一个 @Configuration, 容器中的组件,配置类。Spring IoC 启动就会加载创建这个类对象
- @EnableAutoConfiguration 开启自动配置
- @AutoConfigurationPackage : 扫描主程序包
- 它是利用 @Import 引入一些组件
- 获取当前运行类所在的包名,并把主程序所在的包及其子包的组件都导入进来
- @Import(AutoConfigurationImportSelector.class) : 加载所有自动配置类
-
List<String> configurations = ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()) .getCandidates();
- 扫描SPI文件:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
-
- @ComponentScan 组件扫描
- 排除一些组件,排除哪些不要的组件
- 排除 配置类,和自动配置类,因为上面 @Import 已经把配置类都加载进来了,所以这个扫描是用于扫描用户写的组件
完整启动流程
自定义Starter
共有 0 条评论