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
 16- public 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
 18- public 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
 17- public 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
 11- public 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
 8- public 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
 15- public 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
 7- public class SpringConfig { 
 
 
 public DruidDataSource dataSource(){
 return new DruidDataSource();
 }
 }
 
bean的依赖属性配置管理
- bean在实现对应的业务逻辑时,可能需要开发者提供一些设置值(属性) 
- 先通过yml配置文件,设置bean运行需要使用的配置信息。 - 1 
 2
 3
 4
 5
 6
 7- cartoon: 
 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 
 2- public class CartoonCatAndMouse{ 
 }
- 找出技术X使用过程中的常用配置Y - 1 
 2
 3
 4
 5
 6
 7- cartoon: 
 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
 4- spring: 
 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
 5- IP访问监控 
 +-----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
 15- public 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
 7- public class IpCountService { 
 ...
 
 //当前的request对象的注入工作由使用当前starter的工程提供自动装配
 private HttpServletRequest httpServletRequest;
 ...
 }
- 定义自动配置类:用自动配置实现功能的自动装载,自动配置类需要在spring.factories文件中做配置方可自动运行 - 1 
 2
 3
 4
 5
 6- public 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
 14- public 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
 5- tools: 
 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
 29- public 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
 10- public 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
 66- Springboot30StartupApplication【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
 8- public 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) {
 //自定义事件处理逻辑
 }
 }
 
 
        