Spring之@Import
前言
在平常开发中我们自己开发的组件通常我们可以通过Spring的XML配置文件
,注解(例如@Component)
,配置类(例如@Configuration)
等方式将组件注入到容器中。但是通常情况下对于第三方开发的组件,我们很难通过上面的方式来完成。并且想动态的将组件注册到容器中,实现起也相对麻烦。例如下面的例子:
public class App {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(GlobalConfig.class);
//获取com.buydeem.springimport.UserService实例
System.out.println(context.getBean(UserService.class));
//获取com.buydeem.package2.Person实例
System.out.println(context.getBean(Person.class));
}
}
@ComponentScan
@Configuration
class GlobalConfig{
}
@Component
public class Person {
}
@Component
public class UserService {
}
目录结果
在上面的代码示例中,容器中只能获取到UserService
的实例,但是对于Person
的实例并不能获取到。这是因为通常情况下,@ComponentScan
注解扫描的包为单前配置类下的包,而Person
实例与GlobalConfig
并不在同一包下,而对于第三方组件就是这种情况。当然我们也可以通过设置@ComponentScan
自定义扫描包来将Person
实例注入到容器,不过对于推荐约定大于配置的今天来说,这种方式并不被推荐。
@Import
对于上面的示例中,因为Person
类所在的包路径并不是包扫描的路径所以无法被注册到容器中,有没有什么简单的方式能将其注入到容器呢?最简单的方式就是通过@Import
注解将Person
导入到容器中完成注入。修改代码如下:
@ComponentScan
@Configuration
@Import(Person.class)
class GlobalConfig{
}
通常情况下,@Import
有三种使用方式:
- 导入一个类作为Spring Bean注册到容器中
-
@Import
注解和ImportSelector
组合使用 -
@Import
注解和ImportBeanDefinitionRegistrar
组合使用
直接导入类注册到容器
前面示例中我们使用的就是这种方式,通过在注解@Import
中设置导入类,将普通的一个类导入到容器中。不过需要注意的是,在Spring4.2
之前是无法将一个普通的类导入到容器中,但是在Spring4.2
之后这是允许的,关于这一点可以参考Spring官方文档中Using the @Import
Annotation中的描述。
@Import
和ImportSelector
组合使用
前面我们说过,@Import
方式会更加灵活。但是目前为止,并没有何处体现出它的灵活之处,而使用ImportSelector
我们可以根据相关环境来决定注入哪些类。该接口中只有一个方法selectImports
,该返回一个字符串数组,数组中的值则是要注入的Spring Bean。需要注意的一点是,如果没有需要注入的组件,不能返回null,需要返回一个空的数组。例如现在有一个这样的需求,我们需要向容器中注入一个日志类的实现,这个日志需要根据相关设置动态的来注入。我们可以通过ImportSelector
来完成。
- 日志接口和实现
public interface LogService {
/**
* 打印日志
* @param log
*/
void printLog(String log);
}
public class LogAServiceImpl implements LogService{
@Override
public void printLog(String log) {
System.out.printf("日志A:[%s]",log);
}
}
public class LogBServiceImpl implements LogService{
@Override
public void printLog(String log) {
System.out.printf("日志B:[%s]",log);
}
}
-
LogImportSelector
和EnableLog
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(value = {LogImportSelector.class})
public @interface EnableLog {
String value() default "a";
}
public class LogImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
Map<String, Object> map = importingClassMetadata.getAnnotationAttributes(EnableLog.class.getName(), true);
String value = (String) map.get("value");
if (Objects.equals(value,"a")){
return new String[]{LogAServiceImpl.class.getName()};
}
return new String[]{LogBServiceImpl.class.getName()};
}
}
- 测试实例类
@EnableLog(value = "b")
public class App {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(App.class);
LogService logService = context.getBean(LogService.class);
logService.printLog("日志内容");
}
}
运行上面的代码,如果@EnableLog
中的值为a
时打印的结果为:
日志A:[日志内容]
如果@EnableLog
中的值为b
时打印的结果为:
日志B:[日志内容]
@EnableXXX
的秘密
上面的示例代码中,LogImportSelector#selectImports()
方法通过AnnotationMetadata
获取到注解EnableLog
中的值,根据这个值的配置来动态的确认注入哪个LogService
的实现,而这种方式就是Spring中@EnableXXX
的实现。例如@EnableAsync
注解,一般在方法上面加上@Async
注解,就可以让这个方法变成异步执行(简单的说就是使用线程中的一个线程来执行,而不是调用该方法的线程)。不过想要实现这样的效果,前提条件是需要使用@EnableAsync
开启,下面我们来看看源码中是如何实现的。
下面是AdviceModeImportSelector#selectImports()
方法的实现
public final String[] selectImports(AnnotationMetadata importingClassMetadata) {
//获取到注解
Class<?> annoType = GenericTypeResolver.resolveTypeArgument(getClass(), AdviceModeImportSelector.class);
//获取注解里面的值
AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(importingClassMetadata, annoType);
if (attributes == null) {
throw new IllegalArgumentException(String.format(
"@%s is not present on importing class '%s' as expected",
annoType.getSimpleName(), importingClassMetadata.getClassName()));
}
//获取是基于JDK动态代理还是基于AspectJ做动态代理的值
AdviceMode adviceMode = attributes.getEnum(this.getAdviceModeAttributeName());
//该方法由子类实现
String[] imports = selectImports(adviceMode);
if (imports == null) {
throw new IllegalArgumentException(String.format("Unknown AdviceMode: '%s'", adviceMode));
}
return imports;
}
上面的方法中还有一个抽象方法未实现protected abstract String[] selectImports(AdviceMode adviceMode)
,我们可以看AsyncConfigurationSelector
中对该方法的实现。
public String[] selectImports(AdviceMode adviceMode) {
switch (adviceMode) {
case PROXY:
return new String[] { ProxyAsyncConfiguration.class.getName() };
case ASPECTJ:
return new String[] { ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME };
default:
return null;
}
}
上面代码的逻辑也比较简单,就是根据传入的AdviceMode
枚举值来判断是使用哪种动态代理实现方式,从而注入哪个类到容器中。
@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 Ordered.LOWEST_PRECEDENCE;
}
从EnableAsync
注解可以知道,在默认情况下AdviceMode
的默认值为AdviceMode.PROXY
也就是默认情况下是使用JDK动态代理。所以说默认情况下注入的Bean为ProxyAsyncConfiguration
。简单的说就是,当你的方法使用了@Async
之后,通过容器获得的Bean不是Bean本身,而是一个经过加强后的代理Bean。例如我们可以将LogService#printLog
方法使用@Async
标记,然后从容器中获取LogService
并打印出它的类名,通过类名我们可以知道它是一个代理类。
@Import
和ImportBeanDefinitionRegistrar
组合使用
通过前面的内容我们了解到,我们可以通过@Import
注解导入一个配置类或者一个ImportSelector
子类,同样还可以导入一个ImportBeanDefinitionRegistrar
子类。如果导入的是一个普通类时,容器会创建一个Bean并注册到容器中。如果导入的是一个ImportSelector
子类时,则会创建方法selectImports
返回的类集合。而ImportBeanDefinitionRegistrar
的功能同样如此,该接口只有一个方法registerBeanDefinitions
,这个方法的特别之处在于该方法的入参提供了BeanDefinitionRegistry
实例,而有了BeanDefinitionRegistry
则意味着我们可以通过注册BeanDefinition
的方式向容器中注入Bean。
public interface ImportBeanDefinitionRegistrar {
public void registerBeanDefinitions(
AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry);
}
简单应用
例如现在我们有一个Dog
,我们通过ImportBeanDefinitionRegistrar
导入的方式来完成注入。
//Dog类
public class Dog {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
//ImportBeanDefinitionRegistrar实现类
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AbstractBeanDefinition dogBd = BeanDefinitionBuilder.genericBeanDefinition(Dog.class)
.addPropertyValue("name", "大黄")
.getBeanDefinition();
registry.registerBeanDefinition("dog",dogBd);
}
}
//使用示例
@Import(value = MyImportBeanDefinitionRegistrar.class)
public class App {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(App.class);
Dog dog = context.getBean(Dog.class);
System.out.println(dog.getName());
}
}
运行代码结果打出我们预想中的结果大黄
。之所以我们能从容器中获取到Dog
实例,是因为MyImportBeanDefinitionRegistrar#registerBeanDefinitions
方法向容器中注册了Dog
的BeanDefinition
。
进阶应用
从上面的示例中来看,并没有体现出什么高级灵活的地方。假如现在我们有这样一个需求,我们需要把某个包下类都注入到容器中,同时使用一个注解来标记只有类上带有这个标记的类我们才注入,并不是所有的类都注入到容器中。面对这种自定义的注入需求,使用ImportBeanDefinitionRegistrar
就能很轻松的完成我们的需求。我们将实现分为如下几步:
定义开启扫描包的注解和标记注解
//该注解用来指定扫描包
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(value = {PackageImportBeanDefinitionRegistrar.class})
public @interface PackageScan {
String[] basePackages() default {};
}
//该注解用来指定哪些类需要被注入到容器
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CustomInjection {
}
实现扫描并注入功能
public class PackageImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
//获取注解上的属性
Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(PackageScan.class.getName());
//扫描包
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
//添加过滤器扫描含有注解CustomInjection的类
provider.addIncludeFilter(new AnnotationTypeFilter(CustomInjection.class));
//扫描包下的BeanDefinition
String[] basePackages = (String[]) attributes.get("basePackages");
Set<BeanDefinition> candidateComponents = new LinkedHashSet<>();
for (String basePackage : basePackages) {
Set<BeanDefinition> components = provider.findCandidateComponents(basePackage);
candidateComponents.addAll(components);
}
//注册BeanDefinition
for (BeanDefinition component : candidateComponents) {
beanDefinitionRegistry.registerBeanDefinition(component.getBeanClassName(),component);
}
}
}
上面代码的整体实现逻辑比较简单,首先是获取到PackageScan
中需要指定的包路名集合,接着就是使用ClassPathScanningCandidateComponentProvider
获取到包包中的BeanDefinition
,最后通过BeanDefinitionRegistry
将找到的BeanDefinition
注入到容器。可能难理解的就是获取包下BeanDefinition
时使用的ClassPathScanningCandidateComponentProvider
,该类由Spring
提供的一个工具类,它的主要功能就是可以帮助我们从包路径中获取到所需的 BeanDefinition
集合。
@MapperScans
的实现
在使用Mybatis+Spring
集成时我们会用到一个工具包mybatis-spring
,在该工具包中提供了一个@MapperScans
注解,通过该注解我们可以指定扫描包下的Mapper注入到Spring容器中。该注解的实现其实也是借助了@Import
和ImportBeanDefinitionRegistrar
,它的源码如下:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
//省略部分内容
}
通过注解上的@Import(MapperScannerRegistrar.class)
可以知道,它的实现类为MapperScannerRegistrar
。
public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
//省略部分代码
void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
BeanDefinitionRegistry registry, String beanName) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
//省略部分代码
registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
}
MapperScannerRegistrar
内部要做的事简单来说就是将MapperScannerConfigurer
注入到Spring
容器中。
总结
通过上面的内容我们了解到,@Import
主要有三种使用方式,不管哪种方式其主要目的就是为了动态而灵活的将组建注入到容器中。虽然我们平时很少使用,但是在很多源码中我们会看到,特别是在SpringBoot
中使用的特别多,它的主要使用场景还是在Spring和第三方组建整合的场景。
本文的示例代码地址:https://gitee.com/zengchao_workspace/spring-import.git
网友评论