月薪5萬,恭喜你,面了幾百人,這些問題你是第一個讓我比較滿意的,且超出了預期!
路人甲Java 今天
來看一下月薪5萬的面試題:
- @Import你用過麼?是做什麼的?
- @Import使用有幾種方式?有何區別?
- DeferredImportSelector是做什麼的?他和ImportSelector有什麼區別?
- 可以介紹介紹一下spring中哪些功能是通過@Import來實現的?
- 可以介紹一下spring中是如何解析@Import註解的麼?
@Import出現的背景
目前為止,註解的方式批量註冊bean,前面2篇文章中,我們介紹了2種方式:
到目前,我們知道的批量定義bean的方式有2種:
- @Configuration結合@Bean註解的方式
- @CompontentScan掃描包的方式
下面我們來看幾個問題。
問題1
如果需要註冊的類是在第三方的jar中,那麼我們如果想註冊這些bean有2種方式:
- 通過@Bean標註方法的方式,一個個來註冊
- @CompontentScan的方式:默認的@CompontentScan是無能為力的,默認情況下只會註冊@Compontent標註的類,此時只能自定義@CompontentScan中的過濾器來實現了
這2種方式都不是太好,每次有變化,調整的代碼都比較多。
問題2
通常我們的項目中有很多子模塊,可能每個模塊都是獨立開發的,最後通過jar的方式引進來,每個模塊中都有各自的@Configuration、@Bean標註的類,或者使用@CompontentScan標註的類,被@Configuration、@Bean、@CompontentScan標註的類,我們統稱為bean配置類,配置類可以用來註冊bean ,此時如果我們只想使用其中幾個模塊的配置類,怎麼辦?
@Import可以很好的解決這2個問題,下面我們來看@Import怎麼玩的。
@Import使用
先看Spring對它的註釋,總結下來作用就是和xml配置的 <import>標籤作用一樣,允許通過它引入@Configuration標註的類 , 引入ImportSelector接口和ImportBeanDefinitionRegistrar接口的實現,也包括 @Component註解的普通類。
總的來說:@Import可以用來批量導入需要註冊的各種類,如普通的類、配置類,完後完成普通類和配置類中所有bean的註冊。
@Import的源碼:
<code>@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
/**
* {@link Configuration @Configuration}, {@link ImportSelector},
* {@link ImportBeanDefinitionRegistrar}, or regular component classes to import.
*/
Class>[] value();
}/<code>
@Import可以使用在任何類型上,通常情況下,類和註解上用的比較多。
value:一個Class數組,設置需要導入的類,可以是@Configuration標註的列,可以是ImportSelector接口或者ImportBeanDefinitionRegistrar接口類型的,或者需要導入的普通組件類。
使用步驟
- 將@Import標註在類上,設置value參數
- 將@Import標註的類作為AnnotationConfigApplicationContext構造參數創建AnnotationConfigApplicationContext對象
- 使用AnnotationConfigApplicationContext對象
@Import的value常見的有5種用法
- value為普通的類
- value為@Configuration標註的類
- value為@CompontentScan標註的類
- value為ImportBeanDefinitionRegistrar接口類型
- value為ImportSelector接口類型
- value為DeferredImportSelector接口類型
下面我們通過案例來一個個詳細介紹。
value為普通的類
來2個類
Service1
<code>package com.javacode2018.lesson001.demo24.test1;
public class Service1 {
}/<code>
Service2
<code>package com.javacode2018.lesson001.demo24.test1;
public class Service2 {
}/<code>
總配置類:使用@Import標註
<code>package com.javacode2018.lesson001.demo24.test1;
import org.springframework.context.annotation.Import;
@Import({Service1.class, Service2.class})
public class MainConfig1 {
}/<code>
@Import中導入了2個普通的類:Service1、Service2,這兩個類會被自動註冊到容器中
測試用例
<code>package com.javacode2018.lesson001.demo24;
import com.javacode2018.lesson001.demo24.test1.MainConfig1;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ImportTest {
@Test
public void test1() {
//1.通過AnnotationConfigApplicationContext創建spring容器,參數為@Import標註的類
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig1.class);
//2.輸出容器中定義的所有bean信息
for (String beanName : context.getBeanDefinitionNames()) {
System.out.println(String.format("%s->%s", beanName, context.getBean(beanName)));
}
}
}/<code>
運行輸出
部分輸出如下:
<code>com.javacode2018.lesson001.demo24.test1.Service1->com.javacode2018.lesson001.demo24.test1.Service1@7e0b85f9
com.javacode2018.lesson001.demo24.test1.Service2->com.javacode2018.lesson001.demo24.test1.Service2@63355449/<code>
結果分析
從輸出中可以看出:
- Service1和Service2成功註冊到容器了。
- 通過@Import導入的2個類,bean名稱為完整的類名
我們也可以指定被導入類的bean名稱,使用@Compontent註解就可以了,如下:
<code>@Component("service1")
public class Service1 {
}/<code>
再次運行test1輸出:
<code>service1->com.javacode2018.lesson001.demo24.test1.Service1@45efd90f/<code>
總結一下
按模塊的方式進行導入,需要哪個導入哪個,不需要的時候,直接修改一下總的配置類,調整一下@Import就可以了,非常方便。
value為@Configuration標註的配置類
項目比較大的情況下,會按照模塊獨立開發,每個模塊在maven中就表現為一個個的構建,然後通過座標的方式進行引入需要的模塊。
假如項目中有2個模塊,2個模塊都有各自的配置類,如下
模塊1配置類
<code>package com.javacode2018.lesson001.demo24.test2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 模塊1配置類
*/
@Configuration
public class ConfigModule1 {
@Bean
public String module1() {
return "我是模塊1配置類!";
}
}/<code>
模塊2配置類
<code>package com.javacode2018.lesson001.demo24.test2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 模塊2配置類
*/
@Configuration
public class ConfigModule2 {
@Bean
public String module2() {
return "我是模塊2配置類!";
}
}/<code>
總配置類:通過@Import導入2個模塊的配置類
<code>package com.javacode2018.lesson001.demo24.test2;
import org.springframework.context.annotation.Import;
/**
* 通過Import來彙總多個@Configuration標註的配置類
*/
@Import({ConfigModule1.class, ConfigModule2.class}) //@1
public class MainConfig2 {
}/<code>
@1導入了2個模塊中的模塊配置類,可以按需導入。
測試用例
ImportTest中新增個方法
<code>@Test
public void test2() {
//1.通過AnnotationConfigApplicationContext創建spring容器,參數為@Import標註的類
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig2.class);
//2.輸出容器中定義的所有bean信息
for (String beanName : context.getBeanDefinitionNames()) {
System.out.println(String.format("%s->%s", beanName, context.getBean(beanName)));
}
}/<code>
運行輸出
<code>mainConfig2->com.javacode2018.lesson001.demo24.test2.MainConfig2@ba2f4ec
com.javacode2018.lesson001.demo24.test2.ConfigModule1->com.javacode2018.lesson001.demo24.test2.ConfigModule1$$EnhancerBySpringCGLIB$$700e65cd@1c1bbc4e
module1->我是模塊1配置類!
com.javacode2018.lesson001.demo24.test2.ConfigModule2->com.javacode2018.lesson001.demo24.test2.ConfigModule2$$EnhancerBySpringCGLIB$$a87108ee@55fe41ea
module2->我是模塊2配置類!/<code>
value為@CompontentScan標註的類
項目中分多個模塊,每個模塊有各自獨立的包,我們在每個模塊所在的包中配置一個@CompontentScan類,然後通過@Import來導入需要啟用的模塊。
定義模塊1
2個組件和一個組件掃描類,模塊1所有類所在的包為:
<code>com.javacode2018.lesson001.demo24.test3.module1/<code>
組件1:Module1Service1
<code>package com.javacode2018.lesson001.demo24.test3.module1;
import org.springframework.stereotype.Component;
@Component
public class Module1Service1 {
}/<code>
組件2:Module1Service2
<code>package com.javacode2018.lesson001.demo24.test3.module1;
import org.springframework.stereotype.Component;
@Component
public class Module1Service2 {
}/<code>
組件掃描類:CompontentScanModule1
負責掃描當前模塊中的組件
<code>package com.javacode2018.lesson001.demo24.test3.module1;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Component;
/**
* 模塊1的主鍵掃描
*/
@ComponentScan
public class CompontentScanModule1 {
}/<code>
同樣的方式定義模塊2
2個組件和一個組件掃描類,模塊1所有類所在的包為:
<code>com.javacode2018.lesson001.demo24.test3.module2/<code>
組件1:Module2Service1
<code>package com.javacode2018.lesson001.demo24.test3.module2;
import org.springframework.stereotype.Component;
@Component
public class Module2Service1 {
}/<code>
組件2:Module2Service2
<code>package com.javacode2018.lesson001.demo24.test3.module2;
import org.springframework.stereotype.Component;
@Component
public class Module2Service2 {
}/<code>
組件掃描類:CompontentScanModule1
負責掃描當前模塊中的組件
<code>package com.javacode2018.lesson001.demo24.test3.module2;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Component;
/**
* 模塊2的組件掃描
*/
@ComponentScan
public class CompontentScanModule2 {
}/<code>
總配置類:通過@Import導入每個模塊中的組件掃描類
<code>package com.javacode2018.lesson001.demo24.test3;
import com.javacode2018.lesson001.demo24.test3.module1.CompontentScanModule1;
import com.javacode2018.lesson001.demo24.test3.module2.CompontentScanModule2;
import org.springframework.context.annotation.Import;
/**
* 通過@Import導入多個@CompontentScan標註的配置類
*/
@Import({CompontentScanModule1.class, CompontentScanModule2.class}) //@1
public class MainConfig3 {
}/<code>
@1導入了2個模塊中的組件掃描類,可以按需導入。
測試用例
ImportTest中新增個方法
<code>@Test
public void test3() {
//1.通過AnnotationConfigApplicationContext創建spring容器,參數為@Import標註的類
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig3.class);
//2.輸出容器中定義的所有bean信息
for (String beanName : context.getBeanDefinitionNames()) {
System.out.println(String.format("%s->%s", beanName, context.getBean(beanName)));
}
}/<code>
運行輸出
部分輸出如下:
<code>module1Service1->com.javacode2018.lesson001.demo24.test3.module1.Module1Service1@5b239d7d
module1Service2->com.javacode2018.lesson001.demo24.test3.module1.Module1Service2@6572421
module2Service1->com.javacode2018.lesson001.demo24.test3.module2.Module2Service1@6b81ce95
module2Service2->com.javacode2018.lesson001.demo24.test3.module2.Module2Service2@2a798d51/<code>
兩個模塊中通過@Compontent定義的4個bean都輸出了。
如果只想註冊模塊1中的bean,只需要修改一下@Import,去掉CompontentScanModule2,如下:
<code>@Import({CompontentScanModule1.class})/<code>
再次運行輸出:
<code>module1Service1->com.javacode2018.lesson001.demo24.test3.module1.Module1Service1@6379eb
module1Service2->com.javacode2018.lesson001.demo24.test3.module1.Module1Service2@294425a7/<code>
此時模塊2的bean就沒有了。
先來了解一下相關的幾個接口
ImportBeanDefinitionRegistrar接口
這個接口提供了通過spring容器api的方式直接向容器中註冊bean。
接口的完整名稱:
<code>org.springframework.context.annotation.ImportBeanDefinitionRegistrar/<code>
源碼:
<code>public interface ImportBeanDefinitionRegistrar {
default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
BeanNameGenerator importBeanNameGenerator) {
registerBeanDefinitions(importingClassMetadata, registry);
}
default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
}
}/<code>
2個默認方法,都可以用來調用spring容器api來註冊bean。
2個方法中主要有3個參數
importingClassMetadata
AnnotationMetadata類型的,通過這個可以獲取被@Import註解標註的類所有註解的信息。
registry
BeanDefinitionRegistry類型,是一個接口,內部提供了註冊bean的各種方法。
importBeanNameGenerator
BeanNameGenerator類型,是一個接口,內部有一個方法,用來生成bean的名稱。
關於BeanDefinitionRegistry和BeanNameGenerator接口在來細說一下。
BeanDefinitionRegistry接口:bean定義註冊器
bean定義註冊器,提供了bean註冊的各種方法,來看一下源碼:
<code>public interface BeanDefinitionRegistry extends AliasRegistry {
/**
* 註冊一個新的bean定義
* beanName:bean的名稱
* beanDefinition:bean定義信息
*/
void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException;
/**
* 通過bean名稱移除已註冊的bean
* beanName:bean名稱
*/
void removeBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;
/**
* 通過名稱獲取bean的定義信息
* beanName:bean名稱
*/
BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;
/**
* 查看beanName是否註冊過
*/
boolean containsBeanDefinition(String beanName);
/**
* 獲取已經定義(註冊)的bean名稱列表
*/
String[] getBeanDefinitionNames();
/**
* 返回註冊器中已註冊的bean數量
*/
int getBeanDefinitionCount();
/**
* 確定給定的bean名稱或者別名是否已在此註冊表中使用
* beanName:可以是bean名稱或者bean的別名
*/
boolean isBeanNameInUse(String beanName);
}/<code>
基本上所有bean工廠都實現了這個接口,讓bean工廠擁有bean註冊的各種能力。
上面我們用到的AnnotationConfigApplicationContext類也實現了這個接口。
BeanNameGenerator接口:bean名稱生成器
bean名稱生成器,這個接口只有一個方法,用來生成bean的名稱:
<code>public interface BeanNameGenerator {
String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry);
}/<code>
spring內置了3個實現
DefaultBeanNameGenerator
默認bean名稱生成器,xml中bean未指定名稱的時候,默認就會使用這個生成器,默認為:完整的類名#bean編號
AnnotationBeanNameGenerator
註解方式的bean名稱生成器,比如通過@Component(bean名稱)的方式指定bean名稱,如果沒有通過註解方式指定名稱,默認會將完整的類名作為bean名稱。
FullyQualifiedAnnotationBeanNameGenerator
將完整的類名作為bean的名稱
BeanDefinition接口:bean定義信息
用來表示bean定義信息的接口,我們向容器中註冊bean之前,會通過xml或者其他方式定義bean的各種配置信息,bean的所有配置信息都會被轉換為一個BeanDefinition對象,然後通過容器中BeanDefinitionRegistry接口中的方法,將BeanDefinition註冊到spring容器中,完成bean的註冊操作。
這個接口有很多實現類,有興趣的可以去看看源碼,BeanDefinition的各種用法,以後會通過專題細說。
value為ImportBeanDefinitionRegistrar接口類型
用法(4個步驟)
<code>1. 定義ImportBeanDefinitionRegistrar接口實現類,在registerBeanDefinitions方法中使用registry來註冊bean
2. 使用@Import來導入步驟1中定義的類
3. 使用步驟2中@Import標註的類作為AnnotationConfigApplicationContext構造參數創建spring容器
4. 使用AnnotationConfigApplicationContext操作bean/<code>
案例
來2個普通的類。
Service1
<code>package com.javacode2018.lesson001.demo24.test4;
public class Service1 {
}/<code>
Service2
這個類中需要注入Service1
<code>package com.javacode2018.lesson001.demo24.test4;
public class Service2 {
private Service1 service1;
public Service1 getService1() {
return service1;
}
public void setService1(Service1 service1) {
this.service1 = service1;
}
@Override
public String toString() {
return "Service2{" +
"service1=" + service1 +
'}';
}
}/<code>
來個類實現ImportBeanDefinitionRegistrar接口,然後在裡面實現上面2個類的註冊,如下:
MyImportBeanDefinitionRegistrar
<code>package com.javacode2018.lesson001.demo24.test4;
import org.springframework.beans.factory.config.BeanDefinition;
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;
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//定義一個bean:Service1
BeanDefinition service1BeanDinition = BeanDefinitionBuilder.genericBeanDefinition(Service1.class).getBeanDefinition();
//註冊bean
registry.registerBeanDefinition("service1", service1BeanDinition);
//定義一個bean:Service2,通過addPropertyReference注入service1
BeanDefinition service2BeanDinition = BeanDefinitionBuilder.genericBeanDefinition(Service2.class).
addPropertyReference("service1", "service1").
getBeanDefinition();
//註冊bean
registry.registerBeanDefinition("service2", service2BeanDinition);
}
}/<code>
注意上面的registerBeanDefinitions方法,內部註冊了2個bean,Service1和Service2。
上面使用了BeanDefinitionBuilder這個類,這個是BeanDefinition的構造器,內部提供了很多靜態方法方便構建BeanDefinition對象。
上面定義的2個bean,和下面xml方式效果一樣:
<code><bean>
<bean>
<property>
/<bean>/<code>
來個測試用例
ImportTest中新增個方法
<code>@Test
public void test4() {
//1.通過AnnotationConfigApplicationContext創建spring容器,參數為@Import標註的類
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig4.class);
//2.輸出容器中定義的所有bean信息
for (String beanName : context.getBeanDefinitionNames()) {
System.out.println(String.format("%s->%s", beanName, context.getBean(beanName)));
}
}/<code>
運行輸出
<code>service1->com.javacode2018.lesson001.demo24.test4.Service1@62150f9e
service2->Service2{service1=com.javacode2018.lesson001.demo24.test4.Service1@62150f9e}/<code>
value為ImportSelector接口類型
先來看一下ImportSelector接口
ImportSelector接口
導入選擇器,看一下源碼:
<code>public interface ImportSelector {
/**
* 返回需要導入的類名的數組,可以是任何普通類,配置類(@Configuration、@Bean、@CompontentScan等標註的類)
* @importingClassMetadata:用來獲取被@Import標註的類上面所有的註解信息
*/
String[] selectImports(AnnotationMetadata importingClassMetadata);
}/<code>
用法(4個步驟)
<code>1. 定義ImportSelector接口實現類,在selectImports返回需要導入的類的名稱數組
2. 使用@Import來導入步驟1中定義的類
3. 使用步驟2中@Import標註的類作為AnnotationConfigApplicationContext構造參數創建spring容器
4. 使用AnnotationConfigApplicationContext操作bean/<code>
案例
來個普通類:Service1
<code>package com.javacode2018.lesson001.demo24.test5;
public class Service1 {
}/<code>
來個@Configuration標註的配置類:Module1Config
<code>package com.javacode2018.lesson001.demo24.test5;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Module1Config {
@Bean
public String name() {
return "公眾號:路人甲java";
}
@Bean
public String address() {
return "上海市";
}
}/<code>
上面定義了兩個string類型的bean:name和address
下面自定義一個ImportSelector,然後返回上面2個類的名稱
<code>package com.javacode2018.lesson001.demo24.test5;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{
Service1.class.getName(),
Module1Config.class.getName()
};
}
}/<code>
來個@Import標註的類,導入MyImportSelector
<code>package com.javacode2018.lesson001.demo24.test5;
import com.javacode2018.lesson001.demo24.test4.MyImportBeanDefinitionRegistrar;
import org.springframework.context.annotation.Import;
/**
* 通過@Import導入MyImportSelector接口實現類
*/
@Import({MyImportSelector.class})
public class MainConfig5 {
}/<code>
新增測試用例
ImportTest中新增個方法
<code>@Test
public void test5() {
//1.通過AnnotationConfigApplicationContext創建spring容器,參數為@Import標註的類
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig5.class);
//2.輸出容器中定義的所有bean信息
for (String beanName : context.getBeanDefinitionNames()) {
System.out.println(String.format("%s->%s", beanName, context.getBean(beanName)));
}
}/<code>
運行輸出
部分輸出如下:
<code>com.javacode2018.lesson001.demo24.test5.Service1->com.javacode2018.lesson001.demo24.test5.Service1@45b4c3a9
name->公眾號:路人甲java
address->上海市/<code>
輸出中可以看到Service1以及Module1Config中定義的2個bean都有了。
來一個牛逼的案例
需求
凡是類名中包含service的,調用他們內部任何方法,我們希望調用之後能夠輸出這些方法的耗時。
實現分析
之前我們講過代理, 此處我們就可以通過代理來實現,bean實例創建的過程中,我們可以給這些bean生成一個代理,在代理中統計方法的耗時,這裡面有2點:
- 創建一個代理類,通過代理來間接訪問需要統計耗時的bean對象
- 攔截bean的創建,給bean實例生成代理生成代理
具體實現
先來兩個Service類
Service1
<code>package com.javacode2018.lesson001.demo24.test6;
import org.springframework.stereotype.Component;
@Component
public class Service1 {
public void m1() {
System.out.println(this.getClass() + ".m1()");
}
}/<code>
Service2
<code>package com.javacode2018.lesson001.demo24.test6;
import org.springframework.stereotype.Component;
@Component
public class Service2 {
public void m1() {
System.out.println(this.getClass() + ".m1()");
}
}/<code>
創建統計耗時的代理類
下面我們使用cglib來實現一個代理類,如下:
<code>package com.javacode2018.lesson001.demo24.test6;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class CostTimeProxy implements MethodInterceptor {
//目標對象
private Object target;
public CostTimeProxy(Object target) {
this.target = target;
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
long starTime = System.nanoTime();
//調用被代理對象(即target)的方法,獲取結果
Object result = method.invoke(target, objects); //@1
long endTime = System.nanoTime();
System.out.println(method + ",耗時(納秒):" + (endTime - starTime));
return result;
}
/**
* 創建任意類的代理對象
*
* @param target
* @param/<code>
* @return
*/
public staticT createProxy(T target) {
CostTimeProxy costTimeProxy = new CostTimeProxy(target);
Enhancer enhancer = new Enhancer();
enhancer.setCallback(costTimeProxy);
enhancer.setSuperclass(target.getClass());
return (T) enhancer.create();
}
}
createProxy方法可以用來給某個對象生成代理對象
需要了解cglib的可以看:代理詳解(Java動態代理&cglib代理)
攔截bean實例的創建,返回代理對象
這裡我們需要用到spring中的一個接口:
<code>org.springframework.beans.factory.config.BeanPostProcessor
public interface BeanPostProcessor {
/**
* bean初始化之後會調用的方法
*/
@Nullable
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
/**
* bean初始化之後會調用的方法
*/
@Nullable
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}/<code>
這個接口是bean處理器,內部有2個方法,分別在bean初始化前後會進行調用,以後講聲明週期的時候還會細說的,這裡你只需要知道bean初始化之後會調用postProcessAfterInitialization方法就行,這個方法中我們會給bean創建一個代理對象。
下面我們創建一個BeanPostProcessor實現類:
<code>package com.javacode2018.lesson001.demo24.test6;
import com.javacode2018.lesson001.demo23.test4.CostTimeProxy;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.lang.Nullable;
public class MethodCostTimeProxyBeanPostProcessor implements BeanPostProcessor {
@Nullable
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean.getClass().getName().toLowerCase().contains("service")) {
return CostTimeProxy.createProxy(bean); //@1
} else {
return bean;
}
}
}/<code>
@1:使用上面創建代理類來給當前bean對象創建一個代理
需要將MethodCostTimeProxyBeanPostProcessor註冊到容器中才會起作用,下面我們通過@Import結合ImportSelector的方式來導入這個類,將其註冊到容器中。
MethodCostTimeImportSelector
<code>package com.javacode2018.lesson001.demo24.test6;
import com.javacode2018.lesson001.demo23.test4.MethodCostTimeProxyBeanPostProcessor;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
public class MethodCostTimeImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{MethodCostTimeProxyBeanPostProcessor.class.getName()};
}
}/<code>
來一個@Import來導入MethodCostTimeImportSelector
下面我們使用註解的方式,在註解上使用@Import,如下:
<code>package com.javacode2018.lesson001.demo24.test6;
import com.javacode2018.lesson001.demo23.test4.MethodCostTimeImportSelector;
import org.springframework.context.annotation.Import;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(MethodCostTimeImportSelector.class)
public @interface EnableMethodCostTime {
}/<code>
來一個總的配置類
<code>package com.javacode2018.lesson001.demo24.test6;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan
@EnableMethodCostTime //@1
public class MainConfig6 {
}/<code>
上面使用了@CompontentScan註解,此時會將Servce1和Service2這兩個類註冊到容器中。
@1:此處使用了@EnableMethodCostTime註解,而@EnableMethodCostTime註解上使用了@Import(MethodCostTimeImportSelector.class),此時MethodCostTimeImportSelector類中的MethodCostTimeProxyBeanPostProcessor會被註冊到容器,會攔截bean的創建,創建耗時代理對象。
來個測試用例
ImportTest中新增個方法
<code>@Test
public void test6() {
//1.通過AnnotationConfigApplicationContext創建spring容器,參數為@Import標註的類
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig6.class);
Service1 service1 = context.getBean(Service1.class);
Service2 service2 = context.getBean(Service2.class);
service1.m1();
service2.m1();
}/<code>
上面會調用service1和service2的方法
運行輸出
<code>class com.javacode2018.lesson001.demo24.test6.Service1.m1()
public void com.javacode2018.lesson001.demo24.test6.Service1.m1(),耗時(納秒):74200
class com.javacode2018.lesson001.demo24.test6.Service2.m1()
public void com.javacode2018.lesson001.demo24.test6.Service2.m1(),耗時(納秒):33800/<code>
太牛逼了,需求實現了。
如果我們不想開啟方法耗時統計,只需要將MainConfig6上的@EnableMethodCostTime去掉就可以了,用起來是不是特別爽。
spring中有很多類似的註解,以@EnableXXX開頭的註解,基本上都是通過上面這種方式實現的,如:
<code>@EnableAspectJAutoProxy
@EnableCaching
@EnableAsync/<code>
繼續向下看,還有一個更牛逼的接口DeferredImportSelector。
DeferredImportSelector接口
先給你透露一下,springboot中的核心功能@EnableAutoConfiguration就是靠DeferredImportSelector來實現的。
DeferredImportSelector是ImportSelector的子接口,既然是ImportSelector的子接口,所以也可以通過@Import進行導入,這個接口和ImportSelector不同地方有兩點:
- 延遲導入
- 指定導入的類的處理順序
延遲導入
比如@Import的value包含了多個普通類、多個@Configuration標註的配置類、多個ImportSelector接口的實現類,多個ImportBeanDefinitionRegistrar接口的實現類,還有DeferredImportSelector接口實現類,此時spring處理這些被導入的類的時候,會將DeferredImportSelector類型的放在最後處理,會先處理其他被導入的類,其他類會按照value所在的前後順序進行處理。
那麼我們是可以做很多事情的,比如我們可以在DeferredImportSelector導入的類中判斷一下容器中是否已經註冊了某個bean,如果沒有註冊過,那麼再來註冊。
以後我們會講到另外一個註解@Conditional,這個註解可以按條件來註冊bean,比如可以判斷某個bean不存在的時候才進行註冊,某個類存在的時候才進行註冊等等各種條件判斷,通過@Conditional來結合DeferredImportSelector可以做很多事情。
來個延遲導入的案例
來3個配置類,每個配置類中都通過@Bean定一個string類型的bean,內部輸出一句文字。
Configuration1
<code>package com.javacode2018.lesson001.demo24.test7;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Configuration1 {
@Bean
public String name1() {
System.out.println("name1");
return "name1";
}
}/<code>
Configuration2
<code>package com.javacode2018.lesson001.demo24.test7;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Configuration2 {
@Bean
public String name2() {
System.out.println("name2");
return "name2";
}
}/<code>
Configuration3
<code>package com.javacode2018.lesson001.demo24.test7;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Configuration3 {
@Bean
public String name3() {
System.out.println("name3");
return "name3";
}
}/<code>
來一個ImportSelector實現類,導入Configuration1
<code>package com.javacode2018.lesson001.demo24.test7;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
public class ImportSelector1 implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{
Configuration1.class.getName()
};
}
}/<code>
來一個DeferredImportSelector實現類,導入Configuration2
<code>package com.javacode2018.lesson001.demo24.test7;
import org.springframework.context.annotation.DeferredImportSelector;
import org.springframework.core.type.AnnotationMetadata;
public class DeferredImportSelector1 implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{Configuration2.class.getName()};
}
}/<code>
來一個總的配置類
<code>package com.javacode2018.lesson001.demo24.test7;
import org.springframework.context.annotation.Import;
@Import({
DeferredImportSelector1.class,
Configuration3.class,
ImportSelector1.class,
})
public class MainConfig7 {
}/<code>
注意上面的@Import中被導入類的順序:
DeferredImportSelector1->Configuration3->ImportSelector1
下面來個測試用例,看一下3個配置文件中@Bean標註的方法被執行的先後順序。
測試用例
ImportTest中新增個方法
<code>@Test
public void test7() {
//1.通過AnnotationConfigApplicationContext創建spring容器,參數為@Import標註的類
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig7.class);
}/<code>
運行輸出
<code>name3
name1
name2/<code>
輸出的結果結合一下@Import中被導入的3個類的順序,可以看出DeferredImportSelector1是被最後處理的,其他2個是按照value中所在的先後順序處理的。
指定導入的類的處理順序
當@Import中有多個DeferredImportSelector接口的實現類時候,可以指定他們的順序,指定順序常見2種方式
實現Ordered接口的方式
<code>org.springframework.core.Ordered
public interface Ordered {
int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
int LOWEST_PRECEDENCE = Integer.MAX_VALUE;
int getOrder();
}/<code>
value的值越小,優先級越高。
實現Order註解的方式
<code>org.springframework.core.annotation.Order
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Documented
public @interface Order {
int value() default Ordered.LOWEST_PRECEDENCE;
}/<code>
value的值越小,優先級越高。
下面我們來個案例感受一下。
來個指定導入類處理順序的案例
來2個配置類,內部都有一個@Bean標註的方法,用來註冊一個bean,方法內部輸出一行文字
Configuration1
<code>package com.javacode2018.lesson001.demo24.test8;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Configuration1 {
@Bean
public String name1() {
System.out.println("name1");
return "name1";
}
}/<code>
Configuration2
<code>package com.javacode2018.lesson001.demo24.test8;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Configuration2 {
@Bean
public String name2() {
System.out.println("name2");
return "name2";
}
}/<code>
來2個DeferredImportSelector實現類,分別來導入上面2個配置文件,順便通過Ordered接口指定一下順序
DeferredImportSelector1
<code>package com.javacode2018.lesson001.demo24.test8;
import org.springframework.context.annotation.DeferredImportSelector;
import org.springframework.core.Ordered;
import org.springframework.core.type.AnnotationMetadata;
public class DeferredImportSelector1 implements DeferredImportSelector, Ordered {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{Configuration1.class.getName()};
}
@Override
public int getOrder() {
return 2;
}
}/<code>
DeferredImportSelector2
<code>package com.javacode2018.lesson001.demo24.test8;
import com.javacode2018.lesson001.demo24.test7.Configuration2;
import org.springframework.context.annotation.DeferredImportSelector;
import org.springframework.core.Ordered;
import org.springframework.core.type.AnnotationMetadata;
public class DeferredImportSelector2 implements DeferredImportSelector, Ordered {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{Configuration2.class.getName()};
}
@Override
public int getOrder() {
return 1;
}
}/<code>
DeferredImportSelector1的order為2,DeferredImportSelector2的order為1,order值越小優先級越高。
來個總的配置類,引入上面兩個ImportSelector
MainConfig8
<code>package com.javacode2018.lesson001.demo24.test8;
import org.springframework.context.annotation.Import;
@Import({
DeferredImportSelector1.class,
DeferredImportSelector2.class,
})
public class MainConfig8 {
}/<code>
測試用例
ImportTest中新增個方法
<code>@Test
public void test8() {
//1.通過AnnotationConfigApplicationContext創建spring容器,參數為@Import標註的類
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig8.class);
}/<code>
運行輸出
<code>name2
name1/<code>
結果配合order的值,按照order從小到大來處理,可以看出DeferredImportSelector2先被處理的。
Spring中這塊的源碼
@Import註解是被下面這個類處理的
<code>org.springframework.context.annotation.ConfigurationClassPostProcessor/<code>
前面介紹的@Configuration、@Bean、@CompontentScan、@CompontentScans都是被這個類處理的,這個類是高手必經之路,建議花點時間研究研究。
閱讀更多 Java桔煙 的文章