Spring中你可能不知道的事(二)
在上一節中,我介紹了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,可能這個需求就很難實現了。所以每一塊知識點都很重要。
這一節的內容到這裏就結束了。
Spring中你可能不知道的事(二)