在上一節中,我介紹了Spring中極為重要的BeanPostProcessor BeanFactoryPostProcessor Import ImportSelector,還介紹了一些其他的零碎知識點,正如我上一節所說的,Spring實在是太龐大了,是眾多Java開發大神的結晶,很多功能,很多細節,可能一輩子都不會用到,不會發現,作為普通開發的我們,只能盡力去學習,去挖掘,也許哪天可以用到呢。
讓我們進入正題吧。
Full Lite
在上一節中的第一塊內容,我們知道了Spring中除了可以註冊我們最常用的配置類,還可以註冊一個普通的Bean,今天我就來做一個補充說明。
如果你接到一個需求,要求寫一個配置類,完成掃描,你會怎麼寫?
作為經常使用Spring的來說,這是一個入門級別的問題,並且在20秒鐘之內就可以完成編碼:
@Configuration
@ComponentScan
public class AppConfig {
}
複製程式碼
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
context.getBean(ServiceImpl.class).query();
}
}
複製程式碼
@Component
public class ServiceImpl{
public void query() {
System.out.println("正在查詢中");
}
}
複製程式碼
執行:
但是你有沒有嘗試過把AppConfig類上的@Configuration註解給去除?你在心裡肯定會犯嘀咕,這不能去除啊,這個@Configuration註解申明瞭我們們的AppConfig是一個Spring配置類,去除了@Configuration註解,怎麼可能可以呢?但是事實勝於雄辯,當我們把@Configuration註解給刪除,再次執行,你會見證到奇蹟:
@ComponentScan
public class AppConfig {
}
複製程式碼
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
context.getBean(ServiceImpl.class).query();
}
}
複製程式碼
一點問題都沒有!!!是不是到這裡已經顛覆了你對Spring的認知。
其實,在Spring內部,把帶上了@Configuration的配置類稱之為Full配置類,把沒有帶上@Configuration,但是帶上了@Component @ComponentScan @Import @ImportResource等註解的配置類稱之為Lite配置類。
原諒我,實在找不到合適的中文翻譯來表述這裡的Full和Lite。
也許你會覺得這並沒什麼用,只是“茴的四種寫法”而已。
別急,讓我們看下去,將會繼續重新整理你的三觀:
@ComponentScan
public class AppConfig {
}
複製程式碼
注意現在的AppConfig類上沒有加上@Configuration註解。
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
System.out.println(context.getBean(AppConfig.class).getClass().getSimpleName());
}
}
複製程式碼
我們註冊了Lite配置類,並且從Spring容器中取出了Lite配置類,列印出它的類名。
執行:
可以看到從容器取出來的就是AppConfig類,各位看官肯定會想,這不是廢話嗎,難道從容器取出來會變成了一隻老母雞?
別急嘛,讓我們繼續。
我們再在AppConfig類加上@Configuration註解,使其變成Full配置類,然後還是一樣,註冊這個配置類,取出這個配置類,列印類名:
你會驚訝的發現,的確從容器裡取出了一個老母雞,哦,不,是一個奇怪的類,從類名我們可以看到CGLIB這個關鍵字,CGLIB是動態代理的一種實現方式,也就是說我們的Full配置類被CGLIB代理了。
你是不是從來都沒有注意過,竟然會有如此奇怪的設定,但是更讓人驚訝的事情還在後頭,讓我們想想,為什麼好端端的類,Spring要用Cglib代理?這又不是AOP。Spring內部肯定做了一些什麼!沒錯,確實做了!!!
下面讓我們看看Spring到底做了什麼:
public class ServiceImpl {
public ServiceImpl() {
System.out.println("ServiceImpl類的構造方法");
}
}
複製程式碼
ServiceImpl類中有一個構造方法,列印了一句話。
public class OtherImpl {
}
複製程式碼
再定義一個OtherImpl類,裡面什麼都沒有。
public class AppConfig {
@Bean
public ServiceImpl getServiceImpl() {
return new ServiceImpl();
}
@Bean
public OtherImpl getOtherImpl() {
getServiceImpl();
return new OtherImpl();
}
}
複製程式碼
這個AppConfig沒有加上@Configuration註解,是一個Lite配置類,裡面定義了兩個@Bean方法,其中getServiceImpl方法建立並且返回了ServiceImpl類的物件,getOtherImpl方法再次呼叫了getServiceImpl方法。
然後我們註冊這個配置類:
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
}
}
複製程式碼
執行:
發現列印了兩次"ServiceImpl類的構造方法",這也很好理解,因為new了兩次ServiceImpl嘛,肯定會執行兩次ServiceImpl構造方法呀。
我們在把@Configuration註解給加上,讓AppConfig稱為一個Full配置類,再次執行:
你會驚訝的發現只列印了一次"ServiceImpl類的構造方法",說明只呼叫了一次ServiceImpl類的構造方法,其實這也說的通啊,因為Bean預設是Singleton的,所以只會建立一次物件嘛。
但是問題來了,為什麼我們明明new了兩次ServiceImpl類,但是真正只new了一次?結合上面的內容,很容易知道答案,因為Full配置類被Cglib代理了,它已經不是我們原先定義的AppConfig類了,最終執行的是代理物件。
好了,這個問題就討論到這裡,至於為什麼說(如何證明)帶上@Configuration註解的配置類稱之為Full配置類,不帶的稱之為Lite配置類,Cglib是怎麼代理Full配置類的,代理的規則又是什麼,這就涉及到Spring的原始碼解析了,就不在今天的討論內容之中了。
ImportBeanDefinitionRegistrar
大家一定使用過Mybatis,甚至使用過Mybatis的擴充套件,我在使用的時候,覺得太特麼的神奇了,只要在配置類上打一個MapperScan註解,指定需要掃描哪些包。然後這些包裡面只有介面,根本沒有實現類,為什麼可以完成資料庫的一系列操作,不知道大家有沒有和我一樣的疑惑,直到我知道了ImportBeanDefinitionRegistrar這個神奇的介面,關於這個介面,我不知道該怎麼去描述這個介面的作用,因為這個介面實在是太強大了,實在不是用簡單的文字可以描述清楚的。下面我就利用這個介面來完成一個假的MapperScan,從中慢慢體驗這個介面的強大,對了,這個介面要和Import註解配合使用。
首先需要定義一個註解:
@Import(CodeBearMapperScannerRegistrar.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface CodeBearMapperScanner {
String value();
}
複製程式碼
其中value就是需要掃描的包名,在這個註解類中又打了一個Import註解,來引ImportBeanDefinitionRegistrar類。
再定義一個註解:
@Retention(RetentionPolicy.RUNTIME)
public @interface CodeBearSql {
String value();
}
複製程式碼
這個註解是打在方法上的,接收的是一個sql語句。
然後要定義一個類,去實現ImportBeanDefinitionRegistrar介面,重寫提供的方法。
public class CodeBearMapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
private ResourceLoader resourceLoader;
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
try {
AnnotationAttributes annoAttrs =
AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(CodeBearMapperScanner.class.getName()));
String packageValue = annoAttrs.getString("value");
String pathValue = packageValue.replace(".", "/");
File[] files = resourceLoader.getResource(pathValue).getFile().listFiles();
for (File file : files) {
String name = file.getName().replace(".class", "");
Class<?> aClass = Class.forName(packageValue + "." + name);
if (aClass.isInterface()&&!aClass.isAnnotation()) {
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition();
AbstractBeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition();
beanDefinition.setBeanClass(CodeBeanFactoryBean.class);
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(packageValue + "." + name);
registry.registerBeanDefinition(name, beanDefinition);
}
}
} catch (Exception ex) {
}
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
}
複製程式碼
其中ResourceLoaderAware介面的作用不大,我只是利用這個介面,獲得了ResourceLoader ,然後通過ResourceLoader去獲得包下面的類而已。這方法的核心就是迴圈檔案列表,根據包名和檔名,反射獲得Class,接著判斷Class是不是介面,如果是介面的話,動態註冊Bean。如何動態去註冊Bean呢?我在這裡利用的是BeanDefinitionBuilder,通過BeanDefinitionBuilder獲得一個BeanDefinition,此時BeanDefinition是一個很純淨的BeanDefinition,經過一些處理,再把最終的BeanDefinition註冊到Spring容器。
關鍵就在於處理的這兩行程式碼了,這裡可能還看不懂,我們繼續看下去。
我們需要再定義一個類,去實現FactoryBean,InvocationHandler兩個介面:
public class CodeBeanFactoryBean implements FactoryBean, InvocationHandler {
private Class clazz;
public CodeBeanFactoryBean(Class clazz) {
this.clazz = clazz;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
CodeBearSql annotation = method.getAnnotation(CodeBearSql.class);
String sql= annotation.value();
System.out.println(sql);
return sql;
}
@Override
public Object getObject() throws Exception {
Object o = Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{clazz}, this);
return o;
}
@Override
public Class<?> getObjectType() {
return clazz;
}
}
複製程式碼
關於FactoryBean介面,在上一節中有介紹,這裡就不再闡述了。
這個類有一個構造方法,接收的是一個Class,這裡接收的就是用來進行資料庫操作的介面。getObject方法中,就利用傳進來的介面和動態代理來建立一個代理物件,此時這個代理物件就是FactoryBean生產的一個Bean了,只要對JDK動態代理有一定了解的人都知道,返回出來的代理物件實現了我們用來進行資料庫操作的介面。
我們需要把這個Bean交給Spring去管理,所以就有了CodeBearMapperScannerRegistrar中的這行程式碼:
beanDefinition.setBeanClass(CodeBeanFactoryBean.class);
複製程式碼
因為建立CodeBeanFactoryBean物件需要一個Class引數。所以就有了CodeBearMapperScannerRegistrar中的這行程式碼:
//packageValue + "." +name 就是介面的全名稱
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(packageValue + "." + name);
複製程式碼
invoke方法比較簡單,就是獲得CodeBearSql註解上的sql語句,然後列印一下,當然這裡只是模擬下,所以並沒有去查詢資料庫。
下面讓我們測試一下吧:
public interface UserRepo {
@CodeBearSql(value = "select * from user")
void get();
}
複製程式碼
@Configuration
@CodeBearMapperScanner("com.codebear")
@ComponentScan
public class AppConfig {
}
複製程式碼
@Service
public class Test {
@Autowired
UserRepo userRepo;
public void get(){
userRepo.get();
}
}
複製程式碼
執行結果:
可以看到我們的功能已經實現了。其實Mybatis的MapperScan註解也是利用了ImportBeanDefinitionRegistrar介面去實現的。可以看到第二塊內容,其實已經比較複雜了,不光光有ImportBeanDefinitionRegistrar,還整合FactoryBean,還融入了動態代理。如果我們不知道FactoryBean,可能這個需求就很難實現了。所以每一塊知識點都很重要。
這一節的內容到這裡就結束了。