Spring之@Import
前言
在平常開發(fā)中我們自己開發(fā)的組件通常我們可以通過Spring的XML配置文件
,注解(例如@Component)
,配置類(例如@Configuration)
等方式將組件注入到容器中。但是通常情況下對于第三方開發(fā)的組件,我們很難通過上面的方式來完成。并且想動態(tài)的將組件注冊到容器中,實現(xiàn)起也相對麻煩。例如下面的例子:
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
并不在同一包下,而對于第三方組件就是這種情況。當(dāng)然我們也可以通過設(shè)置@ComponentScan
自定義掃描包來將Person
實例注入到容器,不過對于推薦約定大于配置的今天來說,這種方式并不被推薦。
@Import
對于上面的示例中,因為Person
類所在的包路徑并不是包掃描的路徑所以無法被注冊到容器中,有沒有什么簡單的方式能將其注入到容器呢?最簡單的方式就是通過@Import
注解將Person
導(dǎo)入到容器中完成注入。修改代碼如下:
@ComponentScan
@Configuration
@Import(Person.class)
class GlobalConfig{
}
通常情況下,@Import
有三種使用方式:
- 導(dǎo)入一個類作為Spring Bean注冊到容器中
-
@Import
注解和ImportSelector
組合使用 -
@Import
注解和ImportBeanDefinitionRegistrar
組合使用
直接導(dǎo)入類注冊到容器
前面示例中我們使用的就是這種方式,通過在注解@Import
中設(shè)置導(dǎo)入類,將普通的一個類導(dǎo)入到容器中。不過需要注意的是,在Spring4.2
之前是無法將一個普通的類導(dǎo)入到容器中,但是在Spring4.2
之后這是允許的,關(guān)于這一點可以參考Spring官方文檔中Using the @Import
Annotation中的描述。
@Import
和ImportSelector
組合使用
前面我們說過,@Import
方式會更加靈活。但是目前為止,并沒有何處體現(xiàn)出它的靈活之處,而使用ImportSelector
我們可以根據(jù)相關(guān)環(huán)境來決定注入哪些類。該接口中只有一個方法selectImports
,該返回一個字符串?dāng)?shù)組,數(shù)組中的值則是要注入的Spring Bean。需要注意的一點是,如果沒有需要注入的組件,不能返回null,需要返回一個空的數(shù)組。例如現(xiàn)在有一個這樣的需求,我們需要向容器中注入一個日志類的實現(xiàn),這個日志需要根據(jù)相關(guān)設(shè)置動態(tài)的來注入。我們可以通過ImportSelector
來完成。
- 日志接口和實現(xiàn)
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("日志內(nèi)容");
}
}
運行上面的代碼,如果@EnableLog
中的值為a
時打印的結(jié)果為:
日志A:[日志內(nèi)容]
如果@EnableLog
中的值為b
時打印的結(jié)果為:
日志B:[日志內(nèi)容]
@EnableXXX
的秘密
上面的示例代碼中,LogImportSelector#selectImports()
方法通過AnnotationMetadata
獲取到注解EnableLog
中的值,根據(jù)這個值的配置來動態(tài)的確認(rèn)注入哪個LogService
的實現(xiàn),而這種方式就是Spring中@EnableXXX
的實現(xiàn)。例如@EnableAsync
注解,一般在方法上面加上@Async
注解,就可以讓這個方法變成異步執(zhí)行(簡單的說就是使用線程中的一個線程來執(zhí)行,而不是調(diào)用該方法的線程)。不過想要實現(xiàn)這樣的效果,前提條件是需要使用@EnableAsync
開啟,下面我們來看看源碼中是如何實現(xiàn)的。
下面是AdviceModeImportSelector#selectImports()
方法的實現(xiàn)
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動態(tài)代理還是基于AspectJ做動態(tài)代理的值
AdviceMode adviceMode = attributes.getEnum(this.getAdviceModeAttributeName());
//該方法由子類實現(xiàn)
String[] imports = selectImports(adviceMode);
if (imports == null) {
throw new IllegalArgumentException(String.format("Unknown AdviceMode: '%s'", adviceMode));
}
return imports;
}
上面的方法中還有一個抽象方法未實現(xiàn)protected abstract String[] selectImports(AdviceMode adviceMode)
,我們可以看AsyncConfigurationSelector
中對該方法的實現(xiàn)。
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;
}
}
上面代碼的邏輯也比較簡單,就是根據(jù)傳入的AdviceMode
枚舉值來判斷是使用哪種動態(tài)代理實現(xiàn)方式,從而注入哪個類到容器中。
@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
注解可以知道,在默認(rèn)情況下AdviceMode
的默認(rèn)值為AdviceMode.PROXY
也就是默認(rèn)情況下是使用JDK動態(tài)代理。所以說默認(rèn)情況下注入的Bean為ProxyAsyncConfiguration
。簡單的說就是,當(dāng)你的方法使用了@Async
之后,通過容器獲得的Bean不是Bean本身,而是一個經(jīng)過加強(qiáng)后的代理Bean。例如我們可以將LogService#printLog
方法使用@Async
標(biāo)記,然后從容器中獲取LogService
并打印出它的類名,通過類名我們可以知道它是一個代理類。
@Import
和ImportBeanDefinitionRegistrar
組合使用
通過前面的內(nèi)容我們了解到,我們可以通過@Import
注解導(dǎo)入一個配置類或者一個ImportSelector
子類,同樣還可以導(dǎo)入一個ImportBeanDefinitionRegistrar
子類。如果導(dǎo)入的是一個普通類時,容器會創(chuàng)建一個Bean并注冊到容器中。如果導(dǎo)入的是一個ImportSelector
子類時,則會創(chuàng)建方法selectImports
返回的類集合。而ImportBeanDefinitionRegistrar
的功能同樣如此,該接口只有一個方法registerBeanDefinitions
,這個方法的特別之處在于該方法的入?yún)⑻峁┝?code>BeanDefinitionRegistry實例,而有了BeanDefinitionRegistry
則意味著我們可以通過注冊BeanDefinition
的方式向容器中注入Bean。
public interface ImportBeanDefinitionRegistrar {
public void registerBeanDefinitions(
AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry);
}
簡單應(yīng)用
例如現(xiàn)在我們有一個Dog
,我們通過ImportBeanDefinitionRegistrar
導(dǎo)入的方式來完成注入。
//Dog類
public class Dog {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
//ImportBeanDefinitionRegistrar實現(xiàn)類
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());
}
}
運行代碼結(jié)果打出我們預(yù)想中的結(jié)果大黃
。之所以我們能從容器中獲取到Dog
實例,是因為MyImportBeanDefinitionRegistrar#registerBeanDefinitions
方法向容器中注冊了Dog
的BeanDefinition
。
進(jìn)階應(yīng)用
從上面的示例中來看,并沒有體現(xiàn)出什么高級靈活的地方。假如現(xiàn)在我們有這樣一個需求,我們需要把某個包下類都注入到容器中,同時使用一個注解來標(biāo)記只有類上帶有這個標(biāo)記的類我們才注入,并不是所有的類都注入到容器中。面對這種自定義的注入需求,使用ImportBeanDefinitionRegistrar
就能很輕松的完成我們的需求。我們將實現(xiàn)分為如下幾步:
定義開啟掃描包的注解和標(biāo)記注解
//該注解用來指定掃描包
@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 {
}
實現(xiàn)掃描并注入功能
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);
}
}
}
上面代碼的整體實現(xiàn)邏輯比較簡單,首先是獲取到PackageScan
中需要指定的包路名集合,接著就是使用ClassPathScanningCandidateComponentProvider
獲取到包包中的BeanDefinition
,最后通過BeanDefinitionRegistry
將找到的BeanDefinition
注入到容器。可能難理解的就是獲取包下BeanDefinition
時使用的ClassPathScanningCandidateComponentProvider
,該類由Spring
提供的一個工具類,它的主要功能就是可以幫助我們從包路徑中獲取到所需的 BeanDefinition
集合。
@MapperScans
的實現(xiàn)
在使用Mybatis+Spring
集成時我們會用到一個工具包mybatis-spring
,在該工具包中提供了一個@MapperScans
注解,通過該注解我們可以指定掃描包下的Mapper注入到Spring容器中。該注解的實現(xiàn)其實也是借助了@Import
和ImportBeanDefinitionRegistrar
,它的源碼如下:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
//省略部分內(nèi)容
}
通過注解上的@Import(MapperScannerRegistrar.class)
可以知道,它的實現(xiàn)類為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
內(nèi)部要做的事簡單來說就是將MapperScannerConfigurer
注入到Spring
容器中。
總結(jié)
通過上面的內(nèi)容我們了解到,@Import
主要有三種使用方式,不管哪種方式其主要目的就是為了動態(tài)而靈活的將組建注入到容器中。雖然我們平時很少使用,但是在很多源碼中我們會看到,特別是在SpringBoot
中使用的特別多,它的主要使用場景還是在Spring和第三方組建整合的場景。
本文的示例代碼地址:https://gitee.com/zengchao_workspace/spring-import.git