SpringBoot(3) 原理篇

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的加载方式

方式一:配置文件+<bean/>标签

  • 提供类名,交给spring管理
  • 给出bean的类名,通过反射机制加载成class
1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--xml方式声明自己开发的bean-->
<bean id="cat" class="Cat"/>
<bean class="Dog"/>

<!--xml方式声明第三方开发的bean-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"/>
<bean class="com.alibaba.druid.pool.DruidDataSource"/>
<bean class="com.alibaba.druid.pool.DruidDataSource"/>
</beans>

配置文件扫描+注解定义bean

  • 方式一要将spring管控的bean全部写在xml文件中

  • 一个类要受到spring管控加载成bean,就在这个类的上面加一个注解并起一个bean的名字(id)——@Component以及衍生注解@Service、@Controller、@Repository

  • 无法在第三方提供的技术源代码中添加上述4个注解,此时@Bean定义在一个方法上方,该方法的返回值交给spring管控。该方法所在的类定义在@Component的类中

    1
    2
    3
    @Component("tom")
    public class Cat {
    }
    1
    2
    3
    @Service
    public class Mouse {
    }
  • 无法在第三方提供的技术源代码中去添加上述4个注解,当需要加载第三方开发的bean,@Bean定义在一个方法上方,将当前方法的返回值交给spring管控,这个方法所在的类要定义在@Component修饰的类中,有人会说不是@Configuration吗?@Bean与@Component与@configuration

    1
    2
    3
    4
    5
    6
    7
    8
    @Component
    public class DbConfig {
    @Bean
    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
    <?xml version="1.0" encoding="UTF-8"?>
    <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
    @Configuration
    @ComponentScan({"com.itheima.bean","com.itheima.config"})
    public class SpringConfig3 {
    @Bean
    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> {
      @Override
      public Dog getObject() throws Exception {
      Dog d = new Dog();
      //.........
      return d;
      }
      @Override
      public Class<?> getObjectType() {
      return Dog.class;
      }
      @Override
      public boolean isSingleton() {
      return true;
      }
      }
  • 使用注解@ImportResource,导入XML格式配置的bean(主要用于实现早期xml配置和现在注解配置的统一)

    • 在配置类上直接写上要被融合的xml配置文件名即可,是一种兼容性解决方案

      1
      2
      3
      4
      @Configuration
      @ImportResource("applicationContext1.xml")
      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
    @Configuration(proxyBeanMethods = true)
    public class SpringConfig {
    @Bean
    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
    @Import({Dog.class, DbConfig.class})
    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到容器中
    @Override
    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的全限定名
    }
    }

    @Import(MyImportSelector.class)
    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
    @Service("bookService")
    public class BookServiceImpl1 implements BookSerivce {
    @Override
    public void check() {
    System.out.println("book service 1..");
    }
    }

    public class BookServiceImpl2 implements BookSerivce {
    @Override
    public void check() {
    System.out.println("book service 2....");
    }
    }

    public class MyRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    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 {
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {

    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

    }
    }
  • 第一个方法的作用同ImportBeanDefinitionRegistrar的注册BeanDefinition的参数

    1
    2
    3
    4
    5
    6
    7
    8
    public class MyPostProcessor implements BeanDefinitionRegistryPostProcessor {
    @Override
    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 {
    @Override
    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
      @Bean
      @ConditionalOnClass(name = "com.itheima.bean.Wolf")
      public Cat tom(){
      return new Cat();
      }
    • @ConditionalOnMissingClass注解:虚拟机中没有加载指定的类才加载对应的bean

      1
      2
      3
      4
      5
      @Bean
      @ConditionalOnMissingClass("com.itheima.bean.Dog")
      public Cat tom(){
      return new Cat();
      }
    • 做AND逻辑关系,写2个就是2个条件都成立,写多个就是多个条件都成立。

      1
      2
      3
      4
      5
      6
      @Bean
      @ConditionalOnClass(name = "com.itheima.bean.Wolf")
      @ConditionalOnMissingClass("com.itheima.bean.Mouse")
      public Cat tom(){
      return new Cat();
      }
    • 判定当前的容器类型,例如当前容器环境是否是web环境、是否非web环境

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      @Bean
      @ConditionalOnWebApplication
      public Cat tom(){
      return new Cat();
      }

      @Bean
      @ConditionalOnNotWebApplication
      public Cat tom(){
      return new Cat();
      }
    • 判定是否加载了指定名称的bean

      1
      2
      3
      4
      5
      6
      7
      public class SpringConfig {
      @Bean
      @ConditionalOnClass(name="com.mysql.jdbc.Driver")
      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
    @ConfigurationProperties(prefix = "cartoon")
    @Data
    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
    @Component
    @EnableConfigurationProperties(CartoonProperties.class)
    public class CartoonCatAndMouse{
    @Autowired
    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
      @ConfigurationProperties(prefix = "cartoon")
      @Data
      public class CartoonProperties {
      private Cat cat;
      private Mouse mouse;
      }
    • 做一个配置类,这个类加载的时候初始化对应的功能bean,并且可以加载到对应的配置(可以为该配置类设置激活条件)

      1
      2
      3
      4
      5
      @ConditionalOnClass(name="org.springframework.data.redis.core.RedisOperations")
      @EnableConfigurationProperties(CartoonProperties.class)
      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
    @EnableAutoConfiguration(excludeName = "",exclude = {})
  • 方式三:排除坐标(应用面较窄)

    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 {
    ...
    @Autowired
    //当前的request对象的注入工作由使用当前starter的工程提供自动装配
    private HttpServletRequest httpServletRequest;
    ...
    }
  • 定义自动配置类:用自动配置实现功能的自动装载,自动配置类需要在spring.factories文件中做配置方可自动运行

    1
    2
    3
    4
    5
    6
    public class IpAutoConfiguration {
    @Bean
    public IpCountService ipCountService(){
    return new IpCountService();
    }
    }
    1
    2
    # Auto Configure
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=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
      @RestController
      @RequestMapping("/books")
      public class BookController {
      @Autowired
      private IpCountService ipCountService;
      @GetMapping("{currentPage}/{pageSize}")
      public R getPage(@PathVariable int currentPage,@PathVariable 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
    @EnableScheduling
    public class IpAutoConfiguration {
    @Bean
    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>();
    @Scheduled(cron = "0/5 * * * * ?")
    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
    @ConfigurationProperties(prefix = "tools.ip")
    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
    @EnableScheduling
    @EnableConfigurationProperties(IpProperties.class)
    public class IpAutoConfiguration {
    @Bean
    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>();
    @Autowired
    private IpProperties ipProperties;
    @Scheduled(cron = "0/5 * * * * ?")
    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
    @Scheduled(cron = "0/#{ipProperties.cycle} * * * * ?")
    public void print(){
    }
  • 属性类定义bean并指定bean的访问名称

    1
    2
    3
    4
    @Component("ipProperties")
    @ConfigurationProperties(prefix = "tools.ip")
    public class IpProperties {
    }
  • 配置类中,弃用@EnableConfigurationProperties注解对应的功能,改为导入bean的形式加载配置属性类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @EnableScheduling
    //@EnableConfigurationProperties(IpProperties.class)
    @Import(IpProperties.class)
    public class IpAutoConfiguration {
    @Bean
    public IpCountService ipCountService(){
    return new IpCountService();
    }
    }

拦截器开发

  • 开发拦截器:用自动装配加载统计功能的业务类,并在拦截器中调用对应功能

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class IpCountInterceptor implements HandlerInterceptor {
    @Autowired
    private IpCountService ipCountService;
    @Override
    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
    @Configuration
    public class SpringMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(ipCountInterceptor()).addPathPatterns("/**");
    }
    @Bean
    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) {
      //自定义事件处理逻辑
      }
      }