天天看点

从零开始SpringCloud Alibaba实战(72)——springboot核心之自动配置之Enable*注解原理及应用

前言

SpringBoot中提供了很多Enable开头的注解,这些注解都是用于动态启用某些功能的,而其底层原理是使用@Import注解导入一些配置类,比如实现Bean的动态加载。这句话听起来稀里糊涂,那么我们来思考一个问题:SpringBoot工程是否可以直接获取jar包中定义的Bean?带着问题我们来一起刨析下SpringBoot自动配置之@Enable*注解的源码。

验证

1、首先简单看一下启动注解@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}
)}
)
public @interface SpringBootApplication {
    ...
}
           

解释:

@Target({ElementType.TYPE}) ----------声明注解作用范围是类;

@Retention(RetentionPolicy.RUNTIME)—声明注解作用时机是运行时;

@Documented---------------------------声明生成javaDoc文档;

@SpringBootConfiguration--------------点进去,源码如下,说明@SpringBootConfiguration是一个组合注解,本质其实就是一个@Configuration,也就是一个配置类的标记,这也是为什么SpringBoot主配置类中可以创建Bean的根本原因:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration    //这个注解的核心
@Indexed
public @interface SpringBootConfiguration {
    @AliasFor(
        annotation = Configuration.class
    )
    boolean proxyBeanMethods() default true;
}
           

@EnableAutoConfiguration--------------点进去看源码,发现它也是一个组合注解,有一个@Import注解

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})    //这个注解是核心
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
    Class<?>[] exclude() default {};
    String[] excludeName() default {};
}
           

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

1、创建两个模块演示

本文分别创建springboot-enable、springboot-embedded两个模块,前者代表我们的springBoot项目,后者模拟三方jar包

编写springboot-embedded

1、定义Bean类User.java

public class User {
}
           

2、定义配置类UserConfig.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class UserConfig {
 
    @Bean
    public User user(){
        return new User();
    }
}
           

3、编写springboot-enable

1、pom.xml中引入springboot-embedded的坐标依赖

<dependency>
    <groupId>com.test</groupId>
    <artifactId>springboot-embedded</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
           

2、启动类

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
 
@SpringBootApplication
public class SpringbootEnableApplication {
 
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
        User user = (User)context.getBean("user");
        System.out.println(user);
    }
}
           

启动后,报错

nosuch bean

说明,SpringBoot工程是不可以直接获取jar包中定义的Bean,那么问题又来了,为什么我们在pom.xml中引入Redis的坐标以后,却可以直接使用名为redisTemplate这样一个对象呢?根本原因就是@Import这个注解

SpringBoot实现使用第三方jar包中定义的Bean

上面已经验证SpringBoot工程是不可以直接获取jar包中定义的Bean,原因就是启动类上的@SpringBootApplication注解内部组合之一的@ComponentScan是个扫包范围注解,默认扫包范围是扫描被@SpringBootApplication注解的启动类的同级包及其子包,我们发现启动类的包为com.itlean,而UserConfig.java的包是com.embedded.config,并没有同级或者包含关系,想要启动时候能扫描到UserConfig.java,有3中解决方式,具体如下:

1,第一种方式(扫包范围)

修改启动类,添加扫包范围注解,将UserConfig.java所在包进行手动扫描:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;
 
@SpringBootApplication
@ComponentScan("com.test.config") //UserConfig.java手动扫描加入IOC容器
public class SpringbootEnableApplication {
 
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
        User user = (User)context.getBean("user");
        System.out.println(user);
    }
}
           

但是这种方式是不是太粗放了,我用你一个对象,完了我还得知道这个对象所在的包,那我要用到Redis的redisTemplate对象,我还得一顿找这玩意在哪个包,然后在启动类的@ComponentScan注解中添加扫包,肯定是不现实的

2、第二种方式@Import注解

先看一下源码,发现入参是Class<?>[] value();也就是一堆类的数组,多一嘴哈,在SpringBoot中使用@Import注解引入的这些类都会被加载到IOC容器中。
           
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
    Class<?>[] value();
}
           

既然如上所说,那我们把启动类注解再一次修改如下:

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;
 
@SpringBootApplication
@Import(UserConfig.class)//直接将User的配置类引入
public class SpringbootEnableApplication {
 
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
        User user = (User)context.getBean("user");
        System.out.println(user);
    }
}
           

这种方式要比方式一间接很多了,不需要知道被使用Bean的包路径,但是还是要记住配置类的名字,我用一个对象,还需要知道这个对象对应的配置类叫什么,显然还是不够友好,接下来方式三就是@Import用法的终极优化。

3、第三种方式@Import注解封装

在第三方jar中新建一个注解类,对@Import注解进行封装:
           
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
 
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(UserConfig.class)// 在第三方中直接将UserConfig.class引入,调用方就不需要知道包路径和配置类名称了
public @interface EnableUser {
}
           

调用方启动类修改如下:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
 
@SpringBootApplication
@EnableUser //这种使用起来已经非常简洁了
public class SpringbootEnableApplication {
 
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
        User user = (User)context.getBean("user");
        System.out.println(user);
    }
}
           

实际开发中,都是使用最后这种方式。

SpringBoot中@Enable*注解

以上第三种方式就是SpringBoot底层自动装配的原理,所以说@Enable*这类注解是开启某些功能的注解,底层是使用@Import方式来实现Bean的动态加载,不使用@EnableUser这个注解,我就不能使用User这个对象。再回过头来看启动类的注解@SpringBootAppliction注解,内部注解组合中有一个@EnableAutoConfiguration注解,如下:
           
@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}
)}
)
public @interface SpringBootApplication {
    ...
}
           

@EnableAutoConfiguration注解内部呢又使用@Import注解,如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
    Class<?>[] exclude() default {};
    String[] excludeName() default {};
}