天天看点

7、SpringBoot自动配置原理Condition(详细源码分析)前言1、案例演示2、什么是Condition3、终极原理分析

目录

前言

1、案例演示

情况一:POM依赖不导入Redis的依赖坐标

情况二:POM依赖导入Redis依赖的坐标

2、什么是Condition

案例一

1、没有Condition条件判断的代码

1、Bean对象User.java

2、自定义配置UserConfig.java

3、启动类

2、加入Condition条件判断的代码

1、新建ClassCondition.java条件类

2、修改自定义配置类UserConfig.java

3、不导入Jedis坐标

4、导入Jedis坐标

5、分析源码原理

案例二

1、新建自定义ConditionOnClass.java注解类

2、修改ClassCondition.java条件类

3、修改自定义配置类UserConfig.java

3、终极原理分析

1、修改自定义配置类UserConfig.java

2、添加配置文件application.yml

前言

        SpringBoot在启动的时候会动态的为我们自动配置Spring环境,比如IOC容器准备,会注入所需要的Bean等等,但是SpringBoot怎么知道我们需要哪些呢,原理是根据我们项目中导入的起步依赖,以及配置文件信息进行判断,最终决定项目需要哪些准备,只是语言描述还是有点笼统,那么今天我就从源码分析的角度,纯手写Condition判断器的方式给大家演示一下。

1、案例演示

情况一:POM依赖不导入Redis的依赖坐标

启动类如下:

package com;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class App {
    public static void main(String[] args) {
        //接收启动类run()方法的返回值,其实就是一个Spring的IOC容器
        ConfigurableApplicationContext context = SpringApplication.run(App.class, args);
        //获取名为redisTemplate的Bean
        Object redisTemplate = context.getBean("redisTemplate");
        System.out.println(redisTemplate);
    }
}
           

 启动类源码分析:

        查看源码,得知SpringApplication.run(App.class, args);实际上是有返回值的,返回值本质就是Spring的IOC容器,SpringApplication.java具体源码如下:

public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
        return run(new Class[]{primarySource}, args);
}
           

ConfigurableApplicationContext.java类定义和类关系图如下:

public interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle, Closeable {...}
           
7、SpringBoot自动配置原理Condition(详细源码分析)前言1、案例演示2、什么是Condition3、终极原理分析

 启动类中使用的context.getBean("redisTemplate");方法其实是调用了ConfigurableApplicationContext.java这个接口的实现类BeanFactory.java类中的如下方法:

public interface BeanFactory {
    Object getBean(String var1) throws BeansException;
}
           

 运行启动类,报如下错误,说找不到指定的Bean:

7、SpringBoot自动配置原理Condition(详细源码分析)前言1、案例演示2、什么是Condition3、终极原理分析

情况二:POM依赖导入Redis依赖的坐标

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
           

再次启动,发现IOC容器中有名为redisTemplate的Bean了,输出如下:

7、SpringBoot自动配置原理Condition(详细源码分析)前言1、案例演示2、什么是Condition3、终极原理分析

         那么,SpringBoot怎么知道我们有没有导入Redis坐标呢,或者说SpringBoot怎么知道该不该向容器中注入RedisTemplate这个Bean呢?关键就在Conditions条件判断功能,有了这个条件判断,SpringBoot就能实现启动期间动态的自动配置,接下来我们深入研究下Condition原理。

============================优雅的分割线===============================

2、什么是Condition

        Condition 是Spring 4.0 增加的条件判断功能,通过这个判断可以实现选择性的创建 Bean 操作,下面我通过两个案例,纯手写Condition判断器来给大家进行分析和展示。

案例一

在Spring的IOC容器中注入User的Bean,但前提是IOC中有Jedis字节码,User才注入。

1、没有Condition条件判断的代码

1、Bean对象User.java

package com.domain;

public class User {

}
           

2、自定义配置UserConfig.java

package com.domain.config;

import com.domain.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class UserConfig {
    @Bean
    public User user(){
        return new User();
    }
}
           

3、启动类

package com;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class App {
    public static void main(String[] args) {
        //接收启动类run()方法的返回值,其实就是一个Spring的IOC容器
        ConfigurableApplicationContext context = SpringApplication.run(App.class, args);
        //获取名为user的Bean
        Object user = context.getBean("user");
        System.out.println(user);
    }
}
           

        毋庸置疑,启动后指定是能获取到自定义的User这个Bean的,输出如下:

7、SpringBoot自动配置原理Condition(详细源码分析)前言1、案例演示2、什么是Condition3、终极原理分析

2、加入Condition条件判断的代码

1、新建ClassCondition.java条件类

package com.condition;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class ClassCondition implements Condition {
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        boolean flag = true;
        try {
            /*
            导入Jedis坐标后,加载该Bean,否则不加载
            如果Jedis坐标导入,Jedis类字节码会被加载到内存中,我们通过字节码反射获取类对象
            如果获取异常,说明当前环境中没有Jedis坐标
             */
            Class.forName("redis.clients.jedis.Jedis");
        } catch (ClassNotFoundException e) {
            flag = false;
        }
        return flag;
    }
}
           

2、修改自定义配置类UserConfig.java

package com.domain.config;

import com.condition.ClassCondition;
import com.domain.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;

@Configuration
public class UserConfig {
    /*
    ClassCondition 是我们自定义的Condition条件判断类,
    类中重写org.springframework.context.annotation.Condition接口中的matches方法,返回Boolean类型结果
    @Conditional(UserCondition.class) 返回true则创建当前User这个Bean,否则不创建
     */
    @Bean
    @Conditional(ClassCondition.class)
    public User user(){
        return new User();
    }
}
           

3、不导入Jedis坐标

启动后控制太输出如下:

7、SpringBoot自动配置原理Condition(详细源码分析)前言1、案例演示2、什么是Condition3、终极原理分析

4、导入Jedis坐标

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

启动后控制台输出如下:

7、SpringBoot自动配置原理Condition(详细源码分析)前言1、案例演示2、什么是Condition3、终极原理分析

5、分析源码原理

首先,来看一下@Conditionl注解对应的源码类:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.context.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
    Class<? extends Condition>[] value();
}
           
我们发现Conditional.java这个注解类是个接口,并且继承Condition.java类:      
package org.springframework.context.annotation;

import org.springframework.core.type.AnnotatedTypeMetadata;

@FunctionalInterface
public interface Condition {
    boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}
           

        Condition.java类也是个接口,并且有一个返回类型为Boolean的matches(..)方法,如上代码例子,这个matches(..)方法需要我们重写,方法内部定义条件判断逻辑,当返回值为true时才会创建Bean,为false时不创建Bean。但是上述方式肯定是有问题的,因为写了一堆代码,却只能判断一个Jedis坐标是否导入环境,那如果我想动态的依据指定的类字节码是否存在来判断是否创建某个Bean该怎么做呢?

案例二

动态的依据指定的类字节码是否存在来判断是否创建某个Bean。

1、新建自定义ConditionOnClass.java注解类

package com.condition;
import org.springframework.context.annotation.Conditional;
import java.lang.annotation.*;

@Target({ElementType.TYPE, ElementType.METHOD}) //设置当前注解类作用范围可以是类、方法
@Retention(RetentionPolicy.RUNTIME)             //设置注解生效时机是运行时
@Documented                                     //声明生成javaDoc说明文档
//以上三个注解是@Conditional内部的原生注解,需要一并移植过来

@Conditional(ClassCondition.class)
public @interface ConditionOnClass {
    String[] value();//定义一个接收类似"redis.clients.jedis.Jedis"这样的字节码全包路径名的数组
}
           

2、修改ClassCondition.java条件类

package com.condition;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

import java.util.Map;

public class ClassCondition implements Condition {

    /**
     * @param conditionContext   上下文对象,获取环境、IOC容器、ClassLoader对象
     * @param annotatedTypeMetadata  注解元对象,获取注释定义的属性值
     * @return
     */
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        boolean flag = true;
        try {
            //获取ConditionOnClass注解入参map
            Map<String, Object> annotationAttributes = annotatedTypeMetadata.getAnnotationAttributes(ConditionOnClass.class.getName());
            System.out.println("注解属性入参: "+annotationAttributes);
            //因为ConditionOnClass.java中定义的是String[] value();  所以此处直接使用value作为key获取属性值数组
            String[] values = (String[])annotationAttributes.get("value");
            //循环每一个,有一个不存在即返回false
            for(String className:values){
                Class.forName(className);
            }
        } catch (ClassNotFoundException e) {
            flag = false;
        }
        return flag;
    }
}
           

3、修改自定义配置类UserConfig.java

package com.config;
import com.condition.ClassCondition;
import com.condition.ConditionOnClass;
import com.domain.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;

@Configuration
public class UserConfig {
    @Bean
    //@ConditionOnClass 是我们自定义的Condition条件判断类,可以动态传多个条件
    //实际生产中,当前Bean需要在什么条件下才注入取决于实际业务场景
    //当前含义:你想用我的User对象,首先你要导入Jedis坐标,否则不让你用
    @ConditionOnClass("redis.clients.jedis.Jedis")
    public User user(){
        return new User();
    }
}
           

导入Jedis起步依赖情况下启动后,控制台输出如下:

7、SpringBoot自动配置原理Condition(详细源码分析)前言1、案例演示2、什么是Condition3、终极原理分析

不导入Jedis起步依赖情况下启动后,控制台输出如下: 

7、SpringBoot自动配置原理Condition(详细源码分析)前言1、案例演示2、什么是Condition3、终极原理分析

 说明动态根据注解入参进行自定义Bean注入的功能实现了。

============================优雅的分割线===============================

3、终极原理分析

        以上就是纯手写Condition判断器的整个过程,可能有些小伙伴仍然有点迷糊,没关系,可以多看本文,多琢磨。回到前言中提到的,SpringBoot具体是怎么根据导入的起步依赖进行动态配置项目的,其实和我们上面的Condition判断器是一个思想,我们上面是根据IOC中是否存在某个字节码来判断是否创建某个Bean,SpringBoot中提供了很多的如上类似的Condition判断器,具体是在SpringBoot注解包spring-boot-autoconfigure包下面:

7、SpringBoot自动配置原理Condition(详细源码分析)前言1、案例演示2、什么是Condition3、终极原理分析

         然后,SpringBoot又定义了一系列的配置类,比如说什么jdbc、mongo、jpa、redis等等,我们随便点开redis的配置类进行简单说明,包路径如下图:

7、SpringBoot自动配置原理Condition(详细源码分析)前言1、案例演示2、什么是Condition3、终极原理分析

         如上,这些SpringBoot已经存在的Condition动态配置判断器我们可以直接拿过来用,无需自己手动编写,比如@ConditionalOnProperty的用法:

1、修改自定义配置类UserConfig.java

package com.config;
import com.condition.ClassCondition;
import com.condition.ConditionOnClass;
import com.domain.User;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;

@Configuration
public class UserConfig {
    @Bean
    //@ConditionalOnProperty 为SpringBoot提供的Condition器
    // 含义是:当配置文件中存在key=itLean, value=百里慕溪 的配置时候才会创建user2的Bean
    @ConditionalOnProperty(name = "itLean", havingValue = "百里慕溪")
    public User user(){
        return new User();
    }
}
           

2、添加配置文件application.yml

itLean: We're on our way. Come on
           

启动后,控制台输出:

7、SpringBoot自动配置原理Condition(详细源码分析)前言1、案例演示2、什么是Condition3、终极原理分析

 注释掉配置文件,控制台输出:

7、SpringBoot自动配置原理Condition(详细源码分析)前言1、案例演示2、什么是Condition3、终极原理分析
7、SpringBoot自动配置原理Condition(详细源码分析)前言1、案例演示2、什么是Condition3、终极原理分析

 以上就是SpringBoot自动配置的原理解析,希望对你有帮助,学到了就点赞收藏吧!

继续阅读