在 SpringBoot 中提供了很多 Enable 开头的注解,这些注解都是用于动态启用某些功能的。而其底层原理是使用 @Import 注解导入一些配置类,实现 Bean 的动态加载。
SpringBoot 工程是否可以直接获取 jar 包中定义的 Bean?
答案是否定的,SpringBoot 无法直接引用别人 jar 包里的 Bean。
那么问题就来了:为什么我们之前在工程中引入一个 Redis 的起步依赖就可以直接获取到 RedisTemplate 呢?
我们接下来演示一下 SpringBoot 不能获取第三方 jar 包里的 Bean 这个特点。
因为要模拟演示第三方的,我们需要创建两个模块工程:
springboot-enable、springboot-enable-other。
由于是演示,那么就简单写一些代码实现效果就可以。
在 springboot-enable-other 工程中写一个 Bean:
package com.xh.config;
public class UserConfig {
}
package com.xh.config;
import com.xh.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();
}
}
在 springboot-enable 工程中依赖 springboot-enable-other 工程。
pom.xml 文件中添加代码:
<dependency>
<groupId>com.xhgroupId>
<artifactId>springboot-enable-otherartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
修改启动类中添加获取 User 的代码:
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
// 获取 Bean
Object user = context.getBean("user");
System.out.println(user);
}
运行项目,提示没有获取到为 user 的 Bean:
通过演示我们可以看到 SpringBoot 不能直接获取我们在其他工程中定义的 Bean。
原因:在启动类中 @SpringBootApplication 注解中有一个 @ComponentScan 注解,这个注解扫描的范围是当前引导类所在包及其子包。
我们项目的引导类包路径为:om.xh.springbootenable
而 UserConfig 所在的包路径为:com.xh.config
两者并没有关联关系,那么我们如果想解决这个问题其实是有很多种方案的。
我们可以在引导类上使用 @ComponentScan 注解扫描配置类所在的包
@SpringBootApplication
@ComponentScan("com.xh.config")
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);
}
}
启动项目,输出成功获取到的 user
这样的方案虽然可以解决这个问题,但是看起来还是不太行,如果我们后期需要用到第三方的一些功能,还需要把第三方的包扫描一下,如果获取的特别多的话,那么写一排实在是太 low 了!而且也很难能记住那么多的包路径,所以这种方案是不推荐的。
被 @Import 注解所导入的类,都会被 Spring 创建,并放入 IOC 容器中。
如图可以看到 @Import 注解的 value 值是一个数组,可以传多个值。
修改引导类
@SpringBootApplication
//@ComponentScan("com.xh.config")
@Import(UserConfig.class)
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);
}
}
启动项目,输出成功获取到的 user
这种方案比方案一稍微好一些,但同样会面临同样的问题,我们需要记住很多类的名字,所以仍然不是很方便。
在 springboot-enable-other 工程中编写注解 @EnableUser,在注解中使用 @Import 注解导入 UserConfig,并且添加 @Import 的元注解
package com.xh.config;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
@Import(UserConfig.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnableUser {
}
修改 springboot-enable 工程的引导类,现在可以使用我们自定义的注解。
@SpringBootApplication
//@ComponentScan("com.xh.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);
}
}
启动项目,输出成功获取到的 user
这种自定义注解的方式和方案二是一个原理,只不过是在使用上简化了一下。
SpringBoot 底层是使用 @Import 注解导入一些配置类,实现 Bean 的动态加载。
例如 @SpringBootApplication 其中的 @EnableAutoConfiguration 就使用了 @Import 来实现导入其他的类。
@Enable* 底层依赖于 @Import 注解导入一些类,使用 @Import 导入的类会被 Spring 加载到 IOC 容器中。
@Import 提供了 4 种用法:
修改引导类:
/**
* Import 4 种用法
* 1. 导入Bean
* 2. 导入配置类
* 3. 导入 ImportSelector 实现类。一般用于加载配置文件中的类
* 4. 导入 ImportBeanDefinitionRegistrar 实现类。
*/
@SpringBootApplication
//@ComponentScan("com.xh.config")
//@Import(UserConfig.class)
//@EnableUser
@Import(User.class)
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);
// 由于使用 @Import 注解导入 User.class 获取到的 Bean 名称不叫 user
// 所以通过类型获取 Bean
User user = context.getBean(User.class);
System.out.println("user:" + user);
// 获取 Spring 容器中所有 UserBean 的名称以及 Bean 对应的值
Map<String, User> map = context.getBeansOfType(User.class);
System.out.println("map:" + map);
}
}
使用 @Import(UserConfig.class) 这种方式就是导入配置类,我们可以再创建一个对象并修改配置类的代码,看看一个配置类是否可以加载两个对象
在 enable-other 工程中创建 Role:
package com.xh.domain;
public class Role {
}
在 UserConfig 中添加代码:
@Bean
public Role role(){
return new Role();
}
修改 enable 工程引导类:
@SpringBootApplication
//@ComponentScan("com.xh.config")
//@EnableUser
//@Import(User.class)
@Import(UserConfig.class)
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);
// 由于使用 @Import 注解导入 User.class 获取到的 Bean 名称不叫 user
// 所以通过类型获取 Bean
User user = context.getBean(User.class);
System.out.println("user:" + user);
Role role = context.getBean(Role.class);
System.out.println("role:" + role);
// 获取 Spring 容器中所有 UserBean 的名称以及 Bean 对应的值
// Map map = context.getBeansOfType(User.class);
// System.out.println("map:" + map);
}
}
进入 ImportSelector 接口,我们可以看到 selectImports 方法。
这个方法参数为 AnnotationMetadata,可以用来获取一些注解的值。
返回值为 String 类型的数组,这个方法被复写之后这个方法要返回一些类的全限定名。
在 enable-other 工程中创建 MyImportSelector 并实现 ImportSelector 接口,复写 selectImports 方法:
package com.xh.config;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
// 放入 user、role 的全限定名之后,就会自动去加载 user 和对应的 role 了
// return new String[]{"com.xh.domain.User", "com.xh.domain.Role"};
// 或者可以使用方法获取
return new String[]{User.class.getName(), Role.class.getName()};
}
}
修改 enable 工程引导类:
@SpringBootApplication
//@ComponentScan("com.xh.config")
//@EnableUser
//@Import(User.class)
//@Import(UserConfig.class)
@Import(MyImportSelector.class)
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);
// 由于使用 @Import 注解导入 User.class 获取到的 Bean 名称不叫 user
// 所以通过类型获取 Bean
User user = context.getBean(User.class);
System.out.println("user:" + user);
Role role = context.getBean(Role.class);
System.out.println("role:" + role);
// // 获取 Spring 容器中所有 UserBean 的名称以及 Bean 对应的值
// Map map = context.getBeansOfType(User.class);
// System.out.println("map:" + map);
}
}
看到这里的朋友可能会感觉这种方式跟之前的没什么区别,好像还挺麻烦的,其实我们在复写 selectImports 方法时,返回的数组是字符串形式的,我们可以把全限定名添加到配置文件中,那么在项目启动时就可以动态的加载出来。
由于我们在 enable-other 工程中创建的所有类都没有在引导类的同级或者子级目录下,为了方便演示我们要把当前的目录放到 springbootenableother 包下:
修改 application.properties 配置文件名称为 application.yml,并放入属性值:
name: com.example.springbootenableother.domain.User,com.example.springbootenableother.domain.Role
修改 MyImportSelector,通过读取配置文件实现动态加载:
package com.xh.springbootenableother.config;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public class MyImportSelector implements ImportSelector {
private static Properties properties = new Properties();
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
// 放入 user、role 的全限定名之后,就会自动去加载 user 和对应的 role 了
// return new String[]{"com.xh.domain.User", "com.xh.domain.Role"};
// 或者可以使用方法获取
// return new String[]{User.class.getName(), Role.class.getName()};
// 读取配置文件
InputStream resourceAsStream = Object.class.getResourceAsStream("/application.yml");
try {
// 加载配置文件
properties.load(resourceAsStream);
} catch (IOException e) {
e.printStackTrace();
}
// 根据 key 获取并拆分返回
return properties.getProperty("name").split(",");
}
}
在 enable-other 工程中创建 MyImportBeanDefinitionRegistrar 类,实现 ImportBeanDefinitionRegistrar 接口并重写 registerBeanDefinitions 方法,这种方式是使用注入的形式。
package com.example.springbootenableother.config;
import com.example.springbootenableother.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;
/**
* @author XH
* @create 2021/12/13
* @since 1.0.0
*/
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//在 IOC 容器中注册 Bean,Bean 名称为 user,类型为 User.class
// 获取 beanDefinition
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();
// 注入 user
registry.registerBeanDefinition("user", beanDefinition);
}
}
修改 enable 工程中的引导类,可以根据类型或者 Bean 名称获取:
@SpringBootApplication
//@EnableUser
//@Import(MyImportSelector.class)
@Import(MyImportBeanDefinitionRegistrar.class)
public class SpringbootEnableApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
// 根据类型获取
Object user = context.getBean(User.class);
System.out.println(user);
// 根据 Bean 名称获取
Object user1 = context.getBean("user");
System.out.println(user1);
// Object role = context.getBean(Role.class);
// System.out.println(role);
}
}
我们可以看看 Spring 是如何使用 @Import 注解的:
进入引导类上的 @SpringBootApplication 注解可以看到其注解依赖了 @EnableAutoConfiguration 注解。
进入 @EnableAutoConfiguration 注解可以看到它使用了 @Import 注解导入了 AutoConfigurationImportSelector 这个自动配置的 Selector。
进入 AutoConfigurationImportSelector 之后我们可以找到 selectImports 方法,根据上文所述,这个方法返回了一个 String 类型的数组,数组中定义了很多需要被加载的类:
进入加载方法可以看到有一个 getCandidateConfigurations 方法会返回一个名为 configurations 的 List 集合,在下面的代码中根据条件筛选了这个集合并放入创建的 AutoConfigurationEntry 中返回:
进入 getCandidateConfigurations 可以看到他通过 SpringFactoriesLoader 加载了一些配置信息并返回了一个名为 configurations 的 List 集合,下面的断言表示如果这个集合为空的,那么就会出现异常,大概意思为:“不能自动配置一个 claesses,在 META-INF 目录下的 spring.factories 文件下”,如果没有定义 spring.factories 这个文件,那么他就加载不到,加载不到就会出现断言:
接下来我们在 External Libraries 中找到 org.springframework.boot:spring-boot-autoconfigure:2.6.1 下的
spring-boot-autoconfigure-2.6.1.jar -> META-INF -> spring.factories 并进入:
可以看到这个配置文件中有一个 key 为 org.springframework.boot.autoconfigure.EnableAutoConfiguration 的配置,其中配置了许多的 Configuration,那么这些 Configuration 将来都会被加载。
当然这些 Configuration 能否加载出来还得看他们的条件是否满足,比如我们可以找到前几个章节讲到的 Redis,Redis 在当前配置文件中有一个 RedisAutoConfiguration:
进入 RedisAutoConfiguration 我们可以看到其中有一个 @ConditionalOnClass 条件注解,当这个注解里的条件被满足时,这个类中的 Bean 才会被创建。
之前的章节中讲过这个注解,有不了解的朋友可以传送过去看看:SpringBoot 自动配置之 Condition
当然不止这一个会有条件注解,比如我们再随便挑一个进去看看:
在 KafkaAutoConfiguration 中同样定义了条件注解,当环境中存在 KafkaProperties 时,这个类中的 Bean 才会被加载。