SpringBoot的Condition注解

文章目录
  • 参考资料
  • 运行环境
  • SpringBoot 自动配置原理
  • 一、Condition
  • 二、切换内置web服务器
  • 三、Enable*注解
  • 四、Import 注解
  • 五、@EnableAutoConfiguration 注解
  • 六、案例
  • 七、收获与总结

参考资料


视频链接

运行环境


  • win10
  • IDEA专业版
  • SpringBoot 2.6.2

SpringBoot 自动配置原理


一、Condition


Condition 是在 Spring 4.0 增加的条件判断功能,作用是实现选择性的创建Bean操作

思考:
SpringBoot是如何知道要创建哪个 Bean的?比如SpringBoot是如何知道要创建 RedisTemplate的?
需求:
在Spring的IOC容器中有一个User的Bean,现要求

0x001 导入 Jedis 坐标后,加载该Bean,没导入则不加载

测试的项目结构

SpringBoot的Condition注解_第1张图片

User.java

package com.uni.springbootcondition.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private int id;
    private String username;
    private String password;
}

ClassCondition.java

package com.uni.springbootcondition.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 context, AnnotatedTypeMetadata metadata) {

        // 需求:导入Jedis坐标后创建Bean
        // 思路:判断Redis.clients.jedis.Jedis.class文件是否存在
        boolean flag = true;
        try {
            Class<?> cls = Class.forName("redis.clients.jedis.Jedis");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            flag = false;
        }
        return flag;
    }
}

UserConfig.java

package com.uni.springbootcondition.config;

import com.uni.springbootcondition.condition.ClassCondition;
import com.uni.springbootcondition.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
    @Conditional(ClassCondition.class)
    public User user(){
        return new User();
    }
}

SpringbootConditionApplication.java

package com.uni.springbootcondition;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class SpringbootConditionApplication {

    public static void main(String[] args) {

        // 启动 SpringBoot的应用,返回 Spring的 IOC容器
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootConditionApplication.class, args);

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

0x010 将类的判断定义为动态的。判断哪个字节码文件存在可以动态指定。

项目结构:

SpringBoot的Condition注解_第2张图片

ClassCondition.java

package com.uni.springbootcondition.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 context 上下文对象。 用于获取环境, IOC容器,ClassLoader对象
     * @param metadata 注解元对象。 可以用于获取注解定义的属性值
     * @return
     */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 需求:导入通过注解属性值value指定坐标后创建Bean
        // 思路:获取注解属性值
        Map<String, Object> map = metadata.getAnnotationAttributes(ConditionOnClass.class.getName());
        String[] value = (String[])map.get("value");
        boolean flag = true;
        try {
            for (String className : value) {
                Class<?> cls = Class.forName(className);
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            flag = false;
        }
        return flag;
    }
}


ConditionOnClass.java

package com.uni.springbootcondition.condition;

import org.springframework.context.annotation.Conditional;

import java.lang.annotation.*;


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


UserConfig.java

package com.uni.springbootcondition.config;

import com.uni.springbootcondition.condition.ClassCondition;
import com.uni.springbootcondition.condition.ConditionOnClass;
import com.uni.springbootcondition.domain.User;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;



@Configuration
public class UserConfig {
    @Bean
    @ConditionOnClass("redis.clients.jedis.Jedis")
    public User user(){
        return new User();
    }
}

其实在SpringBoot中,自动实现了之前的需求

SpringBoot的Condition注解_第3张图片

SpringBoot的Condition注解_第4张图片

源码:ConditionalOnBean.java

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

package org.springframework.boot.autoconfigure.condition;

import java.lang.annotation.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;
import org.springframework.context.annotation.Conditional;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional({OnBeanCondition.class})
public @interface ConditionalOnBean {
    Class<?>[] value() default {};

    String[] type() default {};

    Class<? extends Annotation>[] annotation() default {};

    String[] name() default {};

    SearchStrategy search() default SearchStrategy.ALL;

    Class<?>[] parameterizedContainer() default {};
}

当 Spring IOC容器 存在某个Bean时才执行

比如 Redis的自动配置功能,源码org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguraion.java


@Bean
@ConditionalOnMissingBean(
    name = {"redisTemplate"}
)
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    RedisTemplate<Object, Object> template = new RedisTemplate();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
}

@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
    return new StringRedisTemplate(redisConnectionFactory);
}

@ConditionalOnMissingBean表示当某个Bean不存在时,源码中第3行,将name属性值指定为redisTemplate

在第13行也有用到,就是不存在的话就创建一个Redis实例,通过使用注解实现对Bean是否存在的判断,若不存在就进行创建对象的处理,从而实现了自动配置

0x011 小结

  • 自定义条件

    • 定义条件类:自定义实现 Condition 接口,重写 matches 方法, 在matches方法中进行逻辑判断,返回boolean值。 matches方法的两个参数:1) context:上下文对象,可以获取属性值,获取类加载器,获取BeanFactory等;2) metada:元数据对象,用于获取注解属性
    • 判断条件:在初始化Bean时,使用@Conditional(条件类.class)注解
  • SpringBoot 提供的常用条件注解

    注解名 描述
    ConditionalOnProperty 判断配置文件中是否有对应属性和值才初始化Bean
    ConditionalOnClass 判断环境中是否有对应字节码文件才初始化Bean
    ConditionalOnMissingBean 判断环境中没有对应Bean才初始化Bean

二、切换内置web服务器


源码位置:org.springframework.boot.autoconfigure.web.embedded

SpringBoot的Condition注解_第5张图片

EmbeddedWebServerFactoryCustomizerAutoConfiguration.java 源码中自动配置Tomcat部署的实现

@ConditionalOnClass({Tomcat.class, UpgradeProtocol.class})
public static class TomcatWebServerFactoryCustomizerConfiguration {
    public TomcatWebServerFactoryCustomizerConfiguration() {
    }

    @Bean
    public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(Environment environment, ServerProperties serverProperties) {
        return new TomcatWebServerFactoryCustomizer(environment, serverProperties);
    }
}

可以观察到它用到了 @ConditionalOnClass 注解,就是当存在Tomcat的类还有另外一个UpgradeProtocol的类才会进行配置,其他的几个部署模式也是这样的。

所以在SpringBoot中切换内置web服务器,只需要修改pom.xml中的依赖,排除默认的Tomcat包,添加其他的包比如Jetty,就完成了。

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <!-- 排除 Tomcat 依赖 -->
    <exclusions>
        <exclusion>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <groupId>org.springframework.boot</groupId>
        </exclusion>
    </exclusions>
</dependency>
<!-- 引入 Jetty的 依赖 -->
<dependency>
    <artifactId>spring-boot-starter-jetty</artifactId>
    <groupId>org.springframework.boot</groupId>
</dependency>

开启SpringBoot后,通过查看启动信息可以发现,内置的web服务器切换成功

SpringBoot的Condition注解_第6张图片

三、Enable*注解


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

思考:SpringBoot工程是否可以直接获取Jar包中定义的Bean?

测试的项目结构
SpringBoot的Condition注解_第7张图片

其中 springboot-enable-other用于模拟其他jar包,先在其放到enable项目内部,类似于给项目导入jar包,然后在到外面的SpringBoot项目里获取里面项目的Bean,测试看看能否获取到。

UserConfig.java

package com.uni.domain;

public class User {
}


EnableUser.java 模拟一个EnableUser注解,用于导入第三方包User的Bean

package com.uni.config;

import org.springframework.context.annotation.Import;

import java.lang.annotation.*;


@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(UserConfig.class)
public @interface EnableUser {
}


SpringBootEnableAction.java

package com.uni.springbootenable;

import com.uni.config.EnableUser;
import com.uni.config.UserConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;

/**
 * @ComponentScan 扫描范围:当前引导类所在及其子包
 * 1. 使用 @ComponentScan 扫描 com.uni.config包
 * 2. 使用 @Import 注解,加载类,这些类都会被 Spring创建,并放入IOC容器
 * 3. 可以对 @Import注解进行封装
 */
@SpringBootApplication
//@ComponentScan("com.uni.config")
//@Import(UserConfig.class)
@EnableUser
public class SpringbootEnableApplication {

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

        // 获取 Bean
        Object user = context.getBean("user");
        System.out.println(user);
    }
}

在@SpringBootApplication注解的源码中

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}

其中的 @EnableAutoConfiguration 则实现了自动配置

这体现了之前 能直接获取 redisTemplate Bean的原因,在pom.xml中导入 redis后,改包提供了类似于之前手写的@EnableUser注解,注解中用到了@Import注解,直接将类注入到了Spring的IOC容器,从而使得SpringBoot能识别到对应的类

四、Import 注解


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

测试的项目结构:

SpringBoot的Condition注解_第8张图片

EnableUser.java

package com.uni.config;

import org.springframework.context.annotation.Import;

import java.lang.annotation.*;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(UserConfig.class)
public @interface EnableUser {
}

MyImportBeanDefinationRegister.java

package com.uni.config;

import com.uni.domain.User;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;

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


MyImportSelector.java

package com.uni.config;

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"com.uni.domain.User", "com.uni.domain.Role"};
    }
}

UserConfig.java

package com.uni.config;

import com.uni.domain.Role;
import com.uni.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();
    }
    @Bean
    public Role role(){return new Role();}
}

SpringbootEnableApplication.java

package com.uni.springbootenable;

import com.uni.config.EnableUser;
import com.uni.config.MyImportBeanDefinitionRegistrar;
import com.uni.config.MyImportSelector;
import com.uni.config.UserConfig;
import com.uni.domain.Role;
import com.uni.domain.User;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportSelector;

import java.util.Map;

/**
 * Import 四种用法
 * 1. 导入Bean
 * 2. 导入配置类
 * 3. 导入ImportSelector的实现类
 * 4. 导入ImportBeanDefinitionRegistrar 类
 */
@SpringBootApplication
//@Import(User.class)
//@Import(MyImportSelector.class)
@Import(MyImportBeanDefinitionRegistrar.class)
public class SpringbootEnableApplication {

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

        // 第一种方法的测试

//        User user = context.getBean(User.class);
//        System.out.println(user);
//        Map map = context.getBeansOfType(User.class);
//        System.out.println(map);

        // 第二、三种方法的测试

//        User user = context.getBean(User.class);
//        Role role = context.getBean(Role.class);
//        System.out.println(user + "\n" + role);

        // 第四种方法的测试
        System.out.println(context.getBean(User.class));
    }
}

@Import 注解的四种用法

  • 导入Bean
  • 导入配置类
  • 导入ImportSelector的实现类
  • 导入ImportBeanDefinitionRegistrar 类

SpringBoot启动类注解 @SpringBootApplication 是通过上面来实现导入其他类的,步骤依次为:

  • @SpringBootApplication

  • @EnableAutoConfiguration

  • @Import(AutoConfigurationImportSelector.class)

  • interface DeferredImportSelector extends ImportSelector

五、@EnableAutoConfiguration 注解


  • @EnableAutoConfiguration 注解内部使用 @import(AutoConfigurationImportSelector.class) 来加载配置类
  • 配置文件位置:META-INF/spring.factories, 改配置文件中定义了大量的配置类,当SpringBoot应用启动时,会自动加载这些配置类,初始化Bean

一层一层地查找源码:

SpringBoot的Condition注解_第9张图片

SpringBoot的Condition注解_第10张图片
SpringBoot的Condition注解_第11张图片

SpringBoot的Condition注解_第12张图片

通过源码的提示可以看到,自动加载类的配置文件在 META-INF/spring.factories的位置,现在找到org.springframework.boot.autoconfigure中的spring-boot-autoconfigure-2.5.6.jar 可以找到存在 META-INF文件夹,而里面就有一个 spring.factories的文件,如下图

SpringBoot的Condition注解_第13张图片

spring.factories部分内容如下

SpringBoot的Condition注解_第14张图片

可以发现它包含了Redis类,从而就加载到了容器中,这就是为什么SpringBoot可以直接获取到redisTemplate的原因

六、案例


需求:自定义 redis-starter。 要求当导入 redis坐标时,SpringBoot自动创建Jedis的Bean

导入 MyBatis起步依赖

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.2</version>
</dependency>

观察原生的jar包实现,里面会提供具体的自动配置类,以及供SpringBoot扫描的spring.factories

SpringBoot的Condition注解_第15张图片

实现步骤:

  1. 创建 redis-spring-boot-autoconfigure模块
  2. 创建 redis-spring-boot-starter模块, 依赖 redis-spring-boot-autoconfigure的模块
  3. redis-spring-boot-autoconfigure 模块中初始化 Jedis 的 Bean。并定义 META-INF/spring.factories文件
  4. 在测试模块中引入自定义的redis-starer依赖,测试获取Jedis的Bean,操作Redis

项目结构
SpringBoot的Condition注解_第16张图片

在负责起步依赖的redis-spring-boot-starter模块中,只负责在pom.xml里添加confguration模块的依赖(注:该模块的test类需删除,否则会报错)

pom.xml

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

    <!-- 引入 configuation -->
    <dependency>
        <groupId>com.uni</groupId>
        <artifactId>redis-spring-boot-configuration</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>

redis-spring-boot-configuration模块中,负责配置 jedis 模拟 Redis的自动配置(注:该模块的test类需删除,否则会报错)

pom.xml

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

    <!-- 添加 Jedis 依赖 -->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
    </dependency>
</dependencies>

RedisProperties.java 用于Redis连接的配置实体类,将作为Bean注入到Spring 容器中

package com.uni.redis.config;

import org.springframework.boot.context.properties.ConfigurationProperties;


@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;
    }
}

RedisAutoConfiguration.java 根据之前注入的redisProperties配置Bean来创建一个Jedis Bean 并注入到容器中

package com.uni.redis.config;

import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.Jedis;

@Configuration
@EnableConfigurationProperties(RedisProperties.class)
@ConditionalOnClass(Jedis.class)
public class RedisAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean(name = "jedis")
    public Jedis jedis(RedisProperties redisProperties){
        System.out.println("RedisAutoConfiguration...");
        return new Jedis(redisProperties.getHost(), redisProperties.getPort());
    }
}

根据之前的源码查找,在声明自动配置类后,需要在resources/META-INF下创建一个spring.factories配置文件,用于声明实现了自动扫描的配置类

spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.uni.redis.config.RedisAutoConfiguration

至此,已完成了模拟Redis自动配置的模块,现在到另一个SpringBoot模块中,引入实现的redis-spring-boot-starter起步依赖

pom.xml

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- 自定义的 Redis的starter -->
        <dependency>
            <groupId>com.uni</groupId>
            <artifactId>redis-spring-boot-starter</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>


SpringbootTestApplication.java

package com.uni.springboottest;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import redis.clients.jedis.Jedis;

@SpringBootApplication
public class SpringbootTestApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootTestApplication.class, args);
        Jedis jedis = context.getBean(Jedis.class);
        jedis.set("role", "uni");
        String role = jedis.get("role");
        System.out.println(role);
    }
}

测试结果:

SpringBoot的Condition注解_第17张图片

七、收获与总结


又一次温习了SpringBoot是提供了快速开发Spring项目的作用而不是增强功能。

本次写了好几个Demo,跟着视频教程,使用注解去实现Bean的注入,以及起步依赖之类的,最主要的还是第一次查看了SpringBoot启动类的注解,了解到里面自动配置的一些机制,第一次了解到可以将类作为参数传给一个注解。

本次内容比较多,目前笔者没有完全吸收,不过笔者觉得这一次的学习能有助于SpringBoot的使用,至少在将来写项目的时候,可以配置好依赖。

你可能感兴趣的:(spring,boot)