[Microservices] Detailed explanation of spring conditional annotations from use to source code analysis

Table of contents

I. Introduction

2. Overview of spring conditional annotations

2.1 Introduction to Conditional Annotation @Conditional

2.2 @Conditional extension annotation

2.2.1 Summary of @Conditional extension annotations

3. Spring conditional annotation case demonstration

3.1 @ConditionalOnBean

3.2 @ConditionalOnMissingBean

3.2.1 Use on classes

3.2.2 Supplementary Use Scenarios

3.3 @ConditionalOnClass

3.4 @ConditionalOnExpression

3.5 @ConditionalOnProperty

Fourth, conditional annotation source code analysis

4.1 @ConditionalOnMissingClass source code analysis

4.1.1 Debug process analysis

4.1.2 Summary of source code analysis process

5. Application Scenarios of Conditional Annotations

5.1 Conditional annotation use case

Sixth, write at the end of the text


I. Introduction

The reason why the spring framework is widely used is that on the one hand, the excellent encapsulation of the framework can help developers save a lot of low-level development work, thereby improving efficiency. On the other hand, the framework itself provides many extension points, through which developers can use Its own business has been well expanded, such as customizing the starter, so as to seamlessly integrate with the spring framework.

As springboot has gradually become the standard configuration for microservice development, it has been favored by many developers. Compared with spring, springboot has added many development-friendly functions and configurations, especially full-annotation development is a major feature of the springboot framework. , because there are quite a lot of annotations, let's take an extension point in the spring framework that is easily overlooked by everyone, that is, conditional annotations, as an example.

2. Overview of spring conditional annotations

2.1 Introduction to Conditional Annotation @Conditional

The core condition annotation in spring is @Conditional, which is used to determine whether a bean meets a specific condition. If the condition is met (or not), the bean marked with the annotation is registered in the IOC container, otherwise it is not registered. Therefore, the core of @Conditional annotation is used to control the creation of Bean.

A large number of conditional annotations are used in the SpringBoot automatic configuration function, and many third-party components integrated with springboot are also used.

The @Conditional annotation is used together with the Condition interface to inform whether the matching condition is met through the corresponding Condition interface. The source code of the annotation is as follows:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
	/**
     * 所有用于匹配的Condition接口(实现该接口的类),只有这些类都返回true才认为是满足条件
     */
    Class<? extends Condition>[] value();
}

The @Conditional annotation can be added to classes modified by @Configuration, @Component, @Service, etc. to control whether the corresponding Bean needs to be created. If it is added to a method modified by @Bean, it is used to control whether the corresponding Bean of the method needs to be created. create

2.2 @Conditional extension annotation

In fact, in the framework-level source code that is actually developed or seen, there are not many @Conditionals used directly, but more annotations like @ConditionalOnBean, @ConditionalOnMissingBean, or @ConditionalOnClass, etc., which we also call extended annotations , taking @ConditionalOnBean as an example, you might as well take a look at the source code of the annotation. You can see that the annotation adds a reference to @Conditional, which explains the extension of the @Conditional annotation.

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

2.2.1 Summary of @Conditional extension annotations

The following are the extension annotations commonly used by @Conditional

Annotation name use reference effective position Remark
@ConditionalOnBean @ConditionalOnBean(value=AA.class) bean It takes effect when the instance in brackets exists in the IOC container
@ConditionalOnMissingBean @ConditionalOnMissingBean(name = "bean name") bean It takes effect when the instance in brackets does not exist in the IOC container
@ConditionalOnClass @ConditionalOnClass(AA.class) class The corresponding class exists in the class loader to take effect
@ConditionalOnMissingClass @ConditionalOnMissingClass(AA.class) class The corresponding class does not exist in the class loader to take effect
@ConditionalOnExpression @ConditionalOnExpression("'expression value'") class or bean Judging that the SpEL expression is valid
@ConditionalOnProperty @ConditionalOnProperty(prefix = “spring.redis”, name = “port”, havingValue = “3306”, matchIfMissing = true) class or bean Application environment attributes meet the conditions to take effect
@ConditionalOnResource @ConditionalOnResource(resources= “common.properties”) class or bean The specified resource file exists in the application environment to take effect

3. Spring conditional annotation case demonstration

The above summarizes @Conditional conditional annotations and related extended annotations. In order to deepen the understanding of these annotations, the use of each annotation will be demonstrated through code to form a deeper impression.

3.1 @ConditionalOnBean

The Condition processing class corresponding to @ConditionalOnBean is OnBeanCondition, that is, the current bean is created when the specified bean exists in the IOC container. The source code is as follows. This annotation can be used on methods or classes.

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

Create the following configuration class. In the current class, the method of the depart bean is controlled by the ConditionalOnBean annotation, that is, if the bean instance of the role exists in the IOC container, the depart bean will be created;

@ComponentScan("com.congge")
@Configuration
public class ScanConfig {

    @Bean
    public Role role(){
        return new Role("0002","测试角色");
    }

	/**
     * 也可以写为:@ConditionalOnBean(name = "role") , name的值确保是正确的
     * @return
     */
    @Bean
    @ConditionalOnBean(Role.class)
    public Depart depart(){
        return new Depart("0001","架构部");
    }

}

According to the above code, the bean instance of role will be created, so the bean of department will also be created, use the following code to test;

public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ScanConfig.class);

        //角色存在
        Role role = (Role)applicationContext.getBean("role");
        System.out.println(role.getRoleName());

        //部门是否存在
        Depart depart = (Depart)applicationContext.getBean("depart");
        System.out.println(depart.getDeptName());
    }

Run the above code, you can see the following effect, indicating that the bean instance of depart is also created;

If the bean instance creation of role is removed, the modified code is as follows

@ComponentScan("com.congge")
@Configuration
public class ScanConfig {

    @Bean
    @ConditionalOnBean(Role.class)
    public Depart depart(){
        return new Depart("0001","架构部");
    }

}

Retrofit test code

public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ScanConfig.class);

        //角色存在
        //Role role = (Role)applicationContext.getBean("role");
        //System.out.println("role :" + role);
        //System.out.println(role.getRoleName());

        //部门是否存在
        Depart depart = (Depart)applicationContext.getBean("depart");
        System.out.println("depart :" + depart);

        //System.out.println(depart.getDeptName());
    }

When running again, an error is reported directly, that is, the bean depart does not exist, and the reverse indicates that @ConditionalOnBean has taken effect  

3.2 @ConditionalOnMissingBean

The Condition implementation class corresponding to @ConditionalOnMissingBean is OnBeanCondition, that is, if the specified bean does not exist in the IOC container, the current bean (or bean in the class) is created. The corresponding source code is as follows:

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

    String[] type() default {};

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

    String[] ignoredType() default {};

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

    String[] name() default {};

    SearchStrategy search() default SearchStrategy.ALL;

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

Immediately after the above case, use the ConditionalOnMissingBean annotation on the method of creating the user bean, the code is as follows:

@ComponentScan("com.congge")
@Configuration
public class ScanConfig {

    @Bean
    public Role role(){
        return new Role("0002","测试角色");
    }

    @Bean
    @ConditionalOnMissingBean(name = "depart")
    public User user(){
        return new User("0003","jerry");
    }

}

According to the code guess, the user method uses the current annotation, indicating that when the depart bean does not exist in the IOC container, the user bean will be created, using the following test code

public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ScanConfig.class);

        //角色存在
        Role role = (Role)applicationContext.getBean("role");
        System.out.println(role.getRoleName());

        //用户是否存在
        User user = (User)applicationContext.getBean("user");
        System.out.println(user.getUserName());
    }

Run this code and see the following effect, indicating that the annotation is in effect

3.2.1 Use on classes

The above demonstrates the effect of using the two annotations on the method. It can be found from the source code that it can also be used on the class. If it acts on the class, it means that the bean in the entire class takes effect;

We add this annotation to the class. If there is no instance of userBean in the IOC container, we can create bean instances of role and depart

@ComponentScan("com.congge")
@Configuration
@ConditionalOnMissingBean(name = "userBean")
public class ScanConfig {

    @Bean
    public Role role(){
        return new Role("0002","测试角色");
    }

    /**
     * 也可以写为:@ConditionalOnBean(name = "role")
     * @return
     */
    @Bean
    public Depart depart(){
        return new Depart("0001","架构部");
    }
    
}

Run the following test code

 public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ScanConfig.class);

        //用户是否存在
        //User user = (User)applicationContext.getBean("userBean");
        //System.out.println(user.getUserName());

        //角色存在
        Role role = (Role)applicationContext.getBean("role");
        System.out.println(role.getRoleName());

        //角色存在
        Depart depart = (Depart)applicationContext.getBean("depart");
        System.out.println(depart.getDeptName());
    }

The log output through the console also confirms the above conjecture, and the bean corresponding to role or depart can be obtained correctly.

3.2.2 Supplementary Use Scenarios

For the usage scenarios of this annotation, make the following additions:

  • In the project, only one bean is allowed to be injected once, that is, it is guaranteed that there is only one instance of this bean in ioc;
  • Generally speaking, for custom configuration classes, @ConditionalOnMissingBean annotation can be added to avoid the risk of simultaneous injection of multiple configurations;

After adding this annotation, when you register two beans of the same type, 会抛出异常it will ensure that there is only one bean of the same type

As follows, create two User bean instances at the same time, user1 and user2;

	@Bean
    @ConditionalOnMissingBean
    public User user1(){
        return new User("user1","james");
    }

    @Bean
    @ConditionalOnMissingBean
    public User user2(){
        return new User("user2","zhaoyun");
    }

By running the following test code, it is found that in this case, only the user1 bean is finally created

public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ScanConfig.class);
        User user1 = (User)applicationContext.getBean("user1");
        System.out.println(user1.getUserName());

        User user2 = (User)applicationContext.getBean("user2");
        System.out.println(user2.getUserName());
    }

The running effect is as follows:

3.3 @ConditionalOnClass

The Condition processing class corresponding to @ConditionalOnClass is OnClassCondition, that is, it will take effect if there is a specified class under the current class path. The corresponding source code is as follows:

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

    String[] name() default {};
}

In the case that there is a class like User in the current project, through this annotation, it can be expected that the role bean can be created

@ComponentScan("com.congge")
@Configuration
//@ConditionalOnMissingBean(name = "userBean")
public class ScanConfig {

    /**
     * 也可以写成: com.congge.entity.User
     * @return
     */
    @Bean
    @ConditionalOnClass(User.class)
    public Role role(){
        return new Role("0002","测试角色");
    }

}

Run the test code, you can see that the bean corresponding to the role is created;

If the User class is deleted or commented out, use the following code to test again

    /**
     * 也可以写成: com.congge.entity.User
     * @return
     */
    @Bean
    //@ConditionalOnClass(User.class)
    @ConditionalOnClass(name ="com.congge.entity.User")
    public Role role(){
        return new Role("0002","测试角色");
    }

Running can see the following effect

 @ConditionalOnMissingClass can be compared to local validation

3.4 @ConditionalOnExpression

The Condition processing class corresponding to @ConditionalOnExpression is OnExpressionCondition, that is, it takes effect only when the SpEL expression meets the conditions. The corresponding source code is as follows

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@Conditional({OnExpressionCondition.class})
public @interface ConditionalOnExpression {
    String value() default "true";
}

Add the following configuration to the local configuration file application.properties:

server.port=8087

#条件表达式需用到的配置
hxstrive.type=server
hxstrive.user.enable=true
hxstrive.order.size=10

Add the @ConditionalOnExpression annotation to the configuration class. The code is as follows. When acting on the class, it means that when the conditions in the expression are met, the beans in the class will be created;

@Configuration
@ConditionalOnExpression("'${hxstrive.type}'.equals('server') && (${hxstrive.user.enable})")
public class ScanConfig {

    @Bean
    public Role role(){
        return new Role("0002","测试角色");
    }
}

Regarding the writing of SPEL expressions, there are very detailed information on the Internet for reference, so I won’t show them too much here.

Then get the role bean in the startup class. If you can get it, it means that the expression has taken effect

@SpringBootApplication
@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
public class AnnoApp {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(AnnoApp.class, args);
        Role role = (Role) context.getBean("role");
        System.out.println(role.getRoleName());
    }
}

Run the above startup class, and you can find through the console that an instance of the bean role is created.

3.5 @ConditionalOnProperty

The Condition implementation class OnPropertyCondition corresponding to @ConditionalOnProperty will take effect only when the corresponding configuration property is equal to the value of the given condition. The source code is as follows,

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

    String prefix() default "";

    String[] name() default {};

    String havingValue() default "";

    boolean matchIfMissing() default false;
}

This annotation is realized through its three attributes prefix, name and havingValue, where prefix represents the node prefix in the configuration file, name is used to read a property value from application.properties, and havingValue represents the target value, specifically:

  • If the value is empty, return false;
  • If the value is not empty, compare the value with the value specified by havingValue, and return true if they are the same, otherwise return false;
  • If the return value is false, the configuration will not take effect; if it is true, it will take effect;

In the following code, the creation of userBean depends on the annotation @ConditionalOnProperty on the class and whether the property values ​​in it meet the requirements

@Configuration
@ConditionalOnProperty(prefix="spring.user",name = "username", havingValue = "jerry")
public class BeanConfig {

    @Bean("userBean")
    public User userBean(){
        return new User("0003","mike");
    }
}

The following configuration exists in the application.properties configuration file

spring.user.username=jerry

Based on the above configuration conditions, run the following startup class, you can see that you can get the instance of userBean

public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(AnnoApp.class, args);
        User user = (User) context.getBean("userBean");
        System.out.println(user.getUserName());
    }

Fourth, conditional annotation source code analysis

Taking @ConditionalOnMissingClass as an example, analyze the source code execution process of this annotation. There is the following code. This annotation is used on the ScanConfig class, indicating that if the User class in the annotation exists, the role bean will be created;

@Configuration
@ComponentScan("com.congge")
@ConditionalOnMissingClass(value = "com.congge.entity.User")
public class ScanConfig {

    @Bean
    public Role role(){
        return new Role("0002","测试角色");
    }

}

In the current project, we first delete or comment out the User class, and use the following code to test. The effect is that the bean of role is created, which is exactly in line with the functional connotation to be realized by the @ConditionalOnMissingClass annotation;

4.1 @ConditionalOnMissingClass source code analysis

The process of springboot from startup to how to scan, annotation analysis, bean analysis, and post-processor will not continue here. This is a relatively complicated process. Let's start directly from the source code of the annotation to figure out its underlying implementation principle . 

The source code of @ConditionalOnMissingClass is as follows. This annotation can be applied to the class or to the creation of a method, that is, a bean;  

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

OnClassCondition.class is an important entry point for condition selection. Click here, and you can see that this class inherits the FilteringSpringBootCondition class. Generally, if there is an inheritance relationship, you need to find clues from the parent class;

Going deeper from FilteringSpringBootCondition, I saw the class SpringBootCondition,

Continue to go deep into the SpringBootCondition class to find the final answer, that is, to complete the judgment of the condition, you need to call the matches method. The interface definition of this method is defined in the Condition interface;

Next, we only need to figure out the ins and outs of the matches method. Regarding this method, we can understand it through code comments

//针对所有条件注解进行判断
public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        //条件注解是写在哪个类或方法上呢?通过这个方法进行获取
        String classOrMethodName = getClassOrMethodName(metadata);

        try {
        	//拿到条件注解的判断结果
            ConditionOutcome outcome = this.getMatchOutcome(context, metadata);
            
            //记录当前条件判断的结果到日志中
            this.logOutcome(classOrMethodName, outcome);
            
            //将判断的结果记录到ConditionEvaluationReport中去
            this.recordEvaluation(context, classOrMethodName, outcome);
            
            //获取判断的结果
            return outcome.isMatch();
        } catch (NoClassDefFoundError var5) {
            throw new IllegalStateException("Could not evaluate condition on " + classOrMethodName + " due to " + var5.getMessage() + " not found. Make sure your own configuration does not rely on that class. This can also happen if you are @ComponentScanning a springframework package (e.g. if you put a @ComponentScan in the default package by mistake)", var5);
        } catch (RuntimeException var6) {
            throw new IllegalStateException("Error processing condition on " + this.getName(metadata), var6);
        }
    }

4.1.1 Debug process analysis

Starting from the matches method above, come to the following process through debug

Find the implementation corresponding to the specific conditional annotation

Enter the OnClassCondition class to find the getMatchOutcome method. The source code is as follows, which can be understood in combination with comments

public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
        ClassLoader classLoader = context.getClassLoader();
        ConditionMessage matchMessage = ConditionMessage.empty();
        //拿到ConditionalOnClass 注解中的value值,也就是需要判断这个类是否存在
        List<String> onClasses = this.getCandidates(metadata, ConditionalOnClass.class);
        List onMissingClasses;
        if (onClasses != null) {
        	//过滤onClasses中不存在的类
            onMissingClasses = this.filter(onClasses, ClassNameFilter.MISSING, classLoader);
            //如果上一步过滤出来了不存在的类,说明不匹配
            if (!onMissingClasses.isEmpty()) {
                return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnClass.class, new Object[0]).didNotFind("required class", "required classes").items(Style.QUOTE, onMissingClasses));
            }
			//否则就是匹配到了
            matchMessage = matchMessage.andCondition(ConditionalOnClass.class, new Object[0]).found("required class", "required classes").items(Style.QUOTE, this.filter(onClasses, ClassNameFilter.PRESENT, classLoader));
        }
        
		//拿到ConditionalOnMissingClass 注解中的value值,也就是需要判断这个类是否存在
        onMissingClasses = this.getCandidates(metadata, ConditionalOnMissingClass.class);
        if (onMissingClasses != null) {
        	//进一步检查这个ConditionalOnMissingClass中的class类是否存在
            List<String> present = this.filter(onMissingClasses, ClassNameFilter.PRESENT, classLoader);
            //如果过滤出来有不符合条件的,说明不匹配
            if (!present.isEmpty()) {
                return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnMissingClass.class, new Object[0]).found("unwanted class", "unwanted classes").items(Style.QUOTE, present));
            }

            matchMessage = matchMessage.andCondition(ConditionalOnMissingClass.class, new Object[0]).didNotFind("unwanted class", "unwanted classes").items(Style.QUOTE, this.filter(onMissingClasses, ClassNameFilter.MISSING, classLoader));
        }

        return ConditionOutcome.match(matchMessage);
    }

Through breakpoint debugging, you will enter the method of parsing the ConditionalOnMissingClass annotation, and then parse to the User class

Continue to follow up, and finally call the ConditionOutcome.match method. Since the above judgment is passed, the object encapsulated with ConditionOutcome is directly returned here, where match is true;

Return to the place where the call is made. When the above judgment is passed, the matches method in SpringBootCondition returns true

At this point, I believe that students who have a little understanding of the spring registration bean process are not difficult to guess. Next, the analysis of the ScanConfig class will be performed, and the process of creating the method that needs to create a bean in this class is not difficult to understand. , you can spy one or two on the call link of the endpoint

It is not difficult to see from the above endpoint execution link that in the process of executing doRegisterBean, a shouldSkip method is called, which is to complete the parsing process of the above ConditionalOnMissingClass annotation in this method. If the matches method returns true, we continue. Further, the next step is a process of registering beans;

Parsing and registering beans, that is, parsing the ScanConfig class and creating BeanDefinition and other steps;

4.1.2 Summary of source code analysis process

The above-mentioned @ConditionalOnMissingClass annotation is used as an example to analyze the process of source code execution. Several other annotations can be studied similarly. In summary, the parsing of conditional annotations occurs before bean registration, as the judgment condition for whether to create beans, just remember That's it.

5. Application Scenarios of Conditional Annotations

 In the springboot framework, there are many applications for conditional annotations. Interested students can see it in the source code of many external components integrated with the springboot framework. For example, the ConditionalOnMissingClass annotation is used in log integration as a bottom class, such as in log configuration. , if there is no special configuration in a certain project, then use the bottom configuration, otherwise use the bottom configuration. This means that if there is no specified implementation, there will be a default configuration implementation.

5.1 Conditional annotation use case

Let's take the @ConditionalOnProperty annotation as an example to illustrate a scheduled task scheduling scenario. Our requirements are:

Use an external configuration file to control whether the scheduled task is executed;

There are many ways to achieve the above requirements. Here, we choose to use the @ConditionalOnProperty conditional expression annotation to achieve it. Specifically, add this annotation on the class (or method) that executes the scheduled task, and control it by introducing the value of the configuration parameter in the annotation. That's it. code show as below:

@Component
@EnableScheduling
public class UserTask {

    //每5秒钟执行一次
    @Scheduled(cron="0/5 * * * * ?")
    @ConditionalOnProperty(prefix="user.task",name = "enable", havingValue = "true")
    public void cron(){
        System.out.println("执行定时任务");
    }

}

For the first time, when the configuration parameter is false, start the program and wait for 5 seconds before it is executed

Turn on the parameter as true, start the program again, wait for 5 seconds, and the scheduled task will be executed

Sixth, write at the end of the text

Spring conditional annotation is a very important member of the spring framework annotation system, and it is also one of the extension points provided by spring for developers. Reasonable use of conditional annotations can bring unexpected effects to application development. I hope that you who read this article can provide An idea, this article ends with this technology, thank you for watching.

Guess you like

Origin blog.csdn.net/zhangcongyi420/article/details/132376582