MENU

SpringBoot高级原理分析(未完)

February 18, 2020 • Read: 3750 • 学习记录

SpringBoot高级原理分析

一、SpringBoot自动配置--注解说明

1.1 Condition条件判断

Condition(条件):Condition是在Spring4.0增加的条件判断功能,通
过这个可以功能可以实现选择性的创建Bean操作。
1.1.1 Condition案例1

需求:
在 Spring 的 IOC 容器中有一个 User 的 Bean,现要求:

  1. 导入Jedis坐标后,加载该Bean,没导入,则不加载。
  2. 将类的判断定义为动态的。判断哪个字节码文件存在可以动态指定。
  • ClassCondition类

    public class ClassCondition implements Condition {
    
        @Override
        public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
            //返回是false,bean不加载
            //返回是true,bean加载
    
            boolean flag = true;
            try {
                    Class.forName("redis.clients.jedis.Jedis");
                }
            } catch (ClassNotFoundException e) {
                flag = false;
            }
            return flag;
        }
    }
  • UserConfig配置类

    @Configuration
    public class UserConfig {
        /**
         * 将User实体类放入bean工厂
         * 1.如果没有条件,User对象放入Bean里
         * 2.加入condition条件
         *      @Conditional注解,需要导入Condition实现类
         */
        @Bean
        @Conditional(ClassCondition.class)
        public User user(){
            return new User();
        }
    }
1.1.2 Condition案例2

​ 问题:现在的 Class.forName("redis.clients.jedis.Jedis") 这个是写死的,是否可以动态的加载呢?

  • MyConditionalOnClass注解类

    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Conditional(ClassCondition.class)
    public @interface MyConditionalOnClass {
        String[] value();
    
    }
  • ClassCondition类

    public class ClassCondition implements Condition {
    
        /**
         *
         * @param conditionContext 上下文对象,获取Bean工厂、ClassLoader
         * @param annotatedTypeMetadata 注解元对象,获取注解信息
         * @return
         */
        @Override
        public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
            //返回是false,bean不加载
            //返回是true,bean加载
            
            //获取注解信息
            Map<String, Object> map = annotatedTypeMetadata.getAnnotationAttributes(MyConditionalOnClass.class.getName());
            //map = {value=[redis.clients.jedis.Jedis]}
            System.out.println(map);
            boolean flag = true;
            try {
                String[] strings = (String[]) map.get("value");
                for (String string : strings) {
                    Class.forName(string);
                }
    
            } catch (ClassNotFoundException e) {
                flag = false;
            }
            return flag;
        }
    }
  • UserConfig配置类

    @Configuration
    public class UserConfig {
        /**
         * 将User实体类放入bean工厂
         * 1.如果没有条件,User对象放入Bean里
         * 2.加入condition条件
         *      @Conditional注解,需要导入Condition实现类
         * 3.不应该写死"redis.clients.jedis.Jedis",在Conditional注解导入
         */
        @Bean
        //@Conditional(ClassCondition.class)
        @MyConditionalOnClass("redis.clients.jedis.Jedis")
        //这个条件代表配置文件中要有(itcast=itheima)这个键值对
        @ConditionalOnProperty(name = "itcast", havingValue = "itheima")
        public User user(){
            return new User();
        }
    }
    
  • SpringbootConditionApplication类

    @SpringBootApplication
    public class SpringbootConditionApplication {
    
        public static void main(String[] args) {
            // 启动springboot,返回spring的IOC容器
            ConfigurableApplicationContext context =
                    SpringApplication.run(SpringbootConditionApplication.class, args);
    
    //        Object redisTemplate = context.getBean("redisTemplate");
    //        System.out.println(redisTemplate);
            // 这个类似于springboot的加载机制,只要jedis坐标引入,就加载User类。
            // springboot的加载机制,只要jedis坐标引入,就加载jedis类  
            Object user = context.getBean("user");
            System.out.println(user);
        }
    
    }
1.1.3 @Condition*注解小结

​ springboot提供的,作为条件加载Bean

  • 自定义条件

    • 自定义条件类:自定义类实现Condition接口,重写 matches 方法,在matches 方法中进行逻辑判断,返回boolean值 。
      matches 方法两个参数:

      • context:上下文对象,可以获取属性值,获取类加载器,获取FactoryBean等。
      • metadata:元数据对象,用于获取注解属性。
    • 判断条件:在初始化Bean时,使用@Conditional(条件类.class)注解。
  • SpringBoot常用条件注解:

    • ConditionalOnProperty:判断配置文件中是否有对应的属性和值才初始化Bean。
    • ConditionalOnClass:判断环境中是否有对应的字节码文件才初始化Bean。
    • ConditionalOnMissingBean:判断环境中是否有对应的Bean才初始化Bean。

1.2 SpringBoot切换内置服务器

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <groupId>org.springframework.boot</groupId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <artifactId>spring-boot-starter-jetty</artifactId>
    <groupId>org.springframework.boot</groupId>
</dependency>

1.3 @Enable*注解

SpringBoot中提供了很多Enable开头的注解,这些注解都是用于动态启用某些功能的。而其底层原理是使用@Import注解导入一些配置类,实现Bean的动态加载。

问题:SpringBoot 工程是否可以直接获取jar包中定义的Bean?

1.3.1、创建两个模块

一个是springboot-enable,一个是springboot-enable-other。然后在springboot-enable中导入springboot-enable-other坐标,实现导入其模块的Bean

1.3.2、创建实体类与配置类
// User.java
public class User {
}
// UserConfig.java
@Configuration
public class UserConfig {

    @Bean
    public User user(){
        return new User();
    }
}
1.3.3 导入jar包中Bean的三种方式
  • 方式一:用ComponentScan注解,增加扫描包范围
  • 方式二:使用@Import注解
  • 方式三:使用Enable注解,对@Import进行封装

    // 在springboot-enable-other工程内定义
    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Import(UserConfig.class)
    public @interface EnableUser {
    }

springboot-enable的启动类

/**
 * ComponentScan:扫描包,当前包及其子包
 * com.itheima.enable
 * com.itheima.enableother.config
 *
 * 方式1.使用ComponentScan,扫描com.itheima.enableother.config
 * 方式2.使用import注解,导入配置类
 * 方式3.使用Enable注解
 */

@SpringBootApplication
//@ComponentScan("com.itheima.enableother.config")
//@Import(UserConfig.class)
@EnableUser
public class SpringbootEnableApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);

        Object user = context.getBean("user");
        System.out.println(user);
    }

}

image-20200216162621126.png

springboot导入第三方Bean

@SpringBootApplication ===>
@EnableAutoConfiguration ===>
@Import(AutoConfigurationImportSelector.class) ===>
AutoConfigurationImportSelector.java类 ===>
里面的getCandidateConfigurations()方法中的 META-INF/spring.factories 配置文件

1.4 @Import注解

@Enable*底层依赖于@Import注解导入一些类,使用@Import导入的类会被Spring加载到IOC容器中。而@Import提供4中用法:

  • 导入Bean
  • 导入配置类
  • 导入ImportSelector实现类,一般用于加载配置文件中的类(springboot使用的方式)
  • 导入ImportBeanDefinitionRegistrar实现类
1.4.1 导入Bean

SpringbootEnableApplication.java

@SpringBootApplication
@Import(User.class)
public class SpringbootEnableApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
// 这样导入不了的原因在于,我们是通过Bean的名称"user"来获取对象,而导入的这个User.class不一定叫"user"这个名字,所以需要修改启动类,通过类的类型来获取。
        Object user = context.getBean("user");
        System.out.println(user); // 这样导入不了
        
        User user = context.getBean(User.class);
        System.out.println(user);
    }

}

如果想要获取Bean的名称,那么可以使用context.getBeansOfType

//这是得到Import导入Bean的名称
Map<String, User> map = context.getBeansOfType(User.class);
// {com.itheima.enableother.domain.User=com.itheima.enableother.domain.User@c7045b9}
System.out.println(map);
//Import导入Bean的名称 = "com.itheima.enableother.domain.User"
Object bean = context.getBean("com.itheima.enableother.domain.User");
System.out.println(bean);
1.4.2 导入配置类

​ 配置类就是前面的UserConfig.java类

  • SpringbootEnableApplication.java

    @SpringBootApplication
    @Import(UserConfig.class)
    public class SpringbootEnableApplication {
    
        public static void main(String[] args) {
            ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
            Object user = context.getBean("user");
            System.out.println(user); 
        }
    
    }
1.4.3 实现ImportSelector接口

MyImportSelector类

public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        //这里也可以通过配置文件,然后转换为String[]数组返回
        return new String[]{"com.itheima.enableother.domain.User",
                "com.itheima.enableother.domain.Role"};
    }
}

SpringbootEnableApplication.java

@SpringBootApplication
@Import(MyImportSelector.class)
public class SpringbootEnableApplication {
    ......
}
1.4.4 实现ImportBeanDefinitionRegistrar接口

MyImportBeanDefinitionRegistrar类

public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {


    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();
        registry.registerBeanDefinition("user", beanDefinition);
    }
}
@SpringBootApplication
@Import(MyImportBeanDefinitionRegistrar.class)
public class SpringbootEnableApplication {
    ......
}

1.5 @EnableAutoConfiguration

@SpringBootApplication中的@EnableAutoConfiguration注解,也是通过@Import({AutoConfigurationImportSelector.class})来实现类的加载,那么说明Springboot中也是使用第三种方法导入类。

image-20200216192115160.png

配置文件位置:META-INF/spring.factories,该配置文件中定义了大量的配置类,当 SpringBoot 应用启动时,会自动加载这些配置类,初始化Bean。
并不是所有的Bean都会被初始化,在配置类中使用Condition来加载满足条件的Bean。

二、SpringBoot自动配置--自定义Starter

2.1 MyBatis加载过程

image-20200217105951170.png

2.2、自定义Starter需求

自定义redis-starter。要求当导入redis坐标时,SpringBoot自动创建Jedis的Bean。
步骤:
    创建 redis-spring-boot-autoconfigure 模块
    创建 redis-spring-boot-starter 模块,依赖 redis-spring-bootautoconfigure的模块
    在 redis-spring-boot-autoconfigure 模块中初始化 Jedis 的 Bean。并定义META-INF/spring.factories 文件
    在测试模块中引入自定义的 redis-starter 依赖,测试获取 Jedis 的Bean,操作 redis。

2.3 创建两个模块

​ redis-spring-boot-autoconfigure
​ redis-spring-boot-starter

  • 在starter模块引入autoconfigure模块坐标

    <dependency>
        <groupId>com.itheima</groupId>
        <artifactId>redis-spring-bootautoconfigure</
            artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
  • 在autoconfigure模块引入jedis

    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
    </dependency>

2.4 创建相关配置类

  • 在autoconfigure模块创建RedisAutoConfigure类

    RedisAutoConfigure类

    @Configuration
    @ConditionalOnClass(Jedis.class)
    @EnableConfigurationProperties(RedisProperties.class)
    public class RedisAutoConfigure {
    
        @Bean
        @ConditionalOnMissingBean(name = "jedis") //检查是否存在这个名称=redis的Bean,存在就不加载
        public Jedis jedis(RedisProperties redisProperties){
            System.out.println("RedisAutoConfigure.....jedis");
            return new Jedis(redisProperties.getHost(), redisProperties.getPort());
        }
    }
  • 在autoconfigure模块创建RedisProperties类

    RedisProperties类

    @ConfigurationProperties(prefix = "redis")
    public class RedisProperties {
        private String host = "localhost";
        private int port = 6379;
    
        public String getHost() {
            return host;
        }
    
        public void setHost(String host) {
            this.host = host;
        }
    
        public int getPort() {
            return port;
        }
    
        public void setPort(int port) {
            this.port = port;
        }
    }

2.5 创建META-INF/spring.factories

​ 在autoconfigure模块的resources目录下创建

spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.itheima.autoconfigure.RedisAutoConfigure

2.6 利用前面的enable模块

在enable模块导入starter模块坐标,然后在SpringbootEnableApplication类中测试

@SpringBootApplication
public class SpringbootEnableApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
        
Jedis jedis = (Jedis)context.getBean("jedis");
        System.out.println(jedis);

        jedis.set("mykey", "itheima");
        String value = jedis.get("mykey");
        System.out.println(value);

    }
//    这个是验证@ConditionalOnMissBean注解
    /*@Bean
    public Jedis jedis(){
        return new Jedis("localhost", 6379);
    }*/

}

然后测试成功

2.7 autoconfigure模块的内容

image-20200217193517019.png

2.8 总结自定义的流程

​ 1. enable模块导入自定义stater模块坐标,在SpringbootEnableApplication类中测试 ===> 在自中定义stater模块导入自定义autoconfigure模块坐标 ===> 然后在自定义autoconfigure模块中创建RedisConfigure类和RedisProperties类,最后在resources目录下建立META-INF/spring.factories配置文件

​ 2. enable模块的@SpringBootApplication注解 ==> @EnableAutoConfiguration注解 ==> @Import(AutoConfigurationImportSelector.class)注解 ==> AutoConfigurationImportSelector.java类,这里面的selectImports()方法,能够得到autoconfigure模块中的spring.factories中的

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.itheima.autoconfigure.RedisAutoConfigure

值,然后RedisAutoConfigure,java就被加入spring容器管理,对应的Jedis也被加入spring容器

三、SpringBoot监听机制(有时间就补充)

如果有时间后面就继续来补充,可能好久都没时间::quyin:cry::

3.1 JAVA的监听机制

SpringBoot 的监听机制,其实是对Java提供的事件监听机制的封装。
Java中的事件监听机制定义了以下几个角色:
①事件:Event,继承 java.util.EventObject 类的对象
②事件源:Source ,任意对象Object
③监听器:Listener,实现 java.util.EventListener 接口的对象

3.2 Springboot监听器

SpringBoot 在项目启动时,会对几个监听器进行回调,我们可以实现这些监听器接口,在项目启动时完成一些操作。

一共有四种实现方法:

  • ApplicationContextInitializer
  • SpringApplicationRunListener

spring只加载下面两个

  • CommandLineRunner
  • ApplicationRunner

四、SpringBoot监控(有时间就补充)

五、SpringBoot运行流程分析

SpringBoot启动流程.png