@Import是一个非常有用的注解,它的长处在于你可以通过配置来控制是否注入该Bean,也可以通过条件来控制注入哪些Bean到Spring容器中。
比如我们熟悉的:@EnableAsync
、@EnableCaching
、@EnableScheduling
等等统一采用的都是借助@Import注解来实现的。
需要注意的是:ImportSelector、ImportBeanDefinitionRegistrar这两个接口都必须依赖于@Import一起使用,而@Import可以单独使用。
一、普通静态类
定义实体
@Data
public class UserConfig {
/** 用户名*/
private String username;
/**手机号*/
private String phone;
}
通过@Import注入容器
@Import(UserConfig.class)
@Configuration
public class UserConfiguration {
}
也支持多个
@Import({UserConfig.class,UserConfig2.class})
@Configuration
public class UserConfiguration {
}
当在@Configuration
标注的类上使用@Import引入了一个类后,就会把该类注入容器中。
当然除了@Configuration 比如@Component、@Service等一样也可以。
测试
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserServiceTest {
@Autowired
private UserConfig userConfig;
@Test
public void getUser() {
String name = userConfig.getClass().getName();
System.out.println("name = " + name);
}
}
导入带@Configuration的配置类:
@Configuration
public class MyConfig {
// 配置内容
}
@Import(MyConfig.class)
@Configuration
public class AnotherConfig {
// 配置内容
}
如果@Import的功能仅仅是这样,那其实它并没什么特别的价值,我们可以通过其它方式实现?
Bean方式注入:
@Configuration
public class UserConfiguration {
@Bean
public UserConfig userConfig() {
return new UserConfig();
}
}
直接@Configuration配置注入
@Configuration
@Data
public class UserConfig {
/** 用户名*/
private String username;
/**手机号*/
private String phone;
}
确实如果注入静态的Bean到容器中,那完全可以用上面的方式代替,但如果需要动态的带有逻辑性的注入Bean,那才更能体现@Import的价值。
二、ImportSelector的实现类注入
说到ImportSelector这个接口就不得不说这里面最重要的一个方法:selectImports()
。
public interface ImportSelector {
String[] selectImports(AnnotationMetadata importingClassMetadata);
}
这个方法的返回值是一个字符串数组
,只要在配置类被引用了,这里返回的字符串数组中的类名
就会被Spring容器new出来,然后再把这些对象注入IOC容器中。
简单例子:
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{"com.example.MyConfig"};
}
}
Import直接使用:
@Import(MyImportSelector.class)
@Configuration
public class AnotherConfig {
// 配置内容
}
这样一来同样可以通过成功将MyConfig注入容器中。
多类直接注入
public class ModelImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
return new String[]{
"com.springboot.demo.model.Animal",
"com.springboot.demo.model.Car"
};
}
}
@SpringBootApplication
@Import(ModelImportSelector.class)
public class DemoApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run( DemoApplication.class, args );
context.close();
}
}
如果看到这,你肯定会有疑问。我这又是新建MyImportSelector类,又是实现ImportSelector重写selectImports方法,然后我这么做有个卵用呢?直接把类上加个@Component
注入进去不香吗?这个ImportSelector
把简单的功能搞这么复杂。
接下来就要说说如何动态注入Bean了。
2.1 本地缓存与Redis缓存切换
我们来思考一种场景,就是你想通过开关来控制是否注入该Bean,或者说通过配置来控制注入哪些Bean,这个时候就有了ImportSelector的用武之地了。
我们来举个例子,通过ImportSelector的使用实现条件选择是注入本地缓存还是Redis缓存
。
1)、定义缓存接口和实现类
public interface CacheService {
void setData(String key);
}
2)本地缓存
实现类
public class LocalServicempl implements CacheService {
@Override
public void setData(String key) {
System.out.println("本地存储存储数据成功 key= " + key);
}
}
3)redis缓存
实现类
public class RedisServicempl implements CacheService {
@Override
public void setData(String key) {
System.out.println("redis存储数据成功 key= " + key);
}
}
2)、定义ImportSelector实现类
以下代码中根据EnableMyCache
注解中的不同值来切换缓存的实现类再spring中的注册。
public class MyCacheSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(EnableMyCache.class.getName());
//通过 不同type注入不同的缓存到容器中
CacheType type = (CacheType) annotationAttributes.get("type");
switch (type) {
case LOCAL: {
return new String[]{LocalServicempl.class.getName()};
}
case REDIS: {
return new String[]{RedisServicempl.class.getName()};
}
default: {
throw new RuntimeException(MessageFormat.format("unsupport cache type {0}", type.toString()));
}
}
}
}
3)、定义注解
@EnableMyCache
注解就像一个开关,通过这个开关来是否将特定的Bean注入容器。
定义一个枚举
public enum CacheType {
LOCAL, REDIS;
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(MyCacheSelector.class)
public @interface EnableMyCache {
CacheType type() default CacheType.REDIS;
}
4)、测试
这里选择本地缓存:
@EnableMyCache(type = CacheType.LOCAL)
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserServiceTest {
@Autowired
private CacheService cacheService;
@Test
public void test() {
cacheService.setData("key");
}
}
切换成redis缓存
@EnableMyCache(type = CacheType.REDIS)
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserServiceTest {
@Autowired
private CacheService cacheService;
@Test
public void test() {
cacheService.setData("key");
}
}
2.2 扫描包批量注入
这种方式实现了将我们想要交给Spring管理的类进行了托管,但是,如果有N多个类的话,这种写法会累死的,因此,需要通过递归的方式加载包路径名,然后统一初始化,将该包路径下的所有类进行湿实例化。首先我们自己创建一个注解:MyImport,并加入Spring的@Import注解,然后将启动类中的@Import注解改成我们自定义注解。
目的:扫描 "com.springboot.demo.model"下面的所有类,初始化并交给Spring容器管理
自定义注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ModelImportSelector.class)
public @interface MyImport {
String [] packages();
}
启动类加入自定义注解:
@SpringBootApplication
@MyImport(packages = "com.springboot.demo.model")
public class DemoApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run( DemoApplication.class, args );
context.close();
}
}
重写selectImports方法:
public class ModelImportSelector implements ImportSelector {
private List<String> classList=new ArrayList <>();
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
//通过字节码文件解读,获取元素据信息,通过注解名称,返回注解所对应的属性的Map集合
String [] packages = (String [])annotationMetadata.getAnnotationAttributes( MyImport.class.getName() ).get( "packages" );
if (packages == null) {
return null;
}
scanPackagesRecursion(packages);
//获取注解中packages中配置的内容
//获取需要扫描的包的所有类的路径
return !classList.isEmpty()?classList.toArray( new String[classList.size()] ):null;
}
private void scanPackagesRecursion(String [] packages){
for(String path : packages){
doScanPackages(path);
}
}
/**
* 方法递归
* @param path
*/
private void doScanPackages(String path){
URL resource = this.getClass().getClassLoader().
getResource( path.replaceAll( "\\.", "/" ) );
File file = new File( resource.getFile() );
File[] files=file.listFiles();
for (File fileSub : files) {
if(fileSub.isDirectory()){
doScanPackages(path+"."+fileSub.getName());
}else{
String fileName=fileSub.getName();
System.out.println("fileName:"+fileName);
if(fileName.endsWith( ".class" )){
String classPath=path+"."+fileName.replaceAll( "\\.class","" );
this.classList.add( classPath );
System.out.println("classPath:"+classPath);
}
}
}
}
}
2.3 Spring EnableAsync实现
SpringBoot有两个常用注解 @EnableAsync @EnableCaching 其实就是通过ImportSelector来动态注入Bean。看下@EnableAsync注解,它有通过@Import({AsyncConfigurationSelector.class})
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({AsyncConfigurationSelector.class})
public @interface EnableAsync {
Class<? extends Annotation> annotation() default Annotation.class;
boolean proxyTargetClass() default false;
AdviceMode mode() default AdviceMode.PROXY;
int order() default 2147483647;
}
AsyncConfigurationSelector.class
public class AsyncConfigurationSelector extends AdviceModeImportSelector<EnableAsync> {
private static final String ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME = "org.springframework.scheduling.aspectj.AspectJAsyncConfiguration";
public AsyncConfigurationSelector() {
}
@Nullable
public String[] selectImports(AdviceMode adviceMode) {
switch(adviceMode) {
case PROXY:
return new String[]{ProxyAsyncConfiguration.class.getName()};
case ASPECTJ:
return new String[]{"org.springframework.scheduling.aspectj.AspectJAsyncConfiguration"};
default:
return null;
}
}
}
三、ImportBeanDefinitionRegister的实现类
当配置类实现了 ImportBeanDefinitionRegistrar 接口,你就可以自定义往容器中注册想注入的Bean。这个接口相比与 ImportSelector 接口的主要区别就是,ImportSelector接口是返回一个类,你不能对这个类进行任何操作,但是 ImportBeanDefinitionRegistrar 是可以自己注入 BeanDefinition,可以添加属性之类的。
public class MyImportBean implements ImportBeanDefinitionRegistrar {
/**
* @param importingClassMetadata 当前类的注解信息
* @param registry 注册类,其registerBeanDefinition()可以注册bean
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
}
}
3.1 简单的示例
我们通过先通过一个简单的小示例,来理解它的基本使用,假设有个用户配置类如下:
@Data
public class UserConfig {
/** 用户名*/
private String username;
/**手机号*/
private String phone;
}
我们通过实现ImportBeanDefinitionRegistrar的方式来完成注入。
public class MyImportBean implements ImportBeanDefinitionRegistrar {
/**
* @param importingClassMetadata 当前类的注解信息
* @param registry 注册类,其registerBeanDefinition()可以注册bean
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//构建一个 BeanDefinition , Bean的类型为 UserConfig,这个Bean的属性username的值为后端元宇宙
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserConfig.class)
.addPropertyValue("username", "后端元宇宙")
.getBeanDefinition();
//把 UserConfig 这个Bean的定义注册到容器中
registry.registerBeanDefinition("userConfig", beanDefinition);
}
}
通过配置类 中引入MyImportBean对象。
@Import(MyImportBean.class)
@Configuration
public class UserImportConfiguration {
}
我们再来测试下:
@EnableMyCache(type = CacheType.REDIS)
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserServiceTest {
@Autowired
private UserConfig userConfig;
@Test
public void test() {
String username = userConfig.getUsername();
System.out.println("username = " + username);
}
}
说明通过ImportBeanDefinitionRegistrar方式,已经把UserConfig注入容器成功,而且还为给bean设置了新属性。
然后我们再来思考一个问题,就比如我们在其它地方已经将UserConfig注入容器,这里会不会出现冲突,或者不冲突的情况下,属性能不能设置成功?
我们来试下
@Import(MyImportBean.class)
@Configuration
public class UserImportConfiguration {
/**
* 这里通过@Bean注解,将UserConfig注入Spring容器中,而且名称也叫userConfig
*/
@Bean
public UserConfig userConfig() {
return new UserConfig();
}
}
3.2 Mybatis的@MapperScan
Mybatis的@MapperScan就是用这种方式实现的,@MapperScan注解,指定basePackages,扫描Mybatis Mapper接口类注入到容器中。
这里我们自定义一个注解@MyMapperScan
来扫描包路径下所以带@MapperBean
注解的类,并将它们注入到IOC容器中。
1)、先定义一个@MapperBean
注解,就相当于我们的@Mapper注解
/**
* 定义包路径。(指定包下所有添加了MapperBean注解的类作为bean)
* 注意这里 @Import(MyMapperScanImportBean.class) 的使用
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface MapperBean {
}
2)、一个需要注入的bean,这里加上@MapperBean
注解。
package com.jincou.importselector.mapperScan;
import com.jincou.importselector.config.MapperBean;
@MapperBean
public class User {
}
3)、再定一个扫描包路径的注解@MyMapperScan
就相当于mybatis的@MapperScan注解。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MyMapperScanImportBean.class)
public @interface MyMapperScan {
/**
* 扫描包路径
*/
String[] basePackages() default {};
}
4)、MyMapperScanImportBean实现ImportBeanDefinitionRegistrar接口
public class MyMapperScanImportBean implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
private final static String PACKAGE_NAME_KEY = "basePackages";
private ResourceLoader resourceLoader;
/**
* 搜索指定包下所有添加了MapperBean注解的类,并且把这些类添加到ioc容器里面去
*
* @param importingClassMetadata 当前类的注解信息
* @param registry 注册类,其registerBeanDefinition()可以注册bean
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//1. 从BeanIocScan注解获取到我们要搜索的包路径
AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MyMapperScan.class.getName()));
if (annoAttrs == null || annoAttrs.isEmpty()) {
return;
}
String[] basePackages = (String[]) annoAttrs.get(PACKAGE_NAME_KEY);
// 2. 找到指定包路径下所有添加了MapperBean注解的类,并且把这些类添加到IOC容器里面去
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry, false);
scanner.setResourceLoader(resourceLoader);
//路径包含MapperBean的注解的bean
scanner.addIncludeFilter(new AnnotationTypeFilter(MapperBean.class));
//扫描包下路径
scanner.scan(basePackages);
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
}
5)测试
这里扫描的路径就是上面User实体的位置
@RunWith(SpringRunner.class)
@SpringBootTest
@MyMapperScan(basePackages = {"com.jincou.importselector.mapperScan"})
public class UserServiceTest {
@Autowired
private User user;
@Test
public void test() {
System.out.println("username = " + user.getClass().getName());
}
}
运行结果:
username = com.jincou.importselector.mapperScan.User
四、区别:
ImportBeanDefinitionRegister接口相比与 ImportSelector 接口的主要区别就是,ImportSelector接口是返回一个类,你不能对这个类进行任何操作,但是 ImportBeanDefinitionRegistrar 是可以自己注入 BeanDefinition,可以添加属性之类的。
标签:String,Spring,Boot,class,ImportSelector,Import,注解,public,JavaWeb From: https://blog.csdn.net/zhyooo123/article/details/142950555