SpringBoot(3) 原理篇
springboot本身是为了加速spring程序的开发,springboot基本没有自己的原理层面的设计,是实现方案进行改进
- 自动配置工作流程
- 自定义starter开发
- springboot程序启动流程
自动配置工作流程
Spring中bean的加载方式
- spring管理bean就是由spring维护对象的生命周期
- bean的加载:
- 已知类交给spring管理:给.class
- 已知类名交给spring管理:给类名字符串
- 内部一样,都是通过spring的BeanDefinition对象初始化spring的bean
1 | https://www.bilibili.com/video/BV1P44y1N7QG |
方式一:配置文件+<bean/>
标签
- 提供类名,交给spring管理
- 给出bean的类名,通过反射机制加载成class
1 |
|
配置文件扫描+注解定义bean
方式一要将spring管控的bean全部写在xml文件中
一个类要受到spring管控加载成bean,就在这个类的上面加一个注解并起一个bean的名字(id)——@Component以及衍生注解@Service、@Controller、@Repository
无法在第三方提供的技术源代码中添加上述4个注解,此时@Bean定义在一个方法上方,该方法的返回值交给spring管控。该方法所在的类定义在@Component的类中
1
2
3
public class Cat {
}1
2
3
public class Mouse {
}无法在第三方提供的技术源代码中去添加上述4个注解,当需要加载第三方开发的bean,@Bean定义在一个方法上方,将当前方法的返回值交给spring管控,这个方法所在的类要定义在@Component修饰的类中,有人会说不是@Configuration吗?@Bean与@Component与@configuration
1
2
3
4
5
6
7
8
public class DbConfig {
public DruidDataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
return ds;
}
}以上只是bean的声明,spring没有感知到它们(就像上课回答问题,举手和点名的区别)。通过xml配置,让spring检查一些包,发现对应注解就将对应的类纳入spring管控范围
1
2
3
4
5
6
7
8
9
10
11
12
13
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
">
<!--指定扫描加载bean的位置-->
<context:component-scan base-package="com.itheima.bean,com.itheima.config"/>
</beans>本方法下,没有任何一个地方可以查阅整体信息,只有当程序运行起来才能感知到加载了多少个bean
注解方式声明配置类
使用java类替换掉固定格式的xml配置——SSM主流的开发形式
定义配置类,使用@ComponentScan替代原始xml配置中的包扫描,功能和上述方法基本相同
1
2
3
4
5
6
7
8
public class SpringConfig3 {
public DogFactoryBean dog(){
return new DogFactoryBean();
}
}spring提供了一个接口FactoryBean,也可以用于声明bean,但实现了FactoryBean接口的类造出来的对象不是当前类的对象,而是FactoryBean接口泛型指定类型的对象。如下列,造出来的bean并不是DogFactoryBean,而是Dog——可以在对象初始化前做一些事情(实现了FactoryBean接口的类使用@Bean的形式进行加载)
有三个方法需要重写:
- getObject():返回值作为FactoryBean所生产的对象被加载到容器中
- getObjectType():生产的对象的类型,可以和实现FactoryBean接口时指定的泛型保持一致,或者使用该泛型的子类或实现类
- isSingleton() :工厂生产的对象是否为单例——工厂只能生产一个对象还是可以生产多个对象,默认为单例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class DogFactoryBean implements FactoryBean<Dog> {
public Dog getObject() throws Exception {
Dog d = new Dog();
//.........
return d;
}
public Class<?> getObjectType() {
return Dog.class;
}
public boolean isSingleton() {
return true;
}
}
使用注解@ImportResource,导入XML格式配置的bean(主要用于实现早期xml配置和现在注解配置的统一)
在配置类上直接写上要被融合的xml配置文件名即可,是一种兼容性解决方案
1
2
3
4
public class SpringConfig32 {
}
proxyBeanMethods属性(@Configuration比@Component多了一个名为proxyBeanMethod的属性)
例子中用到@Configuration,当使用AnnotationConfigApplicationContext加载配置类时,配置类可以不添加这个注解
1
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
该注解保障配置类中使用方法创建的bean的唯一性(注解的属性proxyBeanMethods默认为true),且必须通过spring容器对象调用该方法时才保证bean的唯一性
- 此时,容器中加载的对象不再是SpringConfig的对象了,而是SpringConfig的代理对象
- 代理对象对所有打上@Bean的方法的返回值进行控制,如果容器中不存在返回值类型的Bean,则return new Cat()会正常创建一个cat类的对象。如果容器中已经存在了返回值类型的Bean,返回值会被强制修改成容器中的Bean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SpringConfig {
public Cat cat(){
return new Cat();
}
}
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
String[] names = ctx.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
System.out.println("-------------------------");
SpringConfig springConfig = ctx.getBean("springConfig", SpringConfig.class);
System.out.println(springConfig.cat());
System.out.println(springConfig.cat());
System.out.println(springConfig.cat());
}
}
使用@Import注解注册bean
扫描的时候不仅可以加载到要的东西,还有可能加载到其他用不到的bean
使用@Import注解实现精准的bean加载——注解的参数上写入加载的类对应的.class(在配置类上通过@Import注解的方式,将指定类加载到spring容器中)
1
2
3
public class SpringConfig {
}上例中,使用@Import注解导入了其他配置类。配置类都已经被加载到容器中了,那么配置类中打上了@Bean的方法所产生的Bean对象自然也就被加载到容器中。这里其他配置类可以有@Configuration注解,也可以没有
编程形式注册bean
以上是容器启动阶段完成bean的加载,本方法在容器初始化完成后手动加载bean(Bean默认的定义名为类名小写)
可能的问题:如果容器中已经有了某种类型的bean,手工加载时会不会覆盖?会!Spring容器就是一个Map集合,既然是Map集合,后添加的覆盖先添加
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class App {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
//上下文容器对象已经初始化完毕后,手工加载bean
ctx.register(B.class);
}
}
public class App {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
//上下文容器对象已经初始化完毕后,手工加载bean
ctx.registerBean("tom", Cat.class, 0);
ctx.registerBean("tom", Cat.class, 1);
ctx.registerBean("tom", Cat.class, 2);
System.out.println(ctx.getBean(Cat.class));
}
}创建Bean时为这个Bean的一些属性赋值,使用registerBean第三个参数。这个参数是一个可变参数,对应的是Bean的构造器参数
导入实现了ImportSelector接口的类
容器初始化过程中进行控制(上一个方法是容器初始化后实现bean的加载控制)——实现ImportSelector接口的类可以设置加载的bean的全路径类名。该方式在日常开发中基本不会用到,但SpringBoot源码中有很多
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class MyImportSelector implements ImportSelector {
// 哪个类导入了MyImportSelector,annotationMetadata就能拿到关于这个类的基本上所有信息。例如,下面SpringConfig导入了MyImportSelector,则能够拿到SpringConfig的信息,从而根据不同的条件返回不同的Bean到容器中
public String[] selectImports(AnnotationMetadata metadata) {
//各种条件的判定,判定完毕后,决定是否装载指定的bean
boolean flag = metadata.hasAnnotation("org.springframework.context.annotation.Configuration");
if(flag){
return new String[]{"com.itheima.bean.Dog"};
}
return new String[]{"com.itheima.bean.Cat"}; // 要加载到容器中的Bean的全限定名
}
}
public class SpringConfig {
}
// 这里没有打上@configuration,因此Cat类的bean被加载
导入实现了ImportBeanDefinitionRegistrar接口的类
spring中定义了BeanDefinition,是控制bean初始化加载的核心。BeanDefinition接口给出若干种方法控制bean的相关属性
- Bean 的类名
- 设置父 bean 名称、是否为 primary
- Bean 行为配置信息,作用域、自动绑定模式、生命周期回调、延迟加载、初始方法、销毁方法等
- Bean 之间的依赖设置,dependencies
- 构造参数、属性设置(单例还是非单例)
本方法通过定义一个类,实现ImportBeanDefinitionRegistrar接口的方式定义bean,对bean的初始化进行更加细粒度的控制
registerBeanDefinitions方法参数:
第一个参数:和上一节的annotationMetadata作用一样
第二个参数:类型是BeanDefinitionRegistry,是一个关于BeanDefinition的注册表
不仅能根据条件来控制需要加载到容器中的类型,还能直接对一个Bean的BeanDefinition进行修改,把这个Bean加载到容器中
如果配置类中导入了多个ImportBeanDefinitionRegistrar,则后导入的覆盖先导入的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class BookServiceImpl1 implements BookSerivce {
public void check() {
System.out.println("book service 1..");
}
}
public class BookServiceImpl2 implements BookSerivce {
public void check() {
System.out.println("book service 2....");
}
}
public class MyRegistrar implements ImportBeanDefinitionRegistrar {
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// 对名为bookService这个Bean进行覆盖,希望底层使用的实现类是BookServiceImpl2而不是BookServiceImpl1
BeanDefinition beanDefinition =
BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl2.class).getBeanDefinition();
registry.registerBeanDefinition("bookService",beanDefinition);
}
}
导入实现了BeanDefinitionRegistryPostProcessor接口的类
以上bean的加载方式,如果之间存在冲突,则最终BeanDefinitionRegistryPostProcessor裁定(bean定义后处理器)
在所有bean注册都完成时,它最后执行
有两个抽象方法需要实现
1
2
3
4
5
6
7
8
9
10
11public class MyPostProcessor implements BeanDefinitionRegistryPostProcessor {
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
}
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
}第一个方法的作用同ImportBeanDefinitionRegistrar的注册BeanDefinition的参数
1
2
3
4
5
6
7
8public class MyPostProcessor implements BeanDefinitionRegistryPostProcessor {
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
BeanDefinition beanDefinition =
BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl4.class).getBeanDefinition();
registry.registerBeanDefinition("bookService",beanDefinition);
}
}
bean的加载控制
饱和式加载:不管用不用,全部加载
必要式加载:用什么加载什么
在spring容器中,通过判定是否加载了某个类来控制某些bean是否加载——先判断一个类的全路径名是否成功加载,加载成功说明有这个类,那就干某项具体的工作,否则就干别的工作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class MyImportSelector implements ImportSelector {
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
try {
Class<?> clazz = Class.forName("com.itheima.bean.Mouse");
if(clazz != null) {
return new String[]{"com.itheima.bean.Cat"};
}
} catch (ClassNotFoundException e) {
// e.printStackTrace();
return new String[0];
}
return null;
}
}以上操作被封装:
@ConditionalOnClass注解:当虚拟机中加载了com.itheima.bean.Wolf类时加载对应的cat bean
1
2
3
4
5
public Cat tom(){
return new Cat();
}@ConditionalOnMissingClass注解:虚拟机中没有加载指定的类才加载对应的bean
1
2
3
4
5
public Cat tom(){
return new Cat();
}做AND逻辑关系,写2个就是2个条件都成立,写多个就是多个条件都成立。
1
2
3
4
5
6
public Cat tom(){
return new Cat();
}判定当前的容器类型,例如当前容器环境是否是web环境、是否非web环境
1
2
3
4
5
6
7
8
9
10
11
public Cat tom(){
return new Cat();
}
public Cat tom(){
return new Cat();
}判定是否加载了指定名称的bean
1
2
3
4
5
6
7public class SpringConfig {
public DruidDataSource dataSource(){
return new DruidDataSource();
}
}
bean的依赖属性配置管理
bean在实现对应的业务逻辑时,可能需要开发者提供一些设置值(属性)
先通过yml配置文件,设置bean运行需要使用的配置信息。
1
2
3
4
5
6
7cartoon:
cat:
name: "A"
age: 5
mouse:
name: "B"
age: 1定义一个封装属性的专用类,加载配置属性,读取对应前缀相关的属性值
1
2
3
4
5
6
public class CartoonProperties {
private Cat cat;
private Mouse mouse;
}@EnableConfigurationProperties注解设定使用属性类时加载bean——@EnableConfigurationProperties(A.class)的作用就是如果 A 这个类上使用了 @ConfigurationProperties 注解,那么 A 这个类会与 xxx.properties 进行动态绑定,并且会将 A 这个类加入 IOC 容器中,交由 IOC 容器进行管理。有了这个注解,就不需要在 A 上加@Component注解了
1
2
3
4
5
6
public class CartoonCatAndMouse{
private CartoonProperties cartoonProperties;
}
自动配置原理(工作流程)
自动配置:SpringBoot根据开发者的行为(导入了什么类)猜测要做什么事情,把要用的bean提前加载好
大致思想:
- 准备阶段:
- SpringBoot的开发人员收集Spring开发者的编程习惯,整理开发过程每一个程序经常使用的技术列表——技术集A
- 收集技术集A的使用参数,得到开发过程中每一个技术的常用设置,每一个技术对应一个设置集B
- 加载阶段:
- SpringBoot初始化Spring容器基础环境,读取用户的配置信息,加载用户自定义的bean和导入的其他坐标,形成初始化环境
- SpringBoot将技术集A包含的所有技术在SpringBoot启动时默认全部加载——优化:SpringBoot会对技术集A的每一个技术约定启动这个技术对应的条件,按条件加载。根据初始化环境对比技术集A中的加载条件,满足了即可加载
- 有些技术不做配置就无法工作,springboot这些相应技术的最常用设置(设置集B)作为默认值
- springboot开放修改设置集B的接口,由开发者根据需要决定是否覆盖默认配置
- 准备阶段:
具体过程(以一个例子说明):
让技术X具备自动配置的功能,它属于技术集A
1
2public class CartoonCatAndMouse{
}找出技术X使用过程中的常用配置Y
1
2
3
4
5
6
7cartoon:
cat:
name: "A"
age: 5
mouse:
name: "B"
age: 1设计Y对应的yml书写格式,定义一个属性类封装对应的配置属性(同上面的bean依赖属性管理)
1
2
3
4
5
6
public class CartoonProperties {
private Cat cat;
private Mouse mouse;
}做一个配置类,这个类加载的时候初始化对应的功能bean,并且可以加载到对应的配置(可以为该配置类设置激活条件)
1
2
3
4
5
public class CartoonCatAndMouse implements ApplicationContextAware {
private CartoonProperties cartoonProperties;
}让springboot启动的时候自动加载这个类:在配置目录中创建META-INF目录,并创建spring.factories文件,在其中添加设置,说明哪些类要启动自动配置(转了一圈,就是个普通的bean的加载,和最初使用xml格式加载bean几乎没有区别,格式变了)——springboot提前将spring.factories文件写好
1
2
3# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
test.bean.CartoonCatAndMouse
变更(排除)自动配置
方式一:通过yaml配置设置排除指定的自动配置类
1
2
3
4spring:
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration方式二:通过注解参数排除自动配置类
1
方式三:排除坐标(应用面较窄)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!--web起步依赖环境中,排除Tomcat起步依赖,匹配自动配置条件-->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--添加Jetty起步依赖,匹配自动配置条件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
</dependencies>
自定义starter开发
- 通过一个案例,展示自定义starter实现自定义功能的快捷添加
案例:记录系统访客独立IP访问次数
统计网站独立IP访问次数的功能,并将访问信息在后台持续输出——后台每10秒输出一次监控信息(格式:IP+访问次数),统计用户的访问行为
1
2
3
4
5IP访问监控
+-----ip-address-----+--num--+
| 192.168.0.135 | 15 |
| 61.129.65.248 | 20 |
+--------------------+-------+一点分析:
数据如何记录:map类型
统计功能运行位置:每次web请求都进行统计,因此使用拦截器
为统计功能添加配置项:输出频度,输出的数据格式,统计数据的显示模式
输出频度,默认10秒
数据特征:累计数据 / 阶段数据,默认累计数据
输出格式:详细模式 / 简单模式
IP计数功能开发(自定义starter)
实现效果:在现有的项目中导入一个starter,对应的功能就添加上了,删除掉对应的starter,功能就消失了
业务功能类:不需要static,因为类加载成bean后是一个单例对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class IpCountService {
private Map<String,Integer> ipCountMap = new HashMap<String,Integer>();
public void count(){
//每次调用当前操作,就记录当前访问的IP,然后累加访问次数
//1.获取当前操作的IP地址
String ip = null;
//2.根据IP地址从Map取值,并递增
Integer count = ipCountMap.get(ip);
if(count == null){
ipCountMap.put(ip,1);
}else{
ipCountMap.put(ip,count + 1);
}
}
}当前功能最终导入到其他项目中,导入当前功能的项目是一个web项目,可以从容器中直接获取请求对象,因此获取IP地址的操作可以通过自动装配得到请求对象,然后获取对应的访问IP地址
1
2
3
4
5
6
7public class IpCountService {
...
//当前的request对象的注入工作由使用当前starter的工程提供自动装配
private HttpServletRequest httpServletRequest;
...
}定义自动配置类:用自动配置实现功能的自动装载,自动配置类需要在spring.factories文件中做配置方可自动运行
1
2
3
4
5
6public class IpAutoConfiguration {
public IpCountService ipCountService(){
return new IpCountService();
}
}1
2# Auto Configure
cn.itcast.autoconfig.IpAutoConfiguration =原始项目中模拟调用,测试功能
导入当前的starter,在一个controller中测试
1
2
3
4
5<dependency>
<groupId>cn.itcast</groupId>
<artifactId>ip_spring_boot_starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class BookController {
private IpCountService ipCountService;
public R getPage(int currentPage, int pageSize,Book book) {
ipCountService.count();
IPage<Book> page = bookService.getPage(currentPage, pageSize,book);
if( currentPage > page.getPages()){
page = bookService.getPage((int)page.getPages(), pageSize,book);
}
return new R(true, page);
}
}
原始代码修改后,需要重新编译并安装到maven仓库中(先clean再install)
定时任务报表开发
监控信息需要每10秒输出1次,需要使用定时器功能(Spring的task)
开启定时任务功能:设置在自动配置类上。加载自动配置类即启用定时任务
1
2
3
4
5
6
7
public class IpAutoConfiguration {
public IpCountService ipCountService(){
return new IpCountService();
}
}格式化输出:设置定时任务,每5秒运行一次统计数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class IpCountService {
private Map<String,Integer> ipCountMap = new HashMap<String,Integer>();
public void print(){
System.out.println(" IP访问监控");
System.out.println("+-----ip-address-----+--num--+");
for (Map.Entry<String, Integer> entry : ipCountMap.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(String.format("|%18s |%5d |",key,value));
}
System.out.println("+--------------------+-------+");
}
}
yml配置设置参数
通过yml文件设置参数,控制报表的显示格式
参数格式:3个属性,分别用来控制显示周期(cycle),阶段数据是否清空(cycleReset),数据显示格式(model)
1
2
3
4
5tools:
ip:
cycle: 10
cycleReset: false
model: "detail"封装参数的属性类,读取配置参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class IpProperties {
/**
* 日志显示周期
*/
private Long cycle = 5L;
/**
* 是否周期内重置数据
*/
private Boolean cycleReset = false;
/**
* 日志输出模式 detail:详细模式 simple:极简模式
*/
private String model = LogModel.DETAIL.value;
public enum LogModel{
DETAIL("detail"),
SIMPLE("simple");
private String value;
LogModel(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
}加载属性类
1
2
3
4
5
6
7
8
public class IpAutoConfiguration {
public IpCountService ipCountService(){
return new IpCountService();
}
}服务中应用配置的属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29public class IpCountService {
private Map<String,Integer> ipCountMap = new HashMap<String,Integer>();
private IpProperties ipProperties;
public void print(){
if(ipProperties.getModel().equals(IpProperties.LogModel.DETAIL.getValue())){
System.out.println(" IP访问监控");
System.out.println("+-----ip-address-----+--num--+");
for (Map.Entry<String, Integer> entry : ipCountMap.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(String.format("|%18s |%5d |",key,value));
}
System.out.println("+--------------------+-------+");
}else if(ipProperties.getModel().equals(IpProperties.LogModel.SIMPLE.getValue())){
System.out.println(" IP访问监控");
System.out.println("+-----ip-address-----+");
for (String key: ipCountMap.keySet()) {
System.out.println(String.format("|%18s |",key));
}
System.out.println("+--------------------+");
}
//阶段内统计数据归零
if(ipProperties.getCycleReset()){
ipCountMap.clear();
}
}
}
yml配置设置定时器参数
以上方法无法在@Scheduled注解上直接使用配置数据,因此放弃@EnableConfigurationProperties注解对应的功能,改成最原始的bean定义格式
@Scheduled注解使用#{}读取bean属性值
1
2
3
public void print(){
}属性类定义bean并指定bean的访问名称
1
2
3
4
public class IpProperties {
}配置类中,弃用@EnableConfigurationProperties注解对应的功能,改为导入bean的形式加载配置属性类
1
2
3
4
5
6
7
8
9
//@EnableConfigurationProperties(IpProperties.class)
public class IpAutoConfiguration {
public IpCountService ipCountService(){
return new IpCountService();
}
}
拦截器开发
开发拦截器:用自动装配加载统计功能的业务类,并在拦截器中调用对应功能
1
2
3
4
5
6
7
8
9
10public class IpCountInterceptor implements HandlerInterceptor {
private IpCountService ipCountService;
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
ipCountService.count();
return true;
}
}配置mvc拦截器,设置拦截对应的请求路径。此处拦截所有请求,用户可以根据使用需要设置要拦截的请求
1
2
3
4
5
6
7
8
9
10
11
public class SpringMvcConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(ipCountInterceptor()).addPathPatterns("/**");
}
public IpCountInterceptor ipCountInterceptor(){
return new IpCountInterceptor();
}
}
功能性完善——开启yml提示功能
导入下列坐标
1
2
3
4
5<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>编译后,META-INF目录中会生成对应的提示文件,拷贝生成出的文件到自己开发的META-INF目录中,并对其进行编辑
文件内容大致如下。groups定义当前配置的提示信息总体描述,当前配置属于哪一个属性封装类;properties描述当前配置中每一个属性的具体设置,包含名称、类型、描述、默认值等信息
hints属性默认空白,可以参考springboot源码中的制作,设置当前属性封装类专用的提示信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46{
"groups": [
{
"name": "tools.ip",
"type": "cn.itcast.properties.IpProperties",
"sourceType": "cn.itcast.properties.IpProperties"
}
],
"properties": [
{
"name": "tools.ip.cycle",
"type": "java.lang.Long",
"description": "日志显示周期",
"sourceType": "cn.itcast.properties.IpProperties",
"defaultValue": 5
},
{
"name": "tools.ip.cycle-reset",
"type": "java.lang.Boolean",
"description": "是否周期内重置数据",
"sourceType": "cn.itcast.properties.IpProperties",
"defaultValue": false
},
{
"name": "tools.ip.model",
"type": "java.lang.String",
"description": "日志输出模式 detail:详细模式 simple:极简模式",
"sourceType": "cn.itcast.properties.IpProperties"
}
],
"hints": [
{
"name": "tools.ip.model",
"values": [
{
"value": "detail",
"description": "详细模式."
},
{
"value": "simple",
"description": "极简模式."
}
]
}
]
}
SpringBoot程序启动流程解析
启动过程本质上是容器的初始化,之后初始化对应的bean并放入容器
在spring环境中,每个bean的初始化都要开发者自己添加设置。springboot中,自动配置功能使得开发者能提前预设bean的初始化过程
springboot初始化参数根据参数的提供方,划分3个大类:
环境属性(Environment)
系统配置(spring.factories)
参数(Arguments、application.properties)
以下通过代码流向介绍springboot程序启动时每一环节做的具体事情。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66Springboot30StartupApplication【10】->SpringApplication.run(Springboot30StartupApplication.class, args);
SpringApplication【1332】->return run(new Class<?>[] { primarySource }, args);
SpringApplication【1343】->return new SpringApplication(primarySources).run(args);
SpringApplication【1343】->SpringApplication(primarySources)
# 加载各种配置信息,初始化各种配置对象
SpringApplication【266】->this(null, primarySources);
SpringApplication【280】->public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources)
SpringApplication【281】->this.resourceLoader = resourceLoader;
# 初始化资源加载器
SpringApplication【283】->this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
# 初始化配置类的类名信息(格式转换)
SpringApplication【284】->this.webApplicationType = WebApplicationType.deduceFromClasspath();
# 确认当前容器加载的类型
SpringApplication【285】->this.bootstrapRegistryInitializers = getBootstrapRegistryInitializersFromSpringFactories();
# 获取系统配置引导信息
SpringApplication【286】->setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
# 获取ApplicationContextInitializer.class对应的实例
SpringApplication【287】->setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
# 初始化监听器,对初始化过程及运行过程进行干预
SpringApplication【288】->this.mainApplicationClass = deduceMainApplicationClass();
# 初始化了引导类类名信息,备用
SpringApplication【1343】->new SpringApplication(primarySources).run(args)
# 初始化容器,得到ApplicationContext对象
SpringApplication【323】->StopWatch stopWatch = new StopWatch();
# 设置计时器
SpringApplication【324】->stopWatch.start();
# 计时开始
SpringApplication【325】->DefaultBootstrapContext bootstrapContext = createBootstrapContext();
# 系统引导信息对应的上下文对象
SpringApplication【327】->configureHeadlessProperty();
# 模拟输入输出信号,避免出现因缺少外设导致的信号传输失败,进而引发错误(模拟显示器,键盘,鼠标...)
java.awt.headless=true
SpringApplication【328】->SpringApplicationRunListeners listeners = getRunListeners(args);
# 获取当前注册的所有监听器
SpringApplication【329】->listeners.starting(bootstrapContext, this.mainApplicationClass);
# 监听器执行了对应的操作步骤
SpringApplication【331】->ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
# 获取参数
SpringApplication【333】->ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
# 将前期读取的数据加载成了一个环境对象,用来描述信息
SpringApplication【333】->configureIgnoreBeanInfo(environment);
# 做了一个配置,备用
SpringApplication【334】->Banner printedBanner = printBanner(environment);
# 初始化logo
SpringApplication【335】->context = createApplicationContext();
# 创建容器对象,根据前期配置的容器类型进行判定并创建
SpringApplication【363】->context.setApplicationStartup(this.applicationStartup);
# 设置启动模式
SpringApplication【337】->prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
# 对容器进行设置,参数来源于前期的设定
SpringApplication【338】->refreshContext(context);
# 刷新容器环境
SpringApplication【339】->afterRefresh(context, applicationArguments);
# 刷新完毕后做后处理
SpringApplication【340】->stopWatch.stop();
# 计时结束
SpringApplication【341】->if (this.logStartupInfo) {
# 判定是否记录启动时间的日志
SpringApplication【342】-> new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
# 创建日志对应的对象,输出日志信息,包含启动时间
SpringApplication【344】->listeners.started(context);
# 监听器执行了对应的操作步骤
SpringApplication【345】->callRunners(context, applicationArguments);
# 调用运行器
SpringApplication【353】->listeners.running(context);
# 监听器执行了对应的操作步骤干预springboot的启动过程,比如自定义一个数据库环境检测的程序,并将该程序加入springboot的启动流程:
一般的处理方式:设计若干个标准接口,对应程序中的所有标准过程。当想干预某个过程时,实现接口就行
1
2
3
4
5
6
7
8public class Abc implements InitializingBean, DisposableBean {
public void destroy() throws Exception {
//销毁操作
}
public void afterPropertiesSet() throws Exception {
//初始化操作
}
}springboot采用监听器模式,避免实现过多的接口实现一个新的启动过程
- 将自身的启动过程视为一个大的事件,该事件是由若干个小的事件组成
- org.springframework.boot.context.event.ApplicationStartingEvent——应用启动事件,在应用运行但未进行任何处理时,将发送 ApplicationStartingEvent
- org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent——环境准备事件,当Environment被使用,且上下文创建之前,将发送 ApplicationEnvironmentPreparedEvent
- org.springframework.boot.context.event.ApplicationContextInitializedEvent——上下文初始化事件
- org.springframework.boot.context.event.ApplicationPreparedEvent——应用准备事件,在开始刷新之前,bean定义被加载之后发送 ApplicationPreparedEvent
- org.springframework.context.event.ContextRefreshedEvent——上下文刷新事件
- org.springframework.boot.context.event.ApplicationStartedEvent——应用启动完成事件,在上下文刷新之后且所有的应用和命令行运行器被调用之前发送 ApplicationStartedEvent
- org.springframework.boot.context.event.ApplicationReadyEvent——应用准备就绪事件,在应用程序和命令行运行器被调用之后,将发出 ApplicationReadyEvent,用于通知应用已经准备处理请求
- org.springframework.context.event.ContextClosedEvent——上下文关闭事件,对应容器关闭
- 将自身的启动过程视为一个大的事件,该事件是由若干个小的事件组成
当应用启动后走到某一个过程点时,监听器监听到某个事件触发,就会执行对应的事件。除了系统内置的事件处理,用户还可以根据需要自定义开发当前事件触发时要做的其他动作。
1
2
3
4
5
6//设定监听器,在应用启动开始事件时进行功能追加
public class MyListener implements ApplicationListener<ApplicationStartingEvent> {
public void onApplicationEvent(ApplicationStartingEvent event) {
//自定义事件处理逻辑
}
}