- 一、基础入门
- 二、底层注解
- 三、自动配置
- 四、最佳实践
- 五、配置文件
- 行内写法:
- 或
- 2.自定义类绑定的配置提示
- 六、web场景
- 七、请求处理
- 八、响应处理
- 九、视图解析
- 十、web实验
- 十一、拦截器
- 十二、文件上传
- 十三、错误处理
- 十四、原生组件注入
- 十五、嵌入式Servlet容器
- 十六、定制化原理
- 十七、数据访问
- link">numbers表示methods for formatting numeric objects.link
- 十八、单元测试
- 十九、指标监控
- 二十、高级特性
一、基础入门
1.SpringBoot2课程介绍
- 学习要求
- 熟悉Spring基础
- 熟悉Maven使用
- 环境要求
- Java8及以上
- Maven 3.3及以上
学习资料
- Spring Boot官网
- Spring Boot官方文档
- 本课程文档地址
- 视频地址1、视频地址2
- 源码地址
2.Spring生态圈
Spring官网2.1 Spring的能力
2.2 Spring的生态
覆盖了:
web开发
- 数据访问
- 安全控制
- 分布式
- 消息服务
- 移动开发
- 批处理
-
2.3 Spring5重大升级
响应式编程
- 内部源码设计
基于Java8的一些新特性,如:接口默认实现。重新设计源码架构。
为什么用SpringBoot
Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can “just run”.link
能快速创建出生产级别的Spring应用。
2.4 SpringBoot优点
- Create stand-alone Spring applications
- 创建独立Spring应用
- Embed Tomcat, Jetty or Undertow directly (no need to deploy WAR files)
- 内嵌web服务器
- Provide opinionated ‘starter’ dependencies to simplify your build configuration
- 自动starter依赖,简化构建配置
- Automatically configure Spring and 3rd party libraries whenever possible
- 自动配置Spring以及第三方功能
- Provide production-ready features such as metrics, health checks, and externalized configuration
- 提供生产级别的监控、健康检查及外部化配置
- Absolutely no code generation and no requirement for XML configuration
- 无代码生成、无需编写XML
- SpringBoot是整合Spring技术栈的一站式框架
SpringBoot是简化Spring技术栈的快速开发脚手架
2.5 SpringBoot缺点
人称版本帝,迭代快,需要时刻关注变化
-
3.SpringBoot的大时代背景
微服务
In short, the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery. There is a bare minimum of centralized management of these services, which may be written in different programming languages and use different data storage technologies.——James Lewis and Martin Fowler (2014) 微服务是一种架构风格
- 一个应用拆分为一组小型服务
- 每个服务运行在自己的进程内,也就是可独立部署和升级
- 服务之间使用轻量级HTTP交互
- 服务围绕业务功能拆分
- 可以由全自动部署机制独立部署
- 去中心化,服务自治。服务可以使用不同的语言、不同的存储技术
分布式
分布式的困难
- 远程调用
- 服务发现
- 负载均衡
- 服务容错
- 配置管理
- 服务监控
- 链路追踪
- 日志管理
- 任务调度
- …
分布式的解决
- SpringBoot + SpringCloud
云原生
原生应用如何上云。 Cloud Native
上云的困难
- 服务自愈
- 弹性伸缩
- 服务隔离
- 自动化部署
- 灰度发布
- 流量治理
- …
4.SpringBoot官方文档架构
- Spring Boot官网
-
4.1 官网文档架构
4.2 查看版本新特性
5.SpringBoot-HelloWorld
5.1 系统要求
Java 8
- Maven 3.3+
- IntelliJ IDEA 2019.1.2
5.2 Maven配置文件
新添内容: ```xmlnexus-aliyun central Nexus aliyun http://maven.aliyun.com/nexus/content/groups/public
<activation>
<activeByDefault>true</activeByDefault>
<jdk>1.8</jdk>
</activation>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
</properties>
</profile>
<a name="Ckvg5"></a>
#### 5.3 HelloWorld项目
需求:浏览发送/hello请求,响应 “Hello,Spring Boot 2”<br />1.创建maven工程<br />2.引入依赖
```xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
3.创建主程序
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}
4.编写业务
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@RequestMapping("/hello")
public String handle01(){
return "Hello, Spring Boot 2!";
}
}
5.运行&测试
- 运行MainApplication类
- 浏览器输入http://localhost:8888/hello,将会输出Hello, Spring Boot 2!。
6.设置配置
maven工程的resource文件夹中创建application.properties文件。
# 设置端口号
server.port=8888
更多配置信息
7.打包部署
在pom.xml添加
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
在IDEA的Maven插件上点击运行 clean 、package,把helloworld工程项目的打包成jar包,
打包好的jar包被生成在helloworld工程项目的target文件夹内。
用cmd运行java -jar boot-01-helloworld-1.0-SNAPSHOT.jar,既可以运行helloworld工程项目。
将jar包直接在目标服务器执行即可。
6.SpringBoot-依赖管理特性
6.1 父项目做依赖管理
依赖管理
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
</parent>
上面项目的父项目如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.4.RELEASE</version>
</parent>
它几乎声明了所有开发中常用的依赖的版本号,自动版本仲裁机制
6.2 开发导入starter场景启动器
- 见到很多 spring-boot-starter- : 就某种场景
- 只要引入starter,这个场景的所有常规需要的依赖我们都自动引入
- 更多SpringBoot所有支持的场景
见到的 *-spring-boot-starter: 第三方为我们提供的简化开发的场景启动器。
6.3 所有场景启动器最底层的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.3.4.RELEASE</version>
<scope>compile</scope>
</dependency>
6.4 无需关注版本号,自动版本仲裁
引入依赖默认都可以不写版本
-
6.5 可以修改默认版本号
查看spring-boot-dependencies里面规定当前依赖的版本 用的 key。
在当前项目里面重写配置,如下面的代码。
<properties>
<mysql.version>5.1.43</mysql.version>
</properties>
7.SpringBoot-自动配置特性
7.1 自动配好Tomcat
引入Tomcat依赖
配置Tomcat
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>2.3.4.RELEASE</version>
<scope>compile</scope>
</dependency>
7.2 自动配好SpringMVC
引入SpringMVC全套组件
-
7.3 自动配好Web常见功能,如:字符编码问题
SpringBoot帮我们配置好了所有web开发的常见场景
public static void main(String[] args) {
//1、返回我们IOC容器
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
//2、查看容器里面的组件
String[] names = run.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
}
7.4 默认的包结构
主程序所在包及其下面的所有子包里面的组件都会被默认扫描进来
- 无需以前的包扫描配置
想要改变扫描路径
默认配置最终都是映射到某个类上,如:MultipartProperties
-
7.6 按需加载所有自动配置项
非常多的starter
- 引入了哪些场景这个场景的自动配置才会开启
SpringBoot所有的自动配置功能都在 spring-boot-autoconfigure 包里面
二、底层注解
1.@Configuration详解
1.1 基本使用
Full模式与Lite模式
示例
/**
* 1、配置类里面使用@Bean标注在方法上给容器注册组件,默认也是单实例的
* 2、配置类本身也是组件
* 3、proxyBeanMethods:代理bean的方法
* Full(proxyBeanMethods = true)(保证每个@Bean方法被调用多少次返回的组件都是单实例的)(默认)
* Lite(proxyBeanMethods = false)(每个@Bean方法被调用多少次返回的组件都是新创建的)
*/
@Configuration(proxyBeanMethods = false) //告诉SpringBoot这是一个配置类 == 配置文件
public class MyConfig {
/**
* Full:外部无论对配置类中的这个组件注册方法调用多少次获取的都是之前注册容器中的单实例对象
* @return
*/
@Bean //给容器中添加组件。以方法名作为组件的id。返回类型就是组件类型。返回的值,就是组件在容器中的实例
public User user01(){
User zhangsan = new User("zhangsan", 18);
//user组件依赖了Pet组件
zhangsan.setPet(tomcatPet());
return zhangsan;
}
@Bean("tom")
public Pet tomcatPet(){
return new Pet("tomcat");
}
}
@Configuration测试代码如下:
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan("com.atguigu.boot")
public class MainApplication {
public static void main(String[] args) {
//1、返回我们IOC容器
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
//2、查看容器里面的组件
String[] names = run.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
//3、从容器中获取组件
Pet tom01 = run.getBean("tom", Pet.class);
Pet tom02 = run.getBean("tom", Pet.class);
System.out.println("组件:"+(tom01 == tom02));
//4、com.atguigu.boot.config.MyConfig$$EnhancerBySpringCGLIB$$51f1e1ca@1654a892
MyConfig bean = run.getBean(MyConfig.class);
System.out.println(bean);
//如果@Configuration(proxyBeanMethods = true)代理对象调用方法。SpringBoot总会检查这个组件是否在容器中有。
//保持组件单实例
User user = bean.user01();
User user1 = bean.user01();
System.out.println(user == user1);
User user01 = run.getBean("user01", User.class);
Pet tom = run.getBean("tom", Pet.class);
System.out.println("用户的宠物:"+(user01.getPet() == tom));
}
}
1.2 最佳实战
配置 类组件之间无依赖关系用Lite模式加速容器启动过程,减少判断
- 配置 类组件之间有依赖关系,方法会被调用得到之前单实例组件,用Full模式(默认)
2.@Import导入组件
@Bean、@Component、@Controller、@Service、@Repository,它们是Spring的基本标签,在Spring Boot中并未改变它们原来的功能。
@Import({User.class, DBHelper.class})给容器中自动创建出这两个类型的组件、默认组件的名字就是全类名
测试类: ```java //1、返回我们IOC容器 ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);@Import({User.class, DBHelper.class})
@Configuration(proxyBeanMethods = false) //告诉SpringBoot这是一个配置类 == 配置文件
public class MyConfig {
}
//…
//5、获取组件 String[] beanNamesForType = run.getBeanNamesForType(User.class);
for (String s : beanNamesForType) { System.out.println(s); }
DBHelper bean1 = run.getBean(DBHelper.class); System.out.println(bean1);
<a name="OWHj4"></a>
### 3.@Conditional条件装配
条件装配:满足Conditional指定的条件,则进行组件注入<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/26803611/1649929579337-00cf674b-9968-4b3e-be65-ee3e230d0cb6.png#clientId=ued26accd-3d58-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=uf2c34bec&margin=%5Bobject%20Object%5D&name=image.png&originHeight=544&originWidth=599&originalType=url&ratio=1&rotation=0&showTitle=false&size=90275&status=done&style=none&taskId=ubd7dcb6b-20b6-4003-bb19-7d1efdc1864&title=)<br />用@ConditionalOnMissingBean举例说明
```java
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(name = "tom")//没有tom名字的Bean时,MyConfig类的Bean才能生效。
public class MyConfig {
@Bean
public User user01(){
User zhangsan = new User("zhangsan", 18);
zhangsan.setPet(tomcatPet());
return zhangsan;
}
@Bean("tom22")
public Pet tomcatPet(){
return new Pet("tomcat");
}
}
public static void main(String[] args) {
//1、返回我们IOC容器
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
//2、查看容器里面的组件
String[] names = run.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
boolean tom = run.containsBean("tom");
System.out.println("容器中Tom组件:"+tom);//false
boolean user01 = run.containsBean("user01");
System.out.println("容器中user01组件:"+user01);//true
boolean tom22 = run.containsBean("tom22");
System.out.println("容器中tom22组件:"+tom22);//true
}
4.@ImportResource导入Spring配置文件
比如,公司使用bean.xml文件生成配置bean,然而你为了省事,想继续复用bean.xml,@ImportResource粉墨登场。
bean.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans ...">
<bean id="haha" class="com.lun.boot.bean.User">
<property name="name" value="zhangsan"></property>
<property name="age" value="18"></property>
</bean>
<bean id="hehe" class="com.lun.boot.bean.Pet">
<property name="name" value="tomcat"></property>
</bean>
</beans>
使用方法:
@ImportResource("classpath:beans.xml")
public class MyConfig {
...
}
测试类:
public static void main(String[] args) {
//1、返回我们IOC容器
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
boolean haha = run.containsBean("haha");
boolean hehe = run.containsBean("hehe");
System.out.println("haha:"+haha);//true
System.out.println("hehe:"+hehe);//true
}
5.@ConfigurationProperties配置绑定
如何使用Java读取到properties文件中的内容,并且把它封装到JavaBean中,以供随时使用
传统方法:
public class getProperties {
public static void main(String[] args) throws FileNotFoundException, IOException {
Properties pps = new Properties();
pps.load(new FileInputStream("a.properties"));
Enumeration enum1 = pps.propertyNames();//得到配置文件的名字
while(enum1.hasMoreElements()) {
String strKey = (String) enum1.nextElement();
String strValue = pps.getProperty(strKey);
System.out.println(strKey + "=" + strValue);
//封装到JavaBean。
}
}
}
Spring Boot一种配置配置绑定:@ConfigurationProperties + @Component
假设有配置文件application.properties
mycar.brand=BYD
mycar.price=100000
只有在容器中的组件,才会拥有SpringBoot提供的强大功能
@Component
@ConfigurationProperties(prefix = "mycar")
public class Car {
...
}
Spring Boot另一种配置配置绑定:@EnableConfigurationProperties + @ConfigurationProperties
- 开启Car配置绑定功能
把这个Car这个组件自动注册到容器中
@EnableConfigurationProperties(Car.class)
public class MyConfig {
...
}
@ConfigurationProperties(prefix = "mycar")
public class Car {
...
}
三、自动配置
1.自动包规则原理
Spring Boot应用的启动类: ```java @SpringBootApplication public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}
@SpringBootApplication源码
```java
@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 {
...
}
重点分析@SpringBootConfiguration,@EnableAutoConfiguration,@ComponentScan。
1.1 @SpringBootConfiguration
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}
1.2 @ComponentScan
1.3 @EnableAutoConfiguration
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
重点分析@AutoConfigurationPackage,@Import(AutoConfigurationImportSelector.class)。
1.4 @AutoConfiguratonPackage
标签名直译为:自动配置包,指定了默认的包规则。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)//给容器中导入一个组件
public @interface AutoConfigurationPackage {
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
}
- 利用Registrar给容器中导入一系列组件
- 将指定的一个包下的所有组件导入进MainApplication所在包下
2.初始加载自动配置类
2.1 @Import(AutoConfigurationImportSelector.class)
- 利用getAutoConfigurationEntry(annotationMetadata);给容器中批量导入一些组件
- 调用List
configurations = getCandidateConfigurations(annotationMetadata, attributes)获取到所有需要导入到容器中的配置类 - 利用工厂加载 Map
> loadSpringFactories(@Nullable ClassLoader classLoader);得到所有的组件 - 从META-INF/spring.factories位置来加载一个文件。
- 默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件
spring-boot-autoconfigure-2.3.4.RELEASE.jar包里面也有META-INF/spring.factories
# 文件里面写死了spring-boot一启动就要给容器中加载的所有配置类
# spring-boot-autoconfigure-2.3.4.RELEASE.jar/META-INF/spring.factories
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
...
虽然我们127个场景的所有自动配置启动的时候默认全部加载,但是xxxxAutoConfiguration按照条件装配规则(@Conditional),最终会按需配置。
如AopAutoConfiguration类:@Configuration(
proxyBeanMethods = false
)
@ConditionalOnProperty(
prefix = "spring.aop",
name = "auto",
havingValue = "true",
matchIfMissing = true
)
public class AopAutoConfiguration {
public AopAutoConfiguration() {
}
...
}
3.自动配置流程
以DispatcherServletAutoConfiguration的内部类DispatcherServletConfiguration为例子:
@Bean
@ConditionalOnBean(MultipartResolver.class) //容器中有这个类型组件
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) //容器中没有这个名字 multipartResolver 的组件
public MultipartResolver multipartResolver(MultipartResolver resolver) {
//给@Bean标注的方法传入了对象参数,这个参数的值就会从容器中找。
//SpringMVC multipartResolver。防止有些用户配置的文件上传解析器不符合规范
// Detect if the user has created a MultipartResolver but named it incorrectly
return resolver;//给容器中加入了文件上传解析器;
}
SpringBoot默认会在底层配好所有的组件,但是如果用户自己配置了以用户的优先。
总结:SpringBoot先加载所有的自动配置类 xxxxxAutoConfiguration
- 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。(xxxxProperties里面读取,xxxProperties和配置文件进行了绑定)
- 生效的配置类就会给容器中装配很多组件
- 只要容器中有这些组件,相当于这些功能就有了
- 定制化配置
- 用户直接自己@Bean替换底层的组件
- 用户去看这个组件是获取的配置文件什么值就去修改。
xxxxxAutoConfiguration —> 组件 —> xxxxProperties里面拿值 ——> application.properties
四、最佳实践
1.SpringBoot应用如何编写
- 引入场景依赖
- 查看自动配置了哪些(选做)
- 自己分析,引入场景对应的自动配置一般都生效了
- 配置文件中debug=true开启自动配置报告。
- Negative(不生效)
- Positive(生效)
是否需要修改
- 参照文档修改配置项
- 官方文档
- 自己分析。xxxxProperties绑定了配置文件的哪些。
- 自定义加入或者替换组件
- @Bean、@Component…
- 自定义器 XXXXXCustomizer;
-
2.Lombok简化开发
Lombok用标签方式代替构造器、getter/setter、toString()等鸡肋代码。
spring boot已经管理Lombok。引入依赖:<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
IDEA中File->Settings->Plugins,搜索安装Lombok插件。
@NoArgsConstructor
//@AllArgsConstructor
@Data
@ToString
@EqualsAndHashCode
public class User {
private String name;
private Integer age;
private Pet pet;
public User(String name,Integer age){
this.name = name;
this.age = age;
}
}
简化日志开发
@Slf4j
@RestController
public class HelloController {
@RequestMapping("/hello")
public String handle01(@RequestParam("name") String name){
log.info("请求进来了....");
return "Hello, Spring Boot 2!"+"你好:"+name;
}
}
3.dev-tools
Spring Boot includes an additional set of tools that can make the application development experience a little more pleasant. The spring-boot-devtools module can be included in any project to provide additional development-time features.——link Applications that use spring-boot-devtools automatically restart whenever files on the classpath change. This can be a useful feature when working in an IDE, as it gives a very fast feedback loop for code changes. By default, any entry on the classpath that points to a directory is monitored for changes. Note that certain resources, such as static assets and view templates, do not need to restart the application.——link Triggering a restart As DevTools monitors classpath resources, the only way to trigger a restart is to update the classpath. The way in which you cause the classpath to be updated depends on the IDE that you are using:
- In Eclipse, saving a modified file causes the classpath to be updated and triggers a restart.
- In IntelliJ IDEA, building the project (Build -> Build Project)(shortcut: Ctrl+F9) has the same effect.
- 参照文档修改配置项
添加依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
4.最佳实践-Spring Initailizr
Spring Initailizr是创建Spring Boot工程向导。
在IDEA中,菜单栏New -> Project -> Spring Initailizr。
五、配置文件
1.yaml的用法
同以前的properties用法
YAML 是 “YAML Ain’t Markup Language”(YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)。
非常适合用来做以数据为中心的配置文件。
1.1 基本语法
- key: value;kv之间有空格
- 大小写敏感
- 使用缩进表示层级关系
- 缩进不允许使用tab,只允许空格
- 缩进的空格数不重要,只要相同层级的元素左对齐即可
- ‘#’表示注释
字符串无需加引号,如果要加,单引号’’、双引号””表示字符串内容会被 转义、不转义
1.2 数据类型
字面量:单个的、不可再分的值。date、boolean、string、number、null
k: v
对象:键值对的集合。map、hash、set、object ```yaml
行内写法:
k: {k1:v1,k2:v2,k3:v3}
或
k: k1: v1 k2: v2 k3: v3
- 数组:一组按次序排列的值。array、list、queue
```yaml
#行内写法:
k: [v1,v2,v3]
#或者
k:
- v1
- v2
- v3
1.3 实例
@Data
public class Person {
private String userName;
private Boolean boss;
private Date birth;
private Integer age;
private Pet pet;
private String[] interests;
private List<String> animal;
private Map<String, Object> score;
private Set<Double> salarys;
private Map<String, List<Pet>> allPets;
}
@Data
public class Pet {
private String name;
private Double weight;
}
用yaml表示以上对象
person:
userName: zhangsan
boss: false
birth: 2019/12/12 20:12:33
age: 18
pet:
name: tomcat
weight: 23.4
interests: [篮球,游泳]
animal:
- jerry
- mario
score:
english:
first: 30
second: 40
third: 50
math: [131,140,148]
chinese: {first: 128,second: 136}
salarys: [3999,4999.98,5999.99]
allPets:
sick:
- {name: tom}
- {name: jerry,weight: 47}
health: [{name: mario,weight: 47}]
2.自定义类绑定的配置提示
You can easily generate your own configuration metadata file from items annotated with @ConfigurationProperties by using the spring-boot-configuration-processor jar. The jar includes a Java annotation processor which is invoked as your project is compiled.——link
自定义的类和配置文件绑定一般没有提示。若要提示,添加如下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- 下面插件作用是工程打包时,不将spring-boot-configuration-processor打进包内,让其只在编码的时候有用 -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
六、web场景
1.web开发简介
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.(大多场景我们都无需自定义配置)
The auto-configuration adds the following features on top of Spring’s defaults:
- Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
- 内容协商视图解析器和BeanName视图解析器
- Support for serving static resources, including support for WebJars (covered later in this document)).
- 静态资源(包括webjars)
- Automatic registration of Converter, GenericConverter, and Formatter beans.
- 自动注册 Converter,GenericConverter,Formatter
- Support for HttpMessageConverters (covered later in this document).
- 支持 HttpMessageConverters (后来我们配合内容协商理解原理)
- Automatic registration of MessageCodesResolver (covered later in this document).
- 自动注册 MessageCodesResolver (国际化用)
- Static index.html support.
- 静态index.html 页支持
- Custom Favicon support (covered later in this document).
- 自定义 Favicon
- Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).
- 自动使用 ConfigurableWebBindingInitializer ,(DataBinder负责将请求数据绑定到JavaBean上)
If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc. 不用@EnableWebMvc注解。使用 @Configuration + WebMvcConfigurer 自定义规则
- 自动使用 ConfigurableWebBindingInitializer ,(DataBinder负责将请求数据绑定到JavaBean上)
If you want to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, and still keep the Spring Boot MVC customizations, you can declare a bean of type WebMvcRegistrations and use it to provide custom instances of those components. 声明 WebMvcRegistrations 改变默认底层组件
If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc, or alternatively add your own @Configuration-annotated DelegatingWebMvcConfiguration as described in the Javadoc of @EnableWebMvc. 使用 @EnableWebMvc+@Configuration+DelegatingWebMvcConfiguration 全面接管SpringMVC
2.静态资源规则与定制化
2.1 静态资源目录
只要静态资源放在类路径下: called /static (or /public or /resources or /META-INF/resources
访问 : 当前项目根路径/ + 静态资源名
原理: 静态映射/**。
请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面。
也可以改变默认的静态资源路径,/static,/public,/resources,/META-INF/resources失效
resources:
static-locations: [classpath:/haha/]
2.2 静态资源访问前缀
spring:
mvc:
static-path-pattern: /res/**
当前项目 + static-path-pattern + 静态资源名 = 静态资源文件夹下找
2.3 webjar
可用jar方式添加css,js等资源文件,
https://www.webjars.org/
例如,添加jquery
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.5.1</version>
</dependency>
访问地址:http://localhost:8080/webjars/jquery/3.5.1/jquery.js 后面地址要按照依赖里面的包路径。
3.welcome与favicon功能
3.1 欢迎页支持
静态资源路径下 index.html。
- 可以配置静态资源路径
- 但是不可以配置静态资源的访问前缀。否则导致 index.html不能被默认访问
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致welcome page功能失效
resources:
static-locations: [classpath:/haha/]
-
3.2 自定义Favicon
指网页标签上的小图标。
favicon.ico 放在静态资源目录下即可。spring:
# mvc:
# static-path-pattern: /res/** 这个会导致 Favicon 功能失效
4.【源码分析】-静态资源原理
SpringBoot启动默认加载 xxxAutoConfiguration 类(自动配置类)
SpringMVC功能的自动配置类WebMvcAutoConfiguration,生效
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
...
}
给容器中配置的内容:
- 配置文件的相关属性的绑定:WebMvcProperties==spring.mvc、ResourceProperties==spring.resources
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
...
}
4.1 配置类只有一个有参构造器
有参构造器所有参数的值都会从容器中确定
public WebMvcAutoConfigurationAdapter(WebProperties webProperties, WebMvcProperties mvcProperties,
ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
ObjectProvider<DispatcherServletPath> dispatcherServletPath,
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
this.mvcProperties.checkConfiguration();
}
- 配置文件的相关属性的绑定:WebMvcProperties==spring.mvc、ResourceProperties==spring.resources
ResourceProperties resourceProperties;获取和spring.resources绑定的所有的值的对象
- WebMvcProperties mvcProperties 获取和spring.mvc绑定的所有的值的对象
- ListableBeanFactory beanFactory Spring的beanFactory
- HttpMessageConverters 找到所有的HttpMessageConverters
- ResourceHandlerRegistrationCustomizer 找到 资源处理器的自定义器。
- DispatcherServletPath
ServletRegistrationBean 给应用注册Servlet、Filter…
4.2 资源处理的默认规则
...
public class WebMvcAutoConfiguration {
...
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {
...
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
super.addResourceHandlers(registry);
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
ServletContext servletContext = getServletContext();
addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations());
if (servletContext != null) {
registration.addResourceLocations(new ServletContextResource(servletContext, SERVLET_LOCATION));
}
});
}
...
}
...
}
根据上述代码,我们可以同过配置禁止所有静态资源规则。
spring:
resources:
add-mappings: false #禁用所有静态资源规则
静态资源规则:
@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties {
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
"classpath:/resources/", "classpath:/static/", "classpath:/public/" };
/**
* Locations of static resources. Defaults to classpath:[/META-INF/resources/,
* /resources/, /static/, /public/].
*/
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
...
}
4.3 欢迎页的处理规则
...
public class WebMvcAutoConfiguration {
...
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {
...
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
return welcomePageHandlerMapping;
}
WelcomePageHandlerMapping的构造方法如下:
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) {
if (welcomePage != null && "/**".equals(staticPathPattern)) {
//要用欢迎页功能,必须是/**
logger.info("Adding welcome page: " + welcomePage);
setRootViewName("forward:index.html");
}
else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
//调用Controller /index
logger.info("Adding welcome page template: index");
setRootViewName("index");
}
}
这构造方法内的代码也解释了web场景-welcome与favicon功能中配置static-path-pattern了,welcome页面和小图标失效的问题。
七、请求处理
1.【源码分析】-Rest映射及源码解析
1.1 请求映射
@xxxMapping;
- @GetMapping
- @PostMapping
- @PutMapping
- @DeleteMapping
Rest风格支持(使用HTTP请求方式动词来表示对资源的操作)
开启页面表单的Rest功能
- 页面 form的属性method=post,隐藏域 _method=put、delete等(如果直接get或post,无需隐藏域)
- 编写请求映射
```javascriptspring:
mvc:
hiddenmethod:
filter:
enabled: true #开启页面表单的Rest功能
- /templates/main.html 主页
thymeleaf内联写法:
```javascript
<p>Hello, [[${session.user.name}]]!</p>
1.3 登录控制层
@Controller
public class IndexController {
/**
* 来登录页
* @return
*/
@GetMapping(value = {"/","/login"})
public String loginPage(){
return "login";
}
@PostMapping("/login")
public String main(User user, HttpSession session, Model model){ //RedirectAttributes
if(StringUtils.hasLength(user.getUserName()) && "123456".equals(user.getPassword())){
//把登陆成功的用户保存起来
session.setAttribute("loginUser",user);
//登录成功重定向到main.html; 重定向防止表单重复提交
return "redirect:/main.html";
}else {
model.addAttribute("msg","账号密码错误");
//回到登录页面
return "login";
}
}
/**
* 去main页面
* @return
*/
@GetMapping("/main.html")
public String mainPage(HttpSession session, Model model){
//最好用拦截器,过滤器
Object loginUser = session.getAttribute("loginUser");
if(loginUser != null){
return "main";
}else {
//session过期,没有登陆过
//回到登录页面
model.addAttribute("msg","请重新登录");
return "login";
}
}
}
1.4 模型
@AllArgsConstructor
@NoArgsConstructor
@Data
public class User {
private String userName;
private String password;
}
2.抽取公共页面
公共页面/templates/common.html ```html <!DOCTYPE html>
…
- /templates/table/basic_table.html
html
<!DOCTYPE html>
<!-- main content start-->
<div class="main-content" >
<div th:replace="common :: headermenu"></div>
...
</div>
<!-- main content end-->
Difference between th:insert and th:replace (and th:include)
<a name="qfMNi"></a>
### 3.遍历数据与页面bug修改
控制层代码:
```java
@GetMapping("/dynamic_table")
public String dynamic_table(Model model){
//表格内容的遍历
List<User> users = Arrays.asList(new User("zhangsan", "123456"),
new User("lisi", "123444"),
new User("haha", "aaaaa"),
new User("hehe ", "aaddd"));
model.addAttribute("users",users);
return "table/dynamic_table";
}
页面代码:
<table class="display table table-bordered" id="hidden-table-info">
<thead>
<tr>
<th>#</th>
<th>用户名</th>
<th>密码</th>
</tr>
</thead>
<tbody>
<tr class="gradeX" th:each="user,stats:${users}">
<td th:text="${stats.count}">Trident</td>
<td th:text="${user.userName}">Internet</td>
<td >[[${user.password}]]</td>
</tr>
</tbody>
</table>
十一、拦截器
1.登录检查与静态资源放行
1.编写一个拦截器实现HandlerInterceptor接口
2.拦截器注册到容器中(实现WebMvcConfigurer的addInterceptors())
3.指定拦截规则(注意,如果是拦截所有,静态资源也会被拦截】
编写一个实现HandlerInterceptor接口的拦截器:
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
/**
* 目标方法执行之前
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
log.info("preHandle拦截的请求路径是{}",requestURI);
//登录检查逻辑
HttpSession session = request.getSession();
Object loginUser = session.getAttribute("loginUser");
if(loginUser != null){
//放行
return true;
}
//拦截住。未登录。跳转到登录页
request.setAttribute("msg","请先登录");
// re.sendRedirect("/");
request.getRequestDispatcher("/").forward(request,response);
return false;
}
/**
* 目标方法执行完成以后
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle执行{}",modelAndView);
}
/**
* 页面渲染以后
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("afterCompletion执行异常{}",ex);
}
}
拦截器注册到容器中 && 指定拦截规则:
@Configuration
public class AdminWebConfig implements WebMvcConfigurer{
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())//拦截器注册到容器中
.addPathPatterns("/**") //所有请求都被拦截包括静态资源
.excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**",
"/js/**","/aa/**"); //放行的请求
}
2.【源码分析】-拦截器的执行时机和原理
1.根据当前请求,找到HandlerExecutionChain(可以处理请求的handler以及handler的所有 拦截器)
2.先来顺序执行 所有拦截器的 preHandle()方法。
- 如果当前拦截器preHandle()返回为true。则执行下一个拦截器的preHandle()
- 如果当前拦截器返回为false。直接倒序执行所有已经执行了的拦截器的 afterCompletion();。
3.如果任何一个拦截器返回false,直接跳出不执行目标方法。
4.所有拦截器都返回true,才执行目标方法。
5.倒序执行所有拦截器的postHandle()方法。
6.前面的步骤有任何异常都会直接倒序触发 afterCompletion()。
7.页面成功渲染完成以后,也会倒序触发 afterCompletion()。
DispatcherServlet中涉及到HandlerInterceptor的地方:
public class DispatcherServlet extends FrameworkServlet {
...
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
...
//该方法内调用HandlerInterceptor的preHandle()
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
...
//该方法内调用HandlerInterceptor的postHandle()
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
//该方法内调用HandlerInterceptor接口的afterCompletion方法
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
//该方法内调用HandlerInterceptor接口的afterCompletion方法
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
...
}
}
private void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, Exception ex) throws Exception {
if (mappedHandler != null) {
//该方法内调用HandlerInterceptor接口的afterCompletion方法
mappedHandler.triggerAfterCompletion(request, response, ex);
}
throw ex;
}
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
...
if (mappedHandler != null) {
//该方法内调用HandlerInterceptor接口的afterCompletion方法
// Exception (if any) is already handled..
mappedHandler.triggerAfterCompletion(request, response, null);
}
}
}
public class HandlerExecutionChain {
...
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for (int i = 0; i < this.interceptorList.size(); i++) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
//HandlerInterceptor的preHandle方法
if (!interceptor.preHandle(request, response, this.handler)) {
triggerAfterCompletion(request, response, null);
return false;
}
this.interceptorIndex = i;
}
return true;
}
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
throws Exception {
for (int i = this.interceptorList.size() - 1; i >= 0; i--) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
//HandlerInterceptor接口的postHandle方法
interceptor.postHandle(request, response, this.handler, mv);
}
}
void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {
for (int i = this.interceptorIndex; i >= 0; i--) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
try {
//HandlerInterceptor接口的afterCompletion方法
interceptor.afterCompletion(request, response, this.handler, ex);
}
catch (Throwable ex2) {
logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
}
}
}
}
十二、文件上传
1.单文件与多文件上传的使用
页面代码/static/form/form_layouts.html
<form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="exampleInputEmail1">邮箱</label>
<input type="email" name="email" class="form-control" id="exampleInputEmail1" placeholder="Enter email">
</div>
<div class="form-group">
<label for="exampleInputPassword1">名字</label>
<input type="text" name="username" class="form-control" id="exampleInputPassword1" placeholder="Password">
</div>
<div class="form-group">
<label for="exampleInputFile">头像</label>
<input type="file" name="headerImg" id="exampleInputFile">
</div>
<div class="form-group">
<label for="exampleInputFile">生活照</label>
<input type="file" name="photos" multiple>
</div>
<div class="checkbox">
<label>
<input type="checkbox"> Check me out
</label>
</div>
<button type="submit" class="btn btn-primary">提交</button>
</form>
控制层代码 ```java @Slf4j @Controller public class FormTestController {
@GetMapping(“/form_layouts”) public String form_layouts(){
return "form/form_layouts";
}
@PostMapping(“/upload”) public String upload(@RequestParam(“email”) String email,
@RequestParam("username") String username,
@RequestPart("headerImg") MultipartFile headerImg,
@RequestPart("photos") MultipartFile[] photos) throws IOException {
log.info("上传的信息:email={},username={},headerImg={},photos={}",
email,username,headerImg.getSize(),photos.length);
if(!headerImg.isEmpty()){
//保存到文件服务器,OSS服务器
String originalFilename = headerImg.getOriginalFilename();
headerImg.transferTo(new File("H:\\cache\\"+originalFilename));
}
if(photos.length > 0){
for (MultipartFile photo : photos) {
if(!photo.isEmpty()){
String originalFilename = photo.getOriginalFilename();
photo.transferTo(new File("H:\\cache\\"+originalFilename));
}
}
}
return "main";
}
}
文件上传相关的配置类:
- org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration
- org.springframework.boot.autoconfigure.web.servlet.MultipartProperties
文件大小相关配置项:
```properties
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=100MB
2.【源码流程】文件上传参数解析器
文件上传相关的自动配置类MultipartAutoConfiguration有创建文件上传参数解析器StandardServletMultipartResolver。
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class })
@ConditionalOnProperty(prefix = "spring.servlet.multipart", name = "enabled", matchIfMissing = true)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(MultipartProperties.class)
public class MultipartAutoConfiguration {
private final MultipartProperties multipartProperties;
public MultipartAutoConfiguration(MultipartProperties multipartProperties) {
this.multipartProperties = multipartProperties;
}
@Bean
@ConditionalOnMissingBean({ MultipartConfigElement.class, CommonsMultipartResolver.class })
public MultipartConfigElement multipartConfigElement() {
return this.multipartProperties.createMultipartConfig();
}
@Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
@ConditionalOnMissingBean(MultipartResolver.class)
public StandardServletMultipartResolver multipartResolver() {
//配置好文件上传解析器
StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());
return multipartResolver;
}
}
//文件上传解析器
public class StandardServletMultipartResolver implements MultipartResolver {
private boolean resolveLazily = false;
public void setResolveLazily(boolean resolveLazily) {
this.resolveLazily = resolveLazily;
}
@Override
public boolean isMultipart(HttpServletRequest request) {
return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");
}
@Override
public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
}
@Override
public void cleanupMultipart(MultipartHttpServletRequest request) {
if (!(request instanceof AbstractMultipartHttpServletRequest) ||
((AbstractMultipartHttpServletRequest) request).isResolved()) {
// To be on the safe side: explicitly delete the parts,
// but only actual file parts (for Resin compatibility)
try {
for (Part part : request.getParts()) {
if (request.getFile(part.getName()) != null) {
part.delete();
}
}
}
catch (Throwable ex) {
LogFactory.getLog(getClass()).warn("Failed to perform cleanup of multipart items", ex);
}
}
}
}
public class DispatcherServlet extends FrameworkServlet {
@Nullable
private MultipartResolver multipartResolver;
private void initMultipartResolver(ApplicationContext context) {
...
//这个就是配置类配置的StandardServletMultipartResolver文件上传解析器
this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);
...
}
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;//最后finally的回收flag
...
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
//做预处理,如果有上传文件 就new StandardMultipartHttpServletRequest包装类
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
...
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
...
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
}
....
finally {
...
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
...
return this.multipartResolver.resolveMultipart(request);
...
}
}
protected void cleanupMultipart(HttpServletRequest request) {
if (this.multipartResolver != null) {
MultipartHttpServletRequest multipartRequest =
WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class);
if (multipartRequest != null) {
this.multipartResolver.cleanupMultipart(multipartRequest);
}
}
}
}
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());跳到以下的类
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
@Override
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ModelAndView mav;
...
mav = invokeHandlerMethod(request, response, handlerMethod);
...
return mav;
}
@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {//关注点
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
...
invocableMethod.invokeAndHandle(webRequest, mavContainer);
...
return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally {
webRequest.requestCompleted();
}
}
}
this.argumentResolvers其中主角类RequestPartMethodArgumentResolver用来生成
public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
...
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
...
}
@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
...
return doInvoke(args);//反射调用
}
@Nullable
protected Object doInvoke(Object... args) throws Exception {
Method method = getBridgedMethod();
ReflectionUtils.makeAccessible(method);
return method.invoke(getBean(), args);
...
}
//处理得出multipart参数,准备稍后的反射调用(@PostMapping标记的上传方法)
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
MethodParameter[] parameters = getMethodParameters();
...
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
//关注点1
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
//关注点2
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
catch (Exception ex) {
...
}
}
return args;
}
}
public class RequestPartMethodArgumentResolver extends AbstractMessageConverterMethodArgumentResolver {
//对应上面代码关注点1
@Override
public boolean supportsParameter(MethodParameter parameter) {
//标注@RequestPart的参数
if (parameter.hasParameterAnnotation(RequestPart.class)) {
return true;
}
else {
if (parameter.hasParameterAnnotation(RequestParam.class)) {
return false;
}
return MultipartResolutionDelegate.isMultipartArgument(parameter.nestedIfOptional());
}
}
//对应上面代码关注点2
@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest request, @Nullable WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
Assert.state(servletRequest != null, "No HttpServletRequest");
RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class);
boolean isRequired = ((requestPart == null || requestPart.required()) && !parameter.isOptional());
String name = getPartName(parameter, requestPart);
parameter = parameter.nestedIfOptional();
Object arg = null;
//封装成MultipartFile类型的对象作参数
Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {
arg = mpArg;
}
...
return adaptArgumentIfNecessary(arg, parameter);
}
}
public final class MultipartResolutionDelegate {
...
@Nullable
public static Object resolveMultipartArgument(String name, MethodParameter parameter, HttpServletRequest request)
throws Exception {
MultipartHttpServletRequest multipartRequest =
WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class);
boolean isMultipart = (multipartRequest != null || isMultipartContent(request));
if (MultipartFile.class == parameter.getNestedParameterType()) {
if (!isMultipart) {
return null;
}
if (multipartRequest == null) {
multipartRequest = new StandardMultipartHttpServletRequest(request);
}
return multipartRequest.getFile(name);
}
else if (isMultipartFileCollection(parameter)) {
if (!isMultipart) {
return null;
}
if (multipartRequest == null) {
multipartRequest = new StandardMultipartHttpServletRequest(request);
}
List<MultipartFile> files = multipartRequest.getFiles(name);
return (!files.isEmpty() ? files : null);
}
else if (isMultipartFileArray(parameter)) {
if (!isMultipart) {
return null;
}
if (multipartRequest == null) {
multipartRequest = new StandardMultipartHttpServletRequest(request);
}
List<MultipartFile> files = multipartRequest.getFiles(name);
return (!files.isEmpty() ? files.toArray(new MultipartFile[0]) : null);
}
else if (Part.class == parameter.getNestedParameterType()) {
if (!isMultipart) {
return null;
}
return request.getPart(name);
}
else if (isPartCollection(parameter)) {
if (!isMultipart) {
return null;
}
List<Part> parts = resolvePartList(request, name);
return (!parts.isEmpty() ? parts : null);
}
else if (isPartArray(parameter)) {
if (!isMultipart) {
return null;
}
List<Part> parts = resolvePartList(request, name);
return (!parts.isEmpty() ? parts.toArray(new Part[0]) : null);
}
else {
return UNRESOLVABLE;
}
}
...
}
十三、错误处理
1.SpringBoot默认错误处理机制
Spring Boot官方文档 - Error Handling
默认规则:
- 默认情况下,Spring Boot提供/error处理所有错误的映射
机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据
{ "timestamp": "2020-11-22T05:53:28.416+00:00", "status": 404, "error": "Not Found", "message": "No message available", "path": "/asadada" }
要对其进行自定义,添加View解析为error
- 要完全替换默认行为,可以实现 ErrorController并注册该类型的Bean定义,或添加ErrorAttributes类型的组件以使用现有机制但替换其内容。
/templates/error/下的4xx,5xx页面会被自动解析
2.【源码分析】底层组件功能分析
ErrorMvcAutoConfiguration 自动配置异常处理规则
- 容器中的组件:类型:DefaultErrorAttributes -> id:errorAttributes
- public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver
- DefaultErrorAttributes:定义错误页面中可以包含数据(异常明细,堆栈信息等)。
- 容器中的组件:类型:BasicErrorController —> id:basicErrorController(json+白页 适配响应)
- 处理默认 /error 路径的请求,页面响应 new ModelAndView(“error”, model);
- 容器中有组件 View->id是error;(响应默认错误页)
- 容器中放组件 BeanNameViewResolver(视图解析器);按照返回的视图名作为组件的id去容器中找View对象。
- 容器中的组件:类型:DefaultErrorViewResolver -> id:conventionErrorViewResolver
如果发生异常错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面(主要作用)。
- error/404、5xx.html
如果想要返回页面,就会找error视图(StaticView默认是一个白页)。
3.【源码流程】异常处理流程
譬如写一个会抛出异常的控制层:
@Slf4j @RestController public class HelloController { @RequestMapping("/hello") public String handle01(){ int i = 1 / 0;//将会抛出ArithmeticException log.info("Hello, Spring Boot 2!"); return "Hello, Spring Boot 2!"; } }
当浏览器发出/hello请求,DispatcherServlet的doDispatch()的mv = ha.handle(processedRequest, response, mappedHandler.getHandler());将会抛出ArithmeticException。 ```java public class DispatcherServlet extends FrameworkServlet { … protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { …
// Actually invoke the handler. //将会抛出ArithmeticException mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { //将会捕捉ArithmeticException dispatchException = ex; } catch (Throwable err) { ... } //捕捉后,继续运行 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
} catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
} catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", err));
} finally {
...
} }
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception {
boolean errorView = false;
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) { ... } else { Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null); //ArithmeticException将在这处理 mv = processHandlerException(request, response, handler, exception); errorView = (mv != null); }
} … }
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) throws Exception {
// Success and error responses may use different content types request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
// Check registered HandlerExceptionResolvers… ModelAndView exMv = null; if (this.handlerExceptionResolvers != null) {
//遍历所有的 handlerExceptionResolvers,看谁能处理当前异常HandlerExceptionResolver处理器异常解析器 for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) { exMv = resolver.resolveException(request, response, handler, ex); if (exMv != null) { break; } }
} …
//若只有系统的自带的异常解析器(没有自定义的),异常还是会抛出 throw ex; }
}
系统自带的异常解析器:<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/26803611/1650040242421-80ee9754-fd69-4dc9-a2ed-633f51349fe5.png#clientId=ud0782635-09b8-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u73381695&margin=%5Bobject%20Object%5D&name=image.png&originHeight=297&originWidth=608&originalType=url&ratio=1&rotation=0&showTitle=false&size=31080&status=done&style=none&taskId=uda5bd683-3c51-446b-b49c-a44800a312c&title=)
- DefaultErrorAttributes先来处理异常,它主要功能把异常信息保存到request域,并且返回null。
```java
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {
...
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
this.storeErrorAttributes(request, ex);
return null;
}
private void storeErrorAttributes(HttpServletRequest request, Exception ex) {
request.setAttribute(ERROR_ATTRIBUTE, ex);//把异常信息保存到request域
}
...
}
默认没有任何解析器(上图的HandlerExceptionResolverComposite)能处理异常,所以最后异常会被抛出。
最终底层就会转发/error 请求。会被底层的BasicErrorController处理。
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
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);
//如果/template/error内没有4**.html或5**.html,
//modelAndView为空,最终还是返回viewName为error的modelAndView
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
...
}
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
...
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
...
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
...
//渲染页面
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
...
}
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
boolean errorView = false;
...
// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
...
}
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
...
View view;
String viewName = mv.getViewName();
if (viewName != null) {
// We need to resolve the view name.
//找出合适error的View,如果/template/error内没有4**.html或5**.html,
//将会返回默认异常页面ErrorMvcAutoConfiguration.StaticView
//这里按需深究代码吧!
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
...
}
...
try {
if (mv.getStatus() != null) {
response.setStatus(mv.getStatus().value());
}
//看下面代码块的StaticView的render块
view.render(mv.getModelInternal(), request, response);
}
catch (Exception ex) {
...
}
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
// Load before the main WebMvcAutoConfiguration so that the error View is available
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class, WebMvcProperties.class })
public class ErrorMvcAutoConfiguration {
...
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {
//将创建一个名为error的系统默认异常页面View的Bean
private final StaticView defaultErrorView = new StaticView();
@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
return this.defaultErrorView;
}
// If the user adds @EnableWebMvc then the bean name view resolver from
// WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment.
@Bean
@ConditionalOnMissingBean
public BeanNameViewResolver beanNameViewResolver() {
BeanNameViewResolver resolver = new BeanNameViewResolver();
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
return resolver;
}
}
private static class StaticView implements View {
private static final MediaType TEXT_HTML_UTF8 = new MediaType("text", "html", StandardCharsets.UTF_8);
private static final Log logger = LogFactory.getLog(StaticView.class);
@Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
if (response.isCommitted()) {
String message = getMessage(model);
logger.error(message);
return;
}
response.setContentType(TEXT_HTML_UTF8.toString());
StringBuilder builder = new StringBuilder();
Object timestamp = model.get("timestamp");
Object message = model.get("message");
Object trace = model.get("trace");
if (response.getContentType() == null) {
response.setContentType(getContentType());
}
//系统默认异常页面html代码
builder.append("<html><body><h1>Whitelabel Error Page</h1>").append(
"<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>")
.append("<div id='created'>").append(timestamp).append("</div>")
.append("<div>There was an unexpected error (type=").append(htmlEscape(model.get("error")))
.append(", status=").append(htmlEscape(model.get("status"))).append(").</div>");
if (message != null) {
builder.append("<div>").append(htmlEscape(message)).append("</div>");
}
if (trace != null) {
builder.append("<div style='white-space:pre-wrap;'>").append(htmlEscape(trace)).append("</div>");
}
builder.append("</body></html>");
response.getWriter().append(builder.toString());
}
private String htmlEscape(Object input) {
return (input != null) ? HtmlUtils.htmlEscape(input.toString()) : null;
}
private String getMessage(Map<String, ?> model) {
Object path = model.get("path");
String message = "Cannot render error page for request [" + path + "]";
if (model.get("message") != null) {
message += " and exception [" + model.get("message") + "]";
}
message += " as the response has already been committed.";
message += " As a result, the response may have the wrong status code.";
return message;
}
@Override
public String getContentType() {
return "text/html";
}
}
}
4.【源码流程】几种异常处理原理
- 自定义错误页
- error/404.html error/5xx.html;有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页
@ControllerAdvice+@ExceptionHandler处理全局异常;底层是 ExceptionHandlerExceptionResolver 支持的
@Slf4j @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler({ArithmeticException.class,NullPointerException.class}) //处理异常 public String handleArithException(Exception e){ log.error("异常是:{}",e); return "login"; //视图地址 } }
@ResponseStatus+自定义异常 ;底层是 ResponseStatusExceptionResolver ,把responseStatus注解的信息底层调用 response.sendError(statusCode, resolvedReason),tomcat发送的/error
@ResponseStatus(value= HttpStatus.FORBIDDEN,reason = "用户数量太多") public class UserTooManyException extends RuntimeException { public UserTooManyException(){ } public UserTooManyException(String message){ super(message); } }
```java @Controller public class TableController {
@GetMapping(“/dynamic_table”) public String dynamic_table(@RequestParam(value=”pn”,defaultValue = “1”) Integer pn,Model model){
//表格内容的遍历 List<User> users = Arrays.asList(new User("zhangsan", "123456"), new User("lisi", "123444"), new User("haha", "aaaaa"), new User("hehe ", "aaddd")); model.addAttribute("users",users); if(users.size()>3){ throw new UserTooManyException();//抛出自定义异常 } return "table/dynamic_table";
}
}
- Spring自家异常如 org.springframework.web.bind.MissingServletRequestParameterException,DefaultHandlerExceptionResolver 处理Spring自家异常。
- response.sendError(HttpServletResponse.SC_BAD_REQUEST/*400*/, ex.getMessage());
- 自定义实现 HandlerExceptionResolver 处理异常;可以作为默认的全局异常处理规则
```java
@Order(value= Ordered.HIGHEST_PRECEDENCE) //优先级,数字越小优先级越高
@Component
public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
try {
response.sendError(511,"我喜欢的错误");
} catch (IOException e) {
e.printStackTrace();
}
return new ModelAndView();
}
}
ErrorViewResolver 实现自定义处理异常
- response.sendError(),error请求就会转给controller。
- 你的异常没有任何人能处理,tomcat底层调用response.sendError(),error请求就会转给controller。
basicErrorController 要去的页面地址是 ErrorViewResolver 。 ```java @Controller @RequestMapping(“${server.error.path:${error.path:/error}}”) public class BasicErrorController extends AbstractErrorController {
…
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE) public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = getStatus(request); Map
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); }
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
Map<String, Object> model) {
//这里用到ErrorViewResolver接口 for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model); if (modelAndView != null) { return modelAndView; }
} return null; }
…
}
```java
@FunctionalInterface
public interface ErrorViewResolver {
ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model);
}
十四、原生组件注入
1.原生注解与Spring方式注入
官方文档 - Servlets, Filters, and listeners
1.1 使用原生的注解
@WebServlet(urlPatterns = "/my")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("66666");
}
}
@Slf4j
@WebFilter(urlPatterns={"/css/*","/images/*"}) //my
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("MyFilter初始化完成");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("MyFilter工作");
chain.doFilter(request,response);
}
@Override
public void destroy() {
log.info("MyFilter销毁");
}
}
@Slf4j
@WebListener
public class MyServletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("MySwervletContextListener监听到项目初始化完成");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("MySwervletContextListener监听到项目销毁");
}
}
最后还要在主启动类添加注解@ServletComponentScan
@ServletComponentScan(basePackages = "com.lun")//
@SpringBootApplication(exclude = RedisAutoConfiguration.class)
public class Boot05WebAdminApplication {
public static void main(String[] args) {
SpringApplication.run(Boot05WebAdminApplication.class, args);
}
}
1.2 Spring方式注入
ServletRegistrationBean, FilterRegistrationBean, and ServletListenerRegistrationBean
@Configuration(proxyBeanMethods = true)
public class MyRegistConfig {
@Bean
public ServletRegistrationBean myServlet(){
MyServlet myServlet = new MyServlet();
return new ServletRegistrationBean(myServlet,"/my","/my02");
}
@Bean
public FilterRegistrationBean myFilter(){
MyFilter myFilter = new MyFilter();
// return new FilterRegistrationBean(myFilter,myServlet());
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));
return filterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean myListener(){
MySwervletContextListener mySwervletContextListener = new MySwervletContextListener();
return new ServletListenerRegistrationBean(mySwervletContextListener);
}
}
2.【源码分析】DispatcherServlet注入原理
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration配置类
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
public class DispatcherServletAutoConfiguration {
/*
* The bean name for a DispatcherServlet that will be mapped to the root URL "/"
*/
public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet";
/*
* The bean name for a ServletRegistrationBean for the DispatcherServlet "/"
*/
public static final String DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = "dispatcherServletRegistration";
@Configuration(proxyBeanMethods = false)
@Conditional(DefaultDispatcherServletCondition.class)
@ConditionalOnClass(ServletRegistration.class)
@EnableConfigurationProperties(WebMvcProperties.class)
protected static class DispatcherServletConfiguration {
//创建DispatcherServlet类的Bean
@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails());
return dispatcherServlet;
}
@Bean
@ConditionalOnBean(MultipartResolver.class)
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
public MultipartResolver multipartResolver(MultipartResolver resolver) {
// Detect if the user has created a MultipartResolver but named it incorrectly
return resolver;
}
}
@Configuration(proxyBeanMethods = false)
@Conditional(DispatcherServletRegistrationCondition.class)
@ConditionalOnClass(ServletRegistration.class)
@EnableConfigurationProperties(WebMvcProperties.class)
@Import(DispatcherServletConfiguration.class)
protected static class DispatcherServletRegistrationConfiguration {
//注册DispatcherServlet类
@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet,
WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet,
webMvcProperties.getServlet().getPath());
registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
multipartConfig.ifAvailable(registration::setMultipartConfig);
return registration;
}
}
...
}
DispatcherServlet默认映射的是 / 路径,可以通过在配置文件修改spring.mvc.servlet.path=/mvc。
十五、嵌入式Servlet容器
1.【源码分析】切换web服务器与定制化
1.1 默认支持的WebServer
- Tomcat, Jetty, or Undertow。
ServletWebServerApplicationContext容器启动寻找ServletWebServerFactory 并引导创建服务器。
1.2 原理
SpringBoot应用启动发现当前是Web应用,web场景包-导入tomcat。
- web应用会创建一个web版的IOC容器 ServletWebServerApplicationContext 。
- ServletWebServerApplicationContext 启动的时候寻找 ServletWebServerFactory (Servlet 的web服务器工厂——>Servlet 的web服务器)。
- SpringBoot底层默认有很多的WebServer工厂(ServletWebServerFactoryConfiguration内创建Bean),如:
- TomcatServletWebServerFactory
- JettyServletWebServerFactory
- UndertowServletWebServerFactory
- 底层直接会有一个自动配置类ServletWebServerFactoryAutoConfiguration。
- ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration(配置类)。
- ServletWebServerFactoryConfiguration根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory
- TomcatServletWebServerFactory创建出Tomcat服务器并启动;TomcatWebServer 的构造器拥有初始化方法initialize——this.tomcat.start();
- 内嵌服务器,与以前手动把启动服务器相比,改成现在使用代码启动(tomcat核心jar包存在)。
Spring Boot默认使用Tomcat服务器,若需更改其他服务器,则修改工程pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
1.3 定制Servlet容器
- 实现WebServerFactoryCustomizer
- 把配置文件的值和ServletWebServerFactory进行绑定
- 修改配置文件 server.xxx
- 直接自定义 ConfigurableServletWebServerFactory
xxxxxCustomizer:定制化器,可以改变xxxx的默认规则
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.stereotype.Component;
@Component
public class CustomizationBean implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
@Override
public void customize(ConfigurableServletWebServerFactory server) {
server.setPort(9000);
}
}
十六、定制化原理
1.SpringBoot定制化组件的几种方式(小结)
1.1 定制化的常见方式
1.修改配置文件
2.xxxxxCustomizer
3.编写自定义的配置类 xxxConfiguration + @Bean替换、增加容器中默认组件,视图解析器
3.Web应用 编写一个配置类实现 WebMvcConfigurer 即可定制化web功能 + @Bean给容器中再扩展一些组件
@Configuration
public class AdminWebConfig implements WebMvcConfigurer{
}
@EnableWebMvc + WebMvcConfigurer — @Bean 可以全面接管SpringMVC,所有规则全部自己重新配置; 实现定制和扩展功能(高级功能,初学者退避三舍)。
- 原理:
- WebMvcAutoConfiguration默认的SpringMVC的自动配置功能类,如静态资源、欢迎页等。
- 一旦使用 @EnableWebMvc ,会@Import(DelegatingWebMvcConfiguration.class)。
- DelegatingWebMvcConfiguration的作用,只保证SpringMVC最基本的使用
- 把所有系统中的WebMvcConfigurer拿过来,所有功能的定制都是这些WebMvcConfigurer合起来一起生效。
- 自动配置了一些非常底层的组件,如RequestMappingHandlerMapping,这些组件依赖的组件都是从容器中获取如。
- public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport。
- WebMvcAutoConfiguration里面的配置要能生效必须 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)。
- @EnableWebMvc 导致了WebMvcAutoConfiguration 没有生效。
1.2 原理分析套路
场景starter - xxxxAutoConfiguration - 导入xxx组件 - 绑定xxxProperties - 绑定配置文件项。十七、数据访问
1.数据库场景的自动配置分析与整合测试
1.1 导入JDBC场景
接着导入数据库驱动包(MySQL为例)。 ```xml<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jdbc</artifactId> </dependency>
8.0.22
<a name="JoleQ"></a>
#### 1.2 相关数据源配置类
- DataSourceAutoConfiguration : 数据源的自动配置。
- 修改数据源相关的配置:spring.datasource。
- 数据库连接池的配置,是自己容器中没有DataSource才自动配置的。
- 底层配置好的连接池是:HikariDataSource。
- DataSourceTransactionManagerAutoConfiguration: 事务管理器的自动配置。
- JdbcTemplateAutoConfiguration: JdbcTemplate的自动配置,可以来对数据库进行CRUD。
- 可以修改前缀为spring.jdbc的配置项来修改JdbcTemplate。
- @Bean @Primary JdbcTemplate:Spring容器中有这个JdbcTemplate组件,使用@Autowired。
- JndiDataSourceAutoConfiguration: JNDI的自动配置。
- XADataSourceAutoConfiguration: 分布式事务相关的。
<a name="OnXkd"></a>
#### 1.3 修改配置项
```yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/db_account
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
1.4 单元测试数据源
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
@SpringBootTest
class Boot05WebAdminApplicationTests {
@Autowired
JdbcTemplate jdbcTemplate;
@Test//用@org.junit.Test会报空指针异常,可能跟JUnit新版本有关
void contextLoads() {
// jdbcTemplate.queryForObject("select * from account_tbl")
// jdbcTemplate.queryForList("select * from account_tbl",)
Long aLong = jdbcTemplate.queryForObject("select count(*) from account_tbl", Long.class);
log.info("记录总数:{}",aLong);
}
}
2.自定义方式整合druid数据源
2.1 Druid是什么?
它是数据库连接池,它能够提供强大的监控和扩展功能。
官方文档 - Druid连接池介绍
Spring Boot整合第三方技术的两种方式:
- 自定义
-
2.2 自定义方式
添加依赖:
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.17</version> </dependency>
2.2.1 配置Druid数据源
```java @Configuration public class MyConfig {
@Bean @ConfigurationProperties(“spring.datasource”)//复用配置文件的数据源配置 public DataSource dataSource() throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();
// druidDataSource.setUrl(); // druidDataSource.setUsername(); // druidDataSource.setPassword();
return druidDataSource;
}
}
[更多配置项](https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE)
<a name="k6Ozz"></a>
##### 2.2.2 配置Druid的监控页功能
- Druid内置提供了一个StatViewServlet用于展示Druid的统计信息。[官方文档 - 配置_StatViewServlet配置](https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_StatViewServlet%E9%85%8D%E7%BD%AE)。这个StatViewServlet的用途包括:
- 提供监控信息展示的html页面
- 提供监控信息的JSON API
- Druid内置提供一个StatFilter,用于统计监控信息。[官方文档 - 配置_StatFilter](https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_StatFilter)
- WebStatFilter用于采集web-jdbc关联监控的数据,如SQL监控、URI监控。[官方文档 - 配置_配置WebStatFilter](https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_%E9%85%8D%E7%BD%AEWebStatFilter)
- Druid提供了WallFilter,它是基于SQL语义分析来实现防御SQL注入攻击的。[官方文档 - 配置 wallfilter](https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE-wallfilter)
```java
@Configuration
public class MyConfig {
@Bean
@ConfigurationProperties("spring.datasource")
public DataSource dataSource() throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();
//加入监控和防火墙功能功能
druidDataSource.setFilters("stat,wall");
return druidDataSource;
}
/**
* 配置 druid的监控页功能
* @return
*/
@Bean
public ServletRegistrationBean statViewServlet(){
StatViewServlet statViewServlet = new StatViewServlet();
ServletRegistrationBean<StatViewServlet> registrationBean =
new ServletRegistrationBean<>(statViewServlet, "/druid/*");
//监控页账号密码:
registrationBean.addInitParameter("loginUsername","admin");
registrationBean.addInitParameter("loginPassword","123456");
return registrationBean;
}
/**
* WebStatFilter 用于采集web-jdbc关联监控的数据。
*/
@Bean
public FilterRegistrationBean webStatFilter(){
WebStatFilter webStatFilter = new WebStatFilter();
FilterRegistrationBean<WebStatFilter> filterRegistrationBean = new FilterRegistrationBean<>(webStatFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/*"));
filterRegistrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
}
3.druid数据源starter整合方式
官方文档 - Druid Spring Boot Starter
3.1 引入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>
3.2 分析自动配置
- 扩展配置项 spring.datasource.druid
- 自动配置类DruidDataSourceAutoConfigure
- DruidSpringAopConfiguration.class, 监控SpringBean的;配置项:spring.datasource.druid.aop-patterns
- DruidStatViewServletConfiguration.class, 监控页的配置。spring.datasource.druid.stat-view-servlet默认开启。
- DruidWebStatFilterConfiguration.class,web监控配置。spring.datasource.druid.web-stat-filter默认开启。
DruidFilterConfiguration.class所有Druid的filter的配置:
private static final String FILTER_STAT_PREFIX = "spring.datasource.druid.filter.stat"; private static final String FILTER_CONFIG_PREFIX = "spring.datasource.druid.filter.config"; private static final String FILTER_ENCODING_PREFIX = "spring.datasource.druid.filter.encoding"; private static final String FILTER_SLF4J_PREFIX = "spring.datasource.druid.filter.slf4j"; private static final String FILTER_LOG4J_PREFIX = "spring.datasource.druid.filter.log4j"; private static final String FILTER_LOG4J2_PREFIX = "spring.datasource.druid.filter.log4j2"; private static final String FILTER_COMMONS_LOG_PREFIX = "spring.datasource.druid.filter.commons-log"; private static final String FILTER_WALL_PREFIX = "spring.datasource.druid.filter.wall";
3.3 配置示例
```yaml spring: datasource: url: jdbc:mysql://localhost:3306/db_account username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver
druid:
aop-patterns: com.atguigu.admin.* #监控SpringBean filters: stat,wall # 底层开启功能,stat(sql监控),wall(防火墙) stat-view-servlet: # 配置监控页功能 enabled: true login-username: admin login-password: admin resetEnable: false web-stat-filter: # 监控web enabled: true urlPattern: /* exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
filter:
stat: # 对上面filters里面的stat的详细配置
slow-sql-millis: 1000
logSlowSql: true
enabled: true
wall:
enabled: true
config:
drop-table-allow: false
<a name="Ka2XC"></a>
### 4.整合MyBatis-配置版
[MyBatis的GitHub仓库](https://github.com/mybatis)<br />[MyBatis官方](https://mybatis.org/mybatis-3/zh/index.html)
<a name="p8EcV"></a>
#### 4.1 starter的命名方式
1.SpringBoot官方的Starter:spring-boot-starter-*<br />2.第三方的: *-spring-boot-starter
<a name="Bd7uh"></a>
#### 4.2 引入依赖
```xml
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
4.3 配置模式
- 全局配置文件
- SqlSessionFactory:自动配置好了
- SqlSession:自动配置了SqlSessionTemplate 组合了SqlSession
- @Import(AutoConfiguredMapperScannerRegistrar.class)
- Mapper: 只要我们写的操作MyBatis的接口标准了@Mapper就会被自动扫描进来 ```java @EnableConfigurationProperties(MybatisProperties.class) : MyBatis配置项绑定类。 @AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class }) public class MybatisAutoConfiguration{ … }
@ConfigurationProperties(prefix = “mybatis”) public class MybatisProperties{ … }
<a name="Gf4JB"></a>
#### 4.4 配置文件
```yaml
spring:
datasource:
username: root
password: 1234
url: jdbc:mysql://localhost:3306/my
driver-class-name: com.mysql.jdbc.Driver
# 配置mybatis规则
mybatis:
config-location: classpath:mybatis/mybatis-config.xml #全局配置文件位置
mapper-locations: classpath:mybatis/*.xml #sql映射文件位置
4.5 mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 由于Spring Boot自动配置缘故,此处不必配置,只用来做做样。-->
</configuration>
4.6 Mapper接口
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lun.boot.mapper.UserMapper">
<select id="getUser" resultType="com.lun.boot.bean.User">
select * from user where id=#{id}
</select>
</mapper>
import com.lun.boot.bean.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper {
public User getUser(Integer id);
}
4.7 POJO
public class User {
private Integer id;
private String name;
//getters and setters...
}
4.8 DB
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(45) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
4.9 Controller and Service
@Controller
public class UserController {
@Autowired
private UserService userService;
@ResponseBody
@GetMapping("/user/{id}")
public User getUser(@PathVariable("id") Integer id){
return userService.getUser(id);
}
}
@Service
public class UserService {
@Autowired
private UserMapper userMapper;//IDEA下标红线,可忽视这红线
public User getUser(Integer id){
return userMapper.getUser(id);
}
}
配置private Configuration configuration; 也就是配置mybatis.configuration相关的,就是相当于改mybatis全局配置文件中的值。(也就是说配置了mybatis.configuration,就不需配置mybatis全局配置文件了)
# 配置mybatis规则
mybatis:
mapper-locations: classpath:mybatis/mapper/*.xml
# 可以不写全局配置文件,所有全局配置文件的配置都放在configuration配置项中了。
# config-location: classpath:mybatis/mybatis-config.xml
configuration:
map-underscore-to-camel-case: true
4.10 小结
- 导入MyBatis官方Starter。
- 编写Mapper接口,需@Mapper注解。
- 编写SQL映射文件并绑定Mapper接口。
在application.yaml中指定Mapper配置文件的所处位置,以及指定全局配置文件的信息 (建议:配置在mybatis.configuration)。
5.整合MyBatis-注解配置混合版
你可以通过Spring Initializr添加MyBatis的Starer。
注解与配置混合搭配,干活不累: ```java @Mapper public interface UserMapper { public User getUser(Integer id);@Select(“select * from user where id=#{id}”) public User getUser2(Integer id);
public void saveUser(User user);
@Insert(“insert into user(
name
) values(#{name})”) @Options(useGeneratedKeys = true, keyProperty = “id”) public void saveUser2(User user);
}
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lun.boot.mapper.UserMapper">
<select id="getUser" resultType="com.lun.boot.bean.User">
select * from user where id=#{id}
</select>
<insert id="saveUser" useGeneratedKeys="true" keyProperty="id">
insert into user(`name`) values(#{name})
</insert>
</mapper>
- 简单DAO方法就写在注解上。复杂的就写在配置文件里。
使用@MapperScan(“com.lun.boot.mapper”) 简化,Mapper接口就可以不用标注@Mapper注解。 ```java @MapperScan(“com.lun.boot.mapper”) @SpringBootApplication public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}
<a name="i6mOE"></a>
### 6.整合MyBatisPlus操作数据库
[IDEA的MyBatis的插件 - MyBatisX](https://plugins.jetbrains.com/plugin/10119-mybatisx)<br />[MyBatisPlus官网](https://baomidou.com/)<br />[MyBatisPlus官方文档](https://baomidou.com/guide/)
<a name="YaJ8z"></a>
#### 6.1 MyBatisPlus是什么
MyBatis-Plus(简称 MP)是一个 MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
---
添加依赖:
```xml
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
- MybatisPlusAutoConfiguration配置类,MybatisPlusProperties配置项绑定。
- SqlSessionFactory自动配置好,底层是容器中默认的数据源。
- mapperLocations自动配置好的,有默认值classpath:/mapper/**/.xml,这表示任意包的类路径下的所有mapper文件夹下任意路径下的所有xml都是sql映射文件。 建议以后sql映射文件放在 mapper下。
- 容器中也自动配置好了SqlSessionTemplate。
- @Mapper 标注的接口也会被自动扫描,建议直接 @MapperScan(“com.lun.boot.mapper”)批量扫描。
- MyBatisPlus优点之一:只需要我们的Mapper继承MyBatisPlus的BaseMapper 就可以拥有CRUD能力,减轻开发工作。 ```java import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.lun.hellomybatisplus.model.User;
public interface UserMapper extends BaseMapper
}
<a name="ds6B3"></a>
### 7.CRUD实验-数据列表展示
[官方文档 - CRUD接口](https://baomidou.com/guide/crud-interface.html)<br />使用MyBatis Plus提供的IService,ServiceImpl,减轻Service层开发工作。
```java
import com.lun.hellomybatisplus.model.User;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
/**
* Service 的CRUD也不用写了
*/
public interface UserService extends IService<User> {
//此处故意为空
}
import com.lun.hellomybatisplus.model.User;
import com.lun.hellomybatisplus.mapper.UserMapper;
import com.lun.hellomybatisplus.service.UserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService {
//此处故意为空
}
8.CRUD实验-分页数据展示
9.CRUD实验-删除用户完成
添加分页插件:
@Configuration
public class MyBatisConfig {
/**
* MybatisPlusInterceptor
* @return
*/
@Bean
public MybatisPlusInterceptor paginationInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
// paginationInterceptor.setOverflow(false);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
// paginationInterceptor.setLimit(500);
// 开启 count 的 join 优化,只针对部分 left join
//这是分页拦截器
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
paginationInnerInterceptor.setOverflow(true);
paginationInnerInterceptor.setMaxLimit(500L);
mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor);
return mybatisPlusInterceptor;
}
}
<table class="display table table-bordered table-striped" id="dynamic-table">
<thead>
<tr>
<th>#</th>
<th>name</th>
<th>age</th>
<th>email</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr class="gradeX" th:each="user: ${users.records}">
<td th:text="${user.id}"></td>
<td>[[${user.name}]]</td>
<td th:text="${user.age}">Win 95+</td>
<td th:text="${user.email}">4</td>
<td>
<a th:href="@{/user/delete/{id}(id=${user.id},pn=${users.current})}"
class="btn btn-danger btn-sm" type="button">删除</a>
</td>
</tr>
</tfoot>
</table>
<div class="row-fluid">
<div class="span6">
<div class="dataTables_info" id="dynamic-table_info">
当前第[[${users.current}]]页 总计 [[${users.pages}]]页 共[[${users.total}]]条记录
</div>
</div>
<div class="span6">
<div class="dataTables_paginate paging_bootstrap pagination">
<ul>
<li class="prev disabled"><a href="#">← 前一页</a></li>
<li th:class="${num == users.current?'active':''}"
th:each="num:${#numbers.sequence(1,users.pages)}" >
<a th:href="@{/dynamic_table(pn=${num})}">[[${num}]]</a>
</li>
<li class="next disabled"><a href="#">下一页 → </a></li>
</ul>
</div>
</div>
</div>
numbers表示methods for formatting numeric objects.link
@GetMapping("/user/delete/{id}")
public String deleteUser(@PathVariable("id") Long id,
@RequestParam(value = "pn",defaultValue = "1")Integer pn,
RedirectAttributes ra){
userService.removeById(id);
ra.addAttribute("pn",pn);
return "redirect:/dynamic_table";
}
@GetMapping("/dynamic_table")
public String dynamic_table(@RequestParam(value="pn",defaultValue = "1") Integer pn,Model model){
//表格内容的遍历
//从数据库中查出user表中的用户进行展示
//构造分页参数
Page<User> page = new Page<>(pn, 2);
//调用page进行分页
Page<User> userPage = userService.page(page, null);
model.addAttribute("users",userPage);
return "table/dynamic_table";
}
10.准备阿里云Redis环境
10.1 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--导入jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
- RedisAutoConfiguration自动配置类,RedisProperties 属性类 —> spring.redis.xxx是对redis的配置。
- 连接工厂LettuceConnectionConfiguration、JedisConnectionConfiguration是准备好的。
- 自动注入了RedisTemplate
- 自动注入了StringRedisTemplate,key,value都是String
底层只要我们使用StringRedisTemplate、RedisTemplate就可以操作Redis。
10.2 外网Redis环境搭建
1.阿里云按量付费Redis,其中选择经典网络。
2.申请Redis的公网连接地址。
3.修改白名单,允许0.0.0.0/0访问。11.Redis操作与统计小实验
11.1 相关Redis配置
spring: redis: # url: redis://lfy:Lfy123456@r-bp1nc7reqesxisgxpipd.redis.rds.aliyuncs.com:6379 host: r-bp1nc7reqesxisgxpipd.redis.rds.aliyuncs.com port: 6379 password: lfy:Lfy123456 client-type: jedis jedis: pool: max-active: 10 # lettuce:# 另一个用来连接redis的java框架 # pool: # max-active: 10 # min-idle: 5
11.2 测试Redis连接
```java @SpringBootTest public class Boot05WebAdminApplicationTests {
@Autowired StringRedisTemplate redisTemplate;
@Autowired
RedisConnectionFactory redisConnectionFactory;
@Test
void testRedis(){
ValueOperations<String, String> operations = redisTemplate.opsForValue();
operations.set("hello","world");
String hello = operations.get("hello");
System.out.println(hello);
System.out.println(redisConnectionFactory.getClass());
}
}
Redis Desktop Manager:可视化Redis管理软件。
<a name="KEaLf"></a>
#### 11.3 URL统计拦截器
```java
@Component
public class RedisUrlCountInterceptor implements HandlerInterceptor {
@Autowired
StringRedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
//默认每次访问当前uri就会计数+1
redisTemplate.opsForValue().increment(uri);
return true;
}
}
11.4 注册URL统计拦截器
@Configuration
public class AdminWebConfig implements WebMvcConfigurer{
@Autowired
RedisUrlCountInterceptor redisUrlCountInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(redisUrlCountInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**",
"/js/**","/aa/**");
}
}
Filter、Interceptor 几乎拥有相同的功能?
- Filter是Servlet定义的原生组件,它的好处是脱离Spring应用也能使用。
Interceptor是Spring定义的接口,可以使用Spring的自动装配等功能。
11.5 调用Redis内的统计数据
@Slf4j @Controller public class IndexController { @Autowired StringRedisTemplate redisTemplate; @GetMapping("/main.html") public String mainPage(HttpSession session,Model model){ log.info("当前方法是:{}","mainPage"); ValueOperations<String, String> opsForValue = redisTemplate.opsForValue(); String s = opsForValue.get("/main.html"); String s1 = opsForValue.get("/sql"); model.addAttribute("mainCount",s); model.addAttribute("sqlCount",s1); return "main"; } }
十八、单元测试
1. JUnit5简介
Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库
JUnit 5官方文档
作为最新版本的JUnit框架,JUnit5与之前版本的JUnit框架有很大的不同。由三个不同子项目的几个不同模块组成。
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit VintageJUnit Platform: Junit Platform是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入。
- JUnit Jupiter: JUnit Jupiter提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部包含了一个测试引擎,用于在Junit Platform上运行。
- JUnit Vintage: 由于JUint已经发展多年,为了照顾老的项目,JUnit Vintage提供了兼容JUnit4.x,JUnit3.x的测试引擎。
注意:
- SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容JUnit4需要自行引入(不能使用JUnit4的功能 @Test)
- JUnit 5’s Vintage已经从spring-boot-starter-test从移除。如果需要继续兼容Junit4需要自行引入Vintage依赖:
使用添加JUnit 5,添加对应的starter:<dependency> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-core</artifactId> </exclusion> </exclusions> </dependency>
Spring的JUnit 5的基本单元测试模板(Spring的JUnit4的是@SpringBootTest+@RunWith(SpringRunner.class)): ```java import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test;//注意不是org.junit.Test(这是JUnit4版本的) import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest;<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
@SpringBootTest class SpringBootApplicationTests {
@Autowired
private Component component;
@Test
//@Transactional 标注后连接数据库有回滚功能
public void contextLoads() {
Assertions.assertEquals(5, component.getFive());
}
}
<a name="PSY5J"></a>
### 2. 常用测试注解
[官方文档 - Annotations](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。
- @Timeout:表示测试方法运行如果超过了指定时间将会返回错误。
- @ExtendWith:为测试类或测试方法提供扩展类引用。
```java
import org.junit.jupiter.api.*;
@DisplayName("junit5功能测试类")
public class Junit5Test {
@DisplayName("测试displayname注解")
@Test
void testDisplayName() {
System.out.println(1);
System.out.println(jdbcTemplate);
}
@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
assertTrue(StringUtils.isPalindrome(candidate));
}
@Disabled
@DisplayName("测试方法2")
@Test
void test2() {
System.out.println(2);
}
@RepeatedTest(5)
@Test
void test3() {
System.out.println(5);
}
/**
* 规定方法超时时间。超出时间测试出异常
*
* @throws InterruptedException
*/
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
@Test
void testTimeout() throws InterruptedException {
Thread.sleep(600);
}
@BeforeEach
void testBeforeEach() {
System.out.println("测试就要开始了...");
}
@AfterEach
void testAfterEach() {
System.out.println("测试结束了...");
}
@BeforeAll
static void testBeforeAll() {
System.out.println("所有测试就要开始了...");
}
@AfterAll
static void testAfterAll() {
System.out.println("所有测试以及结束了...");
}
}
3.断言机制
断言Assertion是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是org.junit.jupiter.api.Assertions的静态方法。检查业务逻辑返回的数据是否合理。所有的测试运行结束以后,会有一个详细的测试报告。
JUnit 5 内置的断言可以分成如下几个类别:
3.1 简单断言
用来对单个值进行简单的验证。如:
方法 | 说明 |
---|---|
assertEquals | 判断两个对象或两个原始类型是否相等 |
assertNotEquals | 判断两个对象或两个原始类型是否不相等 |
assertSame | 判断两个对象引用是否指向同一个对象 |
assertNotSame | 判断两个对象引用是否指向不同的对象 |
assertTrue | 判断给定的布尔值是否为 true |
assertFalse | 判断给定的布尔值是否为 false |
assertNull | 判断给定的对象引用是否为 null |
assertNotNull | 判断给定的对象引用是否不为 null |
@Test
@DisplayName("simple assertion")
public void simple() {
assertEquals(3, 1 + 2, "simple math");
assertNotEquals(3, 1 + 1);
assertNotSame(new Object(), new Object());
Object obj = new Object();
assertSame(obj, obj);
assertFalse(1 > 2);
assertTrue(1 < 2);
assertNull(null);
assertNotNull(new Object());
}
3.2 数组断言
通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等。
@Test
@DisplayName("array assertion")
public void array() {
assertArrayEquals(new int[]{1, 2}, new int[] {1, 2});
}
3.3 组合断言
assertAll()方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言。
@Test
@DisplayName("assert all")
public void all() {
assertAll("Math",
() -> assertEquals(2, 1 + 1),
() -> assertTrue(1 > 0)
);
}
3.4 异常断言
在JUnit4时期,想要测试方法的异常情况时,需要用@Rule注解的ExpectedException变量还是比较麻烦的。而JUnit5提供了一种新的断言方式Assertions.assertThrows(),配合函数式编程就可以进行使用。
@Test
@DisplayName("异常测试")
public void exceptionTest() {
ArithmeticException exception = Assertions.assertThrows(
//扔出断言异常
ArithmeticException.class, () -> System.out.println(1 % 0));
}
3.5 超时断言
JUnit5还提供了Assertions.assertTimeout()为测试方法设置了超时时间。
@Test
@DisplayName("超时测试")
public void timeoutTest() {
//如果测试方法时间超过1s将会异常
Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));
}
3.6 快速失败
通过 fail 方法直接使得测试失败。
@Test
@DisplayName("fail")
public void shouldFail() {
fail("This should fail");
}
4.前置条件
JUnit 5 中的前置条件(assumptions【假设】)类似于断言,不同之处在于不满足的断言assertions会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止。
前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。
@DisplayName("前置条件")
public class AssumptionsTest {
private final String environment = "DEV";
@Test
@DisplayName("simple")
public void simpleAssume() {
assumeTrue(Objects.equals(this.environment, "DEV"));
assumeFalse(() -> Objects.equals(this.environment, "PROD"));
}
@Test
@DisplayName("assume then do")
public void assumeThenDo() {
assumingThat(
Objects.equals(this.environment, "DEV"),
() -> System.out.println("In DEV")
);
}
}
assumeTrue 和 assumFalse 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止。
assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试执行并不会终止。
5.嵌套测试
官方文档 - Nested Tests
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());
}
}
}
}
6.参数化测试
官方文档 - Parameterized Tests
参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。
利用@ValueSource等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。
利用@ValueSource等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。
- @ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
- @NullSource: 表示为参数化测试提供一个null的入参
- @EnumSource: 表示为参数化测试提供一个枚举入参
- @CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
- @MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)
当然如果参数化测试仅仅只能做到指定普通的入参还达不到让我觉得惊艳的地步。让我真正感到他的强大之处的地方在于他可以支持外部的各类入参。如:CSV,YML,JSON 文件甚至方法的返回值也可以作为入参。只需要去实现ArgumentsProvider接口,任何外部文件都可以作为它的入参。
@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("方法来源参数")
public void testWithExplicitLocalMethodSource(String name) {
System.out.println(name);
Assertions.assertNotNull(name);
}
static Stream<String> method() {
return Stream.of("apple", "banana");
}
6.1 迁移指南
官方文档 - Migrating from JUnit 4
在进行迁移的时候需要注意如下的变化:
- 注解在 org.junit.jupiter.api 包中,断言在 org.junit.jupiter.api.Assertions 类中,前置条件在 org.junit.jupiter.api.Assumptions 类中。
- 把@Before 和@After 替换成@BeforeEach 和@AfterEach。
- 把@BeforeClass 和@AfterClass 替换成@BeforeAll 和@AfterAll。
- 把@Ignore 替换成@Disabled。
- 把@Category 替换成@Tag。
把@RunWith、@Rule 和@ClassRule 替换成@ExtendWith。
十九、指标监控
1.SpringBoot Actuator与Endpoint
未来每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot就抽取了Actuator场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。
官方文档 - Spring Boot Actuator: Production-ready Features
1.x与2.x的不同:SpringBoot Actuator 1.x
- 支持SpringMVC
- 基于继承方式进行扩展
- 层级Metrics配置
- 自定义Metrics收集
- 默认较少的安全策略
SpringBoot Actuator 2.x
添加依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
暴露所有监控信息为HTTP。
management: endpoints: enabled-by-default: true #暴露所有端点信息 web: exposure: include: '*' #以web方式暴露
测试例子
- http://localhost:8080/actuator/beans
- http://localhost:8080/actuator/configprops
- http://localhost:8080/actuator/metrics
- http://localhost:8080/actuator/metrics/jvm.gc.pause
- http://localhost:8080/actuator/metrics/endpointName/detailPath
2.常使用的端点及开启与禁用
2.1 常使用的端点
| ID | 描述 | | —- | —- | | auditevents | 暴露当前应用程序的审核事件信息。需要一个AuditEventRepository组件。 | | beans | 显示应用程序中所有Spring Bean的完整列表。 | | caches | 暴露可用的缓存。 | | conditions | 显示自动配置的所有条件信息,包括匹配或不匹配的原因。 | | configprops | 显示所有@ConfigurationProperties。 | | env | 暴露Spring的属性ConfigurableEnvironment | | flyway | 显示已应用的所有Flyway数据库迁移。 需要一个或多个Flyway组件。 | | health | 显示应用程序运行状况信息。 | | httptrace | 显示HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应)。需要一个HttpTraceRepository组件。 | | info | 显示应用程序信息。 | | integrationgraph | 显示Spring integrationgraph 。需要依赖spring-integration-core。 | | loggers | 显示和修改应用程序中日志的配置。 | | liquibase | 显示已应用的所有Liquibase数据库迁移。需要一个或多个Liquibase组件。 | | metrics | 显示当前应用程序的“指标”信息。 | | mappings | 显示所有@RequestMapping路径列表。 | | scheduledtasks | 显示应用程序中的计划任务。 | | sessions | 允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用Spring Session的基于Servlet的Web应用程序。 | | shutdown | 使应用程序正常关闭。默认禁用。 | | startup | 显示由ApplicationStartup收集的启动步骤数据。需要使用SpringApplication进行配置BufferingApplicationStartup。 | | threaddump | 执行线程转储。 |
如果您的应用程序是Web应用程序(Spring MVC,Spring WebFlux或Jersey),则可以使用以下附加端点:
ID | 描述 |
---|---|
heapdump | 返回hprof堆转储文件。 |
jolokia | 通过HTTP暴露JMX bean(需要引入Jolokia,不适用于WebFlux)。需要引入依赖jolokia-core。 |
logfile | 返回日志文件的内容(如果已设置logging.file.name或logging.file.path属性)。支持使用HTTPRange标头来检索部分日志文件的内容。 |
prometheus | 以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus。 |
其中最常用的Endpoint:
- Health:监控状况
- Metrics:运行时指标
-
2.2 Health Endpoint
健康检查端点,我们一般用于在云平台,平台会定时的检查应用的健康状况,我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。
重要的几点: health endpoint返回的结果,应该是一系列健康检查后的一个汇总报告。
- 很多的健康检查默认已经自动配置好了,比如:数据库、redis等。
-
2.3 Metrics Endpoint
提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或者push(被动获取)方式得到:
通过Metrics对接多种监控系统。
- 简化核心Metrics开发。
-
2.4 开启与禁用Endpoints
默认所有的Endpoint除过shutdown都是开启的。
需要开启或者禁用某个Endpoint。配置模式为management.endpoint.
.enabled = true management: endpoint: beans: enabled: true
或者禁用所有的Endpoint然后手动开启指定的Endpoint。
management: endpoints: enabled-by-default: false endpoint: beans: enabled: true health: enabled: true
2.5 暴露Endpoints
支持的暴露方式
HTTP:默认只暴露health和info。
- JMX:默认暴露所有Endpoint。
- 除过health和info,剩下的Endpoint都应该进行保护访问。如果引入Spring Security,则会默认配置安全访问规则。 | ID | JMX | Web | | —- | —- | —- | | auditevents | Yes | No | | beans | Yes | No | | caches | Yes | No | | conditions | Yes | No | | configprops | Yes | No | | env | Yes | No | | flyway | Yes | No | | health | Yes | Yes | | heapdump | N/A | No | | httptrace | Yes | No | | info | Yes | Yes | | integrationgraph | Yes | No | | jolokia | N/A | No | | logfile | N/A | No | | loggers | Yes | No | | liquibase | Yes | No | | metrics | Yes | No | | mappings | Yes | No | | prometheus | N/A | No | | scheduledtasks | Yes | No | | sessions | Yes | No | | shutdown | Yes | No | | startup | Yes | No | | threaddump | Yes | No |
若要更改公开的Endpoint,请配置以下的包含和排除属性:
Property | Default |
---|---|
management.endpoints.jmx.exposure.exclude | |
management.endpoints.jmx.exposure.include | * |
management.endpoints.web.exposure.exclude | |
management.endpoints.web.exposure.include | info, health |
3.定制Endpoint
3.1 定制 Health 信息
management:
health:
enabled: true
show-details: always #总是显示详细信息。可显示每个模块的状态信息
通过实现HealthIndicator接口,或继承MyComHealthIndicator类。
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class MyHealthIndicator implements HealthIndicator {
@Override
public Health health() {
int errorCode = check(); // perform some specific health check
if (errorCode != 0) {
return Health.down().withDetail("Error Code", errorCode).build();
}
return Health.up().build();
}
}
/*
构建Health
Health build = Health.down()
.withDetail("msg", "error service")
.withDetail("code", "500")
.withException(new RuntimeException())
.build();
*/
@Component
public class MyComHealthIndicator extends AbstractHealthIndicator {
/**
* 真实的检查方法
* @param builder
* @throws Exception
*/
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
//mongodb。 获取连接进行测试
Map<String,Object> map = new HashMap<>();
// 检查完成
if(1 == 2){
// builder.up(); //健康
builder.status(Status.UP);
map.put("count",1);
map.put("ms",100);
}else {
// builder.down();
builder.status(Status.OUT_OF_SERVICE);
map.put("err","连接超时");
map.put("ms",3000);
}
builder.withDetail("code",100)
.withDetails(map);
}
}
3.2 定制info信息
常用两种方式:
编写配置文件
info: appName: boot-admin version: 2.0.1 mavenProjectName: @project.artifactId@ #使用@@可以获取maven的pom文件值 mavenProjectVersion: @project.version@
编写InfoContributor ```java import java.util.Collections;
import org.springframework.boot.actuate.info.Info; import org.springframework.boot.actuate.info.InfoContributor; import org.springframework.stereotype.Component;
@Component public class ExampleInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("example",
Collections.singletonMap("key", "value"));
}
}
[http://localhost:8080/actuator/info](http://localhost:8080/actuator/info) 会输出以上方式返回的所有info信息
<a name="M5Lz2"></a>
#### 3.3 定制Metrics信息
[Spring Boot支持的metrics](https://docs.spring.io/spring-boot/docs/2.4.2/reference/htmlsingle/#production-ready-metrics-meter)<br />增加定制Metrics:
```java
class MyService{
Counter counter;
public MyService(MeterRegistry meterRegistry){
counter = meterRegistry.counter("myservice.method.running.counter");
}
public void hello() {
counter.increment();
}
}
//也可以使用下面的方式
@Bean
MeterBinder queueSize(Queue queue) {
return (registry) -> Gauge.builder("queueSize", queue::size).register(registry);
}
3.4 定制Endpoint
@Component
@Endpoint(id = "container")
public class DockerEndpoint {
@ReadOperation
public Map getDockerInfo(){
return Collections.singletonMap("info","docker started...");
}
@WriteOperation
private void restartDocker(){
System.out.println("docker restarted....");
}
}
场景:
- 开发ReadinessEndpoint来管理程序是否就绪。
- 开发LivenessEndpoint来管理程序是否存活。
4.Boot Admin Server
官方Github
官方文档
可视化指标监控What is Spring Boot Admin? codecentric’s Spring Boot Admin is a community project to manage and monitor your Spring Boot ® applications. The applications register with our Spring Boot Admin Client (via HTTP) or are discovered using Spring Cloud ® (e.g. Eureka, Consul). The UI is just a Vue.js application on top of the Spring Boot Actuator endpoints.
二十、高级特性
1.Profile环境切换
为了方便多环境适配,Spring Boot简化了profile功能。
- 默认配置文件application.yaml任何时候都会加载。
- 指定环境配置文件application-{env}.yaml,env通常替代为test,
- 激活指定环境
- 配置文件激活:spring.profiles.active=prod
- 命令行激活:java -jar xxx.jar —spring.profiles.active=prod —person.name=haha(修改配置文件的任意值,命令行优先)
- 默认配置与环境配置同时生效
-
1.1 @Profile条件装配功能
@Data @Component @ConfigurationProperties("person")//在配置文件中配置 public class Person{ private String name; private Integer age; }
application.properties
person: name: lun age: 8
```java public interface Person {
String getName(); Integer getAge();
}
@Profile(“test”)//加载application-test.yaml里的 @Component @ConfigurationProperties(“person”) @Data public class Worker implements Person {
private String name;
private Integer age;
}
@Profile(value = {“prod”,”default”})//加载application-prod.yaml里的 @Component @ConfigurationProperties(“person”) @Data public class Boss implements Person {
private String name;
private Integer age;
}
application-test.yaml
```yaml
person:
name: test-张三
server:
port: 7000
application-prod.yaml
person:
name: prod-张三
server:
port: 8000
application.properties
# 激活prod配置文件
spring.profiles.active=prod
@Autowired
private Person person;
@GetMapping("/")
public String hello(){
//激活了prod,则返回Boss;激活了test,则返回Worker
return person.getClass().toString();
}
@Profile还可以修饰在方法上:
class Color {
}
@Configuration
public class MyConfig {
@Profile("prod")
@Bean
public Color red(){
return new Color();
}
@Profile("test")
@Bean
public Color green(){
return new Color();
}
}
可以激活一组:
spring.profiles.active=production
spring.profiles.group.production[0]=proddb
spring.profiles.group.production[1]=prodmq
2.配置加载优先级
2.1 外部化配置
官方文档 - Externalized Configuration
Spring Boot uses a very particular PropertySource order that is designed to allow sensible overriding of values. Properties are considered in the following order (with values from lower items overriding earlier ones)(1优先级最低,14优先级最高):
- Default properties (specified by setting SpringApplication.setDefaultProperties).
- @PropertySource annotations on your @Configuration classes. Please note that such property sources are not added to the Environment until the application context is being refreshed. This is too late to configure certain properties such as logging. and spring.main. which are read before refresh begins.
- Config data (such as application.properties files)
- A RandomValuePropertySource that has properties only in random.*.
- OS environment variables.
- Java System properties (System.getProperties()).
- JNDI attributes from java:comp/env.
- ServletContext init parameters.
- ServletConfig init parameters.
- Properties from SPRING_APPLICATION_JSON (inline JSON embedded in an environment variable or system property).
- Command line arguments.
- properties attribute on your tests. Available on @SpringBootTest and the test annotations for testing a particular slice of your application.
- @TestPropertySource annotations on your tests.
- Devtools global settings properties in the $HOME/.config/spring-boot directory when devtools is active. ```java import org.springframework.stereotype.; import org.springframework.beans.factory.annotation.;
@Component public class MyBean {
@Value("${name}")//以这种方式可以获得配置值
private String name;
// ...
}
---
- 外部配置源
- Java属性文件。
- YAML文件。
- 环境变量。
- 命令行参数。
- 配置文件查找位置
- classpath 根路径。
- classpath 根路径下config目录。
- jar包当前目录。
- jar包当前目录的config目录。
- /config子目录的直接子目录。
- 配置文件加载顺序:
1. 当前jar包内部的application.properties和application.yml。
1. 当前jar包内部的application-{profile}.properties 和 application-{profile}.yml。
1. 引用的外部jar包的application.properties和application.yml。
1. 引用的外部jar包的application-{profile}.properties和application-{profile}.yml。
- 指定环境优先,外部优先,后面的可以覆盖前面的同名配置项。
<a name="qZnXR"></a>
### 3.自定义starter细节
<a name="JkTyO"></a>
#### 3.1 starter启动原理
- starter的pom.xml引入autoconfigure依赖
![image.png](https://cdn.nlark.com/yuque/0/2022/png/26803611/1650190198552-6f09bce5-e754-49b8-a883-4336779bc6f5.png#clientId=u706f1a21-2791-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=74&id=u60015fe6&margin=%5Bobject%20Object%5D&name=image.png&originHeight=74&originWidth=445&originalType=binary&ratio=1&rotation=0&showTitle=false&size=3222&status=done&style=none&taskId=ub0f4f352-b2ea-4e51-bce3-ca81d37e3c5&title=&width=445)
- autoconfigure包中配置使用META-INF/spring.factories中EnableAutoConfiguration的值,使得项目启动加载指定的自动配置类
- 编写自动配置类 xxxAutoConfiguration -> xxxxProperties
- @Configuration
- @Conditional
- @EnableConfigurationProperties
- @Bean
- …
- 引入starter — xxxAutoConfiguration — 容器中放入组件 ---- 绑定xxxProperties ---- 配置项
<a name="ELbXX"></a>
#### 3.2 自定义starter
- 目标:创建HelloService的自定义starter。
- 创建两个工程,分别命名为hello-spring-boot-starter(普通Maven工程),hello-spring-boot-starter-autoconfigure(需用用到Spring Initializr创建的Maven工程)。
- hello-spring-boot-starter无需编写什么代码,只需让该工程引入hello-spring-boot-starter-autoconfigure依赖:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.lun</groupId>
<artifactId>hello-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>com.lun</groupId>
<artifactId>hello-spring-boot-starter-autoconfigure</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
hello-spring-boot-starter-autoconfigure的pom.xml如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.2</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.lun</groupId> <artifactId>hello-spring-boot-starter-autoconfigure</artifactId> <version>1.0.0-SNAPSHOT</version> <name>hello-spring-boot-starter-autoconfigure</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> </dependencies> </project>
创建4个文件:
- com/lun/hello/auto/HelloServiceAutoConfiguration
- com/lun/hello/bean/HelloProperties
- com/lun/hello/service/HelloService
- src/main/resources/META-INF/spring.factories ```java import com.lun.hello.bean.HelloProperties; import com.lun.hello.service.HelloService; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;
@Configuration @ConditionalOnMissingBean(HelloService.class) @EnableConfigurationProperties(HelloProperties.class)//默认HelloProperties放在容器中 public class HelloServiceAutoConfiguration {
@Bean
public HelloService helloService(){
return new HelloService();
}
}
```java
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("hello")
public class HelloProperties {
private String prefix;
private String suffix;
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getSuffix() {
return suffix;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
}
import com.lun.hello.bean.HelloProperties;
import org.springframework.beans.factory.annotation.Autowired;
/**
* 默认不要放在容器中
*/
public class HelloService {
@Autowired
private HelloProperties helloProperties;
public String sayHello(String userName){
return helloProperties.getPrefix() + ": " + userName + " > " + helloProperties.getSuffix();
}
}
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.lun.hello.auto.HelloServiceAutoConfiguration
- 用maven插件,将两工程install到本地。
接下来,测试使用自定义starter,用Spring Initializr创建名为hello-spring-boot-starter-test工程,引入hello-spring-boot-starter依赖,其pom.xml如下: ```xml <?xml version=”1.0” encoding=”UTF-8”?> <project xmlns=”http://maven.apache.org/POM/4.0.0“ xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance“
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0 <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.2</version> <relativePath/> <!-- lookup parent from repository -->
com.lun hello-spring-boot-starter-test 1.0.0-SNAPSHOT hello-spring-boot-starter-test Demo project for Spring Boot <java.version>1.8</java.version>
org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-test test com.lun hello-spring-boot-starter 1.0.0-SNAPSHOT org.springframework.boot spring-boot-maven-plugin
- 添加配置文件application.properties:
```properties
hello.prefix=hello
hello.suffix=666
- 添加单元测试类: ```java import com.lun.hello.service.HelloService;//来自自定义starter import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest class HelloSpringBootStarterTestApplicationTests {
@Autowired
private HelloService helloService;
@Test
void contextLoads() {
// System.out.println(helloService.sayHello("lun"));
Assertions.assertEquals("hello: lun > 666", helloService.sayHello("lun"));
}
}
<a name="KWRul"></a>
## 二十一、原理解析
<a name="vyzXe"></a>
### 1.SpringApplication创建初始化流程
<a name="gzGPU"></a>
#### 1.1 SpringBoot启动过程
Spring Boot应用的启动类:
```java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class HelloSpringBootStarterTestApplication {
public static void main(String[] args) {
SpringApplication.run(HelloSpringBootStarterTestApplication.class, args);
}
}
public class SpringApplication {
...
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[] { primarySource }, args);
}
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
//先看看new SpringApplication(primarySources),下一节再看看run()
public SpringApplication(Class<?>... primarySources) {
this(null, primarySources);
}
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
//WebApplicationType是枚举类,有NONE,SERVLET,REACTIVE,下行webApplicationType是SERVLET
this.webApplicationType = WebApplicationType.deduceFromClasspath();
//初始启动引导器,去spring.factories文件中找org.springframework.boot.Bootstrapper,但我找不到实现Bootstrapper接口的类
this.bootstrappers = new ArrayList<>(getSpringFactoriesInstances(Bootstrapper.class));
//去spring.factories找 ApplicationContextInitializer
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
//去spring.factories找 ApplicationListener
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
private Class<?> 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;
}
...
}
spring.factories:
...
# 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.rsocket.context.RSocketPortInfoApplicationContextInitializer,\
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer
# 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.DelegatingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.env.EnvironmentPostProcessorApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener
...
2.SpringBoot完整启动过程
继续上一节,接着讨论return new SpringApplication(primarySources).run(args)的run方法
public class SpringApplication {
...
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();//开始计时器
stopWatch.start();//开始计时
//1.
//创建引导上下文(Context环境)createBootstrapContext()
//获取到所有之前的 bootstrappers 挨个执行 intitialize() 来完成对引导启动器上下文环境设置
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
//2.到最后该方法会返回这context
ConfigurableApplicationContext context = null;
//3.让当前应用进入headless模式
configureHeadlessProperty();
//4.获取所有 RunListener(运行监听器),为了方便所有Listener进行事件感知
SpringApplicationRunListeners listeners = getRunListeners(args);
//5. 遍历 SpringApplicationRunListener 调用 starting 方法;
// 相当于通知所有感兴趣系统正在启动过程的人,项目正在 starting。
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
//6.保存命令行参数 ApplicationArguments
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
//7.准备环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
configureIgnoreBeanInfo(environment);
/*打印标志
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.4.2)
*/
Banner printedBanner = printBanner(environment);
// 创建IOC容器(createApplicationContext())
// 根据项目类型webApplicationType(NONE,SERVLET,REACTIVE)创建容器,
// 当前会创建 AnnotationConfigServletWebServerApplicationContext
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
//8.准备ApplicationContext IOC容器的基本信息
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
//9.刷新IOC容器,创建容器中的所有组件,Spring框架的内容
refreshContext(context);
//该方法没内容,大概为将来填入
afterRefresh(context, applicationArguments);
stopWatch.stop();//停止计时
if (this.logStartupInfo) {//this.logStartupInfo默认是true
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
//10.
listeners.started(context);
//11.调用所有runners
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
//13.
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
//12.
listeners.running(context);
}
catch (Throwable ex) {
//13.
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}
//1.
private DefaultBootstrapContext createBootstrapContext() {
DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();
this.bootstrappers.forEach((initializer) -> initializer.intitialize(bootstrapContext));
return bootstrapContext;
}
//3.
private void configureHeadlessProperty() {
//this.headless默认为true
System.setProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS,
System.getProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, Boolean.toString(this.headless)));
}
private static final String SYSTEM_PROPERTY_JAVA_AWT_HEADLESS = "java.awt.headless";
//4.
private SpringApplicationRunListeners getRunListeners(String[] args) {
Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
//getSpringFactoriesInstances 去 spring.factories 找 SpringApplicationRunListener
return new SpringApplicationRunListeners(logger,
getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args),
this.applicationStartup);
}
//7.准备环境
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
// Create and configure the environment
//返回或者创建基础环境信息对象,如:StandardServletEnvironment, StandardReactiveWebEnvironment
ConfigurableEnvironment environment = getOrCreateEnvironment();
//配置环境信息对象,读取所有的配置源的配置属性值。
configureEnvironment(environment, applicationArguments.getSourceArgs());
//绑定环境信息
ConfigurationPropertySources.attach(environment);
//7.1 通知所有的监听器当前环境准备完成
listeners.environmentPrepared(bootstrapContext, environment);
DefaultPropertiesPropertySource.moveToEnd(environment);
configureAdditionalProfiles(environment);
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}
//8.
private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, Banner printedBanner) {
//保存环境信息
context.setEnvironment(environment);
//IOC容器的后置处理流程
postProcessApplicationContext(context);
//应用初始化器
applyInitializers(context);
//8.1 遍历所有的 listener 调用 contextPrepared。
//EventPublishRunListenr通知所有的监听器contextPrepared
listeners.contextPrepared(context);
bootstrapContext.close(context);
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// Add boot specific singleton beans
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory)
.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
// Load the sources
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
load(context, sources.toArray(new Object[0]));
//8.2
listeners.contextLoaded(context);
}
//11.调用所有runners
private void callRunners(ApplicationContext context, ApplicationArguments args) {
List<Object> runners = new ArrayList<>();
//获取容器中的 ApplicationRunner
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
//获取容器中的 CommandLineRunner
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
//合并所有runner并且按照@Order进行排序
AnnotationAwareOrderComparator.sort(runners);
//遍历所有的runner。调用 run 方法
for (Object runner : new LinkedHashSet<>(runners)) {
if (runner instanceof ApplicationRunner) {
callRunner((ApplicationRunner) runner, args);
}
if (runner instanceof CommandLineRunner) {
callRunner((CommandLineRunner) runner, args);
}
}
}
//13.
private void handleRunFailure(ConfigurableApplicationContext context, Throwable exception,
SpringApplicationRunListeners listeners) {
try {
try {
handleExitCode(context, exception);
if (listeners != null) {
//14.
listeners.failed(context, exception);
}
}
finally {
reportFailure(getExceptionReporters(context), exception);
if (context != null) {
context.close();
}
}
}
catch (Exception ex) {
logger.warn("Unable to close ApplicationContext", ex);
}
ReflectionUtils.rethrowRuntimeException(exception);
}
...
}
//2. new SpringApplication(primarySources).run(args) 最后返回的接口类型
public interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle, Closeable {
String CONFIG_LOCATION_DELIMITERS = ",; \t\n";
String CONVERSION_SERVICE_BEAN_NAME = "conversionService";
String LOAD_TIME_WEAVER_BEAN_NAME = "loadTimeWeaver";
String ENVIRONMENT_BEAN_NAME = "environment";
String SYSTEM_PROPERTIES_BEAN_NAME = "systemProperties";
String SYSTEM_ENVIRONMENT_BEAN_NAME = "systemEnvironment";
String APPLICATION_STARTUP_BEAN_NAME = "applicationStartup";
String SHUTDOWN_HOOK_THREAD_NAME = "SpringContextShutdownHook";
void setId(String var1);
void setParent(@Nullable ApplicationContext var1);
void setEnvironment(ConfigurableEnvironment var1);
ConfigurableEnvironment getEnvironment();
void setApplicationStartup(ApplicationStartup var1);
ApplicationStartup getApplicationStartup();
void addBeanFactoryPostProcessor(BeanFactoryPostProcessor var1);
void addApplicationListener(ApplicationListener<?> var1);
void setClassLoader(ClassLoader var1);
void addProtocolResolver(ProtocolResolver var1);
void refresh() throws BeansException, IllegalStateException;
void registerShutdownHook();
void close();
boolean isActive();
ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException;
}
#4.
#spring.factories
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener
class SpringApplicationRunListeners {
private final Log log;
private final List<SpringApplicationRunListener> listeners;
private final ApplicationStartup applicationStartup;
SpringApplicationRunListeners(Log log, Collection<? extends SpringApplicationRunListener> listeners,
ApplicationStartup applicationStartup) {
this.log = log;
this.listeners = new ArrayList<>(listeners);
this.applicationStartup = applicationStartup;
}
//5.遍历 SpringApplicationRunListener 调用 starting 方法;
//相当于通知所有感兴趣系统正在启动过程的人,项目正在 starting。
void starting(ConfigurableBootstrapContext bootstrapContext, Class<?> mainApplicationClass) {
doWithListeners("spring.boot.application.starting", (listener) -> listener.starting(bootstrapContext),
(step) -> {
if (mainApplicationClass != null) {
step.tag("mainApplicationClass", mainApplicationClass.getName());
}
});
}
//7.1
void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
doWithListeners("spring.boot.application.environment-prepared",
(listener) -> listener.environmentPrepared(bootstrapContext, environment));
}
//8.1
void contextPrepared(ConfigurableApplicationContext context) {
doWithListeners("spring.boot.application.context-prepared", (listener) -> listener.contextPrepared(context));
}
//8.2
void contextLoaded(ConfigurableApplicationContext context) {
doWithListeners("spring.boot.application.context-loaded", (listener) -> listener.contextLoaded(context));
}
//10.
void started(ConfigurableApplicationContext context) {
doWithListeners("spring.boot.application.started", (listener) -> listener.started(context));
}
//12.
void running(ConfigurableApplicationContext context) {
doWithListeners("spring.boot.application.running", (listener) -> listener.running(context));
}
//14.
void failed(ConfigurableApplicationContext context, Throwable exception) {
doWithListeners("spring.boot.application.failed",
(listener) -> callFailedListener(listener, context, exception), (step) -> {
step.tag("exception", exception.getClass().toString());
step.tag("message", exception.getMessage());
});
}
private void doWithListeners(String stepName, Consumer<SpringApplicationRunListener> listenerAction,
Consumer<StartupStep> stepAction) {
StartupStep step = this.applicationStartup.start(stepName);
this.listeners.forEach(listenerAction);
if (stepAction != null) {
stepAction.accept(step);
}
step.end();
}
...
}
3.自定义事件监听组件
MyApplicationContextInitializer.java
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
public class MyApplicationContextInitializer implements ApplicationContextInitializer {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
System.out.println("MyApplicationContextInitializer ....initialize.... ");
}
}
MyApplicationListener.java
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
public class MyApplicationListener implements ApplicationListener {
@Override
public void onApplicationEvent(ApplicationEvent event) {
System.out.println("MyApplicationListener.....onApplicationEvent...");
}
}
MyApplicationRunner.java
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Order(1)
@Component//放入容器
public class MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("MyApplicationRunner...run...");
}
}
MyCommandLineRunner.java
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 应用启动做一个一次性事情
*/
@Order(2)
@Component//放入容器
public class MyCommandLineRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("MyCommandLineRunner....run....");
}
}
MySpringApplicationRunListener.java
import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
public class MySpringApplicationRunListener implements SpringApplicationRunListener {
private SpringApplication application;
public MySpringApplicationRunListener(SpringApplication application, String[] args){
this.application = application;
}
@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) {
System.out.println("MySpringApplicationRunListener....starting....");
}
@Override
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
System.out.println("MySpringApplicationRunListener....environmentPrepared....");
}
@Override
public void contextPrepared(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener....contextPrepared....");
}
@Override
public void contextLoaded(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener....contextLoaded....");
}
@Override
public void started(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener....started....");
}
@Override
public void running(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener....running....");
}
@Override
public void failed(ConfigurableApplicationContext context, Throwable exception) {
System.out.println("MySpringApplicationRunListener....failed....");
}
}
注册MyApplicationContextInitializer,MyApplicationListener,MySpringApplicationRunListener:
resources / META-INF / spring.factories:
org.springframework.context.ApplicationContextInitializer=\
com.lun.boot.listener.MyApplicationContextInitializer
org.springframework.context.ApplicationListener=\
com.lun.boot.listener.MyApplicationListener
org.springframework.boot.SpringApplicationRunListener=\
com.lun.boot.listener.MySpringApplicationRunListener