上次去頭條面試:
面試官:spring中,類上加不加@Configuration注解,有什么區別?
我:當某個類上有@Configuration注解的時候,可以在這個類中使用@Bean注解向spring容器中注冊bean;如果不加@Configuration注解,不能通過@Bean注解注冊bean。
面試官:你確定不用@Configuration注解,不能通過@Bean注解來注冊bean?
我:看着面試官,猶豫了10秒鍾,說:不加@Configuration通過@Bean注解也可以注冊bean
面試官:你確定可以注冊?
我:嗯。。。。嗯。。。。嗯,我確定可以注冊
面試官:那加不加到底有什么區別呢?
我:好像沒有什么區別啊
面試官:好像沒區別。。。。先回去等通知吧!
結果可想而知,沒戲了!
回去之后立即看spring的源碼,終於搞清了這個問題,原來加不加@Configuration還是有相當大的區別的。
下面我們就來看看@Configuration注解到底是干什么的,加不加到底有什么區別,下次你們去面試的時候就可以給面試官吹吹了。
之前我們都是通過xml的方式定義bean,里面會寫很多bean元素,然后spring啟動的時候,就會讀取bean xml配置文件,然后解析這些配置,然后會將這些bean注冊到spring容器中,供使用者使用。
jdk1.5里面有了注解的功能,spring也沒閑着,覺得注解挺好用的,就將注解加了進來,讓我們通過注解的方式來定義bean,用起來能達到xml中定義bean一樣的效果,並且更簡潔一些,這里面需要用到的注解就有@Configuration
注解和@Bean
注解。
@Configuration注解
用法
@Configuration這個注解可以加在類上,讓這個類的功能等同於一個bean xml配置文件,如下:
@Configuration
public class ConfigBean {
}
上面代碼類似於下面的xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd">
</beans>
通過AnnotationConfigApplicationContext
來加載@Configuration
修飾的類,如下:
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConfigBean.class);
此時ConfigBean類中沒有任何內容,相當於一個空的xml配置文件,此時我們要在ConfigBean類中注冊bean,那么我們就要用到@Bean注解了。
總結一下
@Configuration
使用步驟:
-
在類上使用
@Configuration
注解 -
通過
AnnotationConfigApplicationContext
容器來加@Configuration
注解修飾的類
@Bean注解
用法
這個注解類似於bean xml配置文件中的bean元素,用來在spring容器中注冊一個bean。
@Bean注解用在方法上,表示通過方法來定義一個bean,默認將方法名稱作為bean名稱,將方法返回值作為bean對象,注冊到spring容器中。
如:
@Bean
public User user1() {
return new User();
}
@Bean注解還有很多屬性,我們來看一下其源碼:
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) //@1
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Bean {
@AliasFor("name")
String[] value() default {};
@AliasFor("value")
String[] name() default {};
@Deprecated
Autowire autowire() default Autowire.NO;
boolean autowireCandidate() default true;
String initMethod() default "";
String destroyMethod() default AbstractBeanDefinition.INFER_METHOD;
}
@1:說明這個注解可以用在方法和注解類型上面。
每個參數含義:
value和name是一樣的,設置的時候,這2個參數只能選一個,原因是@AliasFor導致的
@AliasFor這個注解不清楚的可以看這個文章:詳解注解
value:字符串數組,第一個值作為bean的名稱,其他值作為bean的別名
autowire:這個參數上面標注了@Deprecated,表示已經過期了,不建議使用了
autowireCandidate:是否作為其他對象注入時候的候選bean,之前的文章中專門介紹過這個屬性,不清楚的可以去看看:autowire-candidate詳解
initMethod:bean初始化的方法,這個和生命周期有關,以后詳解
destroyMethod:bean銷毀的方法,也是和生命周期相關的,以后詳解
案例
User類
package com.javacode2018.lesson001.demo20;
public class User {
}
Bean配置類:ConfigBean
package com.javacode2018.lesson001.demo20;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ConfigBean {
//bean名稱為方法默認值:user1
@Bean
public User user1() {
return new User();
}
//bean名稱通過value指定了:user2Bean
@Bean("user2Bean")
public User user2() {
return new User();
}
//bean名稱為:user3Bean,2個別名:[user3BeanAlias1,user3BeanAlias2]
@Bean({"user3Bean", "user3BeanAlias1", "user3BeanAlias2"})
public User user3() {
return new User();
}
}
上面通過@Bean注解定義了3個bean,比較簡單
來個測試類
package com.javacode2018.lesson001.demo20;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.Arrays;
public class ConfigurationTest {
@Test
public void test1() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConfigBean.class);//@1
for (String beanName : context.getBeanDefinitionNames()) {
//別名
String[] aliases = context.getAliases(beanName);
System.out.println(String.format("bean名稱:%s,別名:%s,bean對象:%s",
beanName,
Arrays.asList(aliases),
context.getBean(beanName)));
}
}
}
@1:通過
AnnotationConfigApplicationContext
來加載配置類ConfigBean
,會將配置類中所有的bean注冊到spring容器中for循環中輸出了bean名稱、別名、bean對象
運行test1方法輸出
bean名稱:org.springframework.context.annotation.internalConfigurationAnnotationProcessor,別名:[],bean對象:org.springframework.context.annotation.ConfigurationClassPostProcessor@3bd82cf5
bean名稱:org.springframework.context.annotation.internalAutowiredAnnotationProcessor,別名:[],bean對象:org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor@544fa968
bean名稱:org.springframework.context.annotation.internalCommonAnnotationProcessor,別名:[],bean對象:org.springframework.context.annotation.CommonAnnotationBeanPostProcessor@247bddad
bean名稱:org.springframework.context.event.internalEventListenerProcessor,別名:[],bean對象:org.springframework.context.event.EventListenerMethodProcessor@d35dea7
bean名稱:org.springframework.context.event.internalEventListenerFactory,別名:[],bean對象:org.springframework.context.event.DefaultEventListenerFactory@7770f470
bean名稱:configBean,別名:[],bean對象:com.javacode2018.lesson001.demo20.ConfigBean$$EnhancerBySpringCGLIB$$dde45976@5e5d171f
bean名稱:user1,別名:[],bean對象:com.javacode2018.lesson001.demo20.User@24313fcc
bean名稱:user2Bean,別名:[],bean對象:com.javacode2018.lesson001.demo20.User@7d20d0b
bean名稱:user3Bean,別名:[user3BeanAlias2, user3BeanAlias1],bean對象:com.javacode2018.lesson001.demo20.User@77f1baf5
上面的輸出,我們主要關注與最后4行,前面的可以先忽略。
從輸出中可以看出,有個名稱為
configBean
的bean,正是ConfigBean這個類型,可以得出,被@Configuration修飾的類,也被注冊到spring容器中了最后3行輸出就是幾個User的bean對象了。
上面的用法應該很多人都比較熟悉,下面的屬於重點了。
去掉@Configuration會怎樣?
我們來看一下沒有@Configuration的時候,什么效果。
新建一個ConfigBean1類
內容和ConfigBean類一樣,只是將@Configuration注解去掉了,如下:
public class ConfigBean1 {
//bean名稱為方法默認值:user1
@Bean
public User user1() {
return new User();
}
//bean名稱通過value指定了:user2Bean
@Bean("user2Bean")
public User user2() {
return new User();
}
//bean名稱為:user3Bean,2個別名:[user3BeanAlias1,user3BeanAlias2]
@Bean({"user3Bean", "user3BeanAlias1", "user3BeanAlias2"})
public User user3() {
return new User();
}
}
來個測試用例test2
代碼類似於test1,給spring容器傳遞ConfigBean1
@Test
public void test2() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConfigBean1.class);
for (String beanName : context.getBeanDefinitionNames()) {
//別名
String[] aliases = context.getAliases(beanName);
System.out.println(String.format("bean名稱:%s,別名:%s,bean對象:%s",
beanName,
Arrays.asList(aliases),
context.getBean(beanName)));
}
}
運行輸出
bean名稱:org.springframework.context.annotation.internalConfigurationAnnotationProcessor,別名:[],bean對象:org.springframework.context.annotation.ConfigurationClassPostProcessor@333291e3
bean名稱:org.springframework.context.annotation.internalAutowiredAnnotationProcessor,別名:[],bean對象:org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor@479d31f3
bean名稱:org.springframework.context.annotation.internalCommonAnnotationProcessor,別名:[],bean對象:org.springframework.context.annotation.CommonAnnotationBeanPostProcessor@40ef3420
bean名稱:org.springframework.context.event.internalEventListenerProcessor,別名:[],bean對象:org.springframework.context.event.EventListenerMethodProcessor@498d318c
bean名稱:org.springframework.context.event.internalEventListenerFactory,別名:[],bean對象:org.springframework.context.event.DefaultEventListenerFactory@6e171cd7
bean名稱:configBean1,別名:[],bean對象:com.javacode2018.lesson001.demo20.ConfigBean1@402bba4f
bean名稱:user1,別名:[],bean對象:com.javacode2018.lesson001.demo20.User@795cd85e
bean名稱:user2Bean,別名:[],bean對象:com.javacode2018.lesson001.demo20.User@59fd97a8
bean名稱:user3Bean,別名:[user3BeanAlias2, user3BeanAlias1],bean對象:com.javacode2018.lesson001.demo20.User@f5ac9e4
分析結果
我們將2個輸出的最后4行拿來對比一下:
有@Configuration注解的
bean名稱:configBean,別名:[],bean對象:com.javacode2018.lesson001.demo20.ConfigBean$$EnhancerBySpringCGLIB$$dde45976@5e5d171f
bean名稱:user1,別名:[],bean對象:com.javacode2018.lesson001.demo20.User@24313fcc
bean名稱:user2Bean,別名:[],bean對象:com.javacode2018.lesson001.demo20.User@7d20d0b
bean名稱:user3Bean,別名:[user3BeanAlias2, user3BeanAlias1],bean對象:com.javacode2018.lesson001.demo20.User@77f1baf5
沒有@Configuration注解的
bean名稱:configBean1,別名:[],bean對象:com.javacode2018.lesson001.demo20.ConfigBean1@402bba4f
bean名稱:user1,別名:[],bean對象:com.javacode2018.lesson001.demo20.User@795cd85e
bean名稱:user2Bean,別名:[],bean對象:com.javacode2018.lesson001.demo20.User@59fd97a8
bean名稱:user3Bean,別名:[user3BeanAlias2, user3BeanAlias1],bean對象:com.javacode2018.lesson001.demo20.User@f5ac9e4
對比得出
-
對比最后3行,可以看出:有沒有@Configuration注解,@Bean都會起效,都會將@Bean修飾的方法作為bean注冊到容器中
-
兩個內容的第一行有點不一樣,被@Configuration修飾的bean最后輸出的時候帶有
EnhancerBySpringCGLIB
的字樣,而沒有@Configuration注解的bean沒有Cglib的字樣;有EnhancerBySpringCGLIB
字樣的說明這個bean被cglib處理過的,變成了一個代理對象。
目前為止我們還是看不出二者本質上的區別,繼續向下看。
@Configuration加不加到底區別在哪?
通常情況下,bean之間是有依賴關系的,我們來創建個有依賴關系的bean,通過這個案例你就可以看出根本的區別了。
再來一個加@Configuration的案例
定義2個類。
ServiceA
package com.javacode2018.lesson001.demo20;
public class ServiceA {
}
ServiceB
package com.javacode2018.lesson001.demo20;
public class ServiceB {
private ServiceA serviceA;
public ServiceB(ServiceA serviceA) {
this.serviceA = serviceA;
}
@Override
public String toString() {
return "ServiceB{" +
"serviceA=" + serviceA +
'}';
}
}
上面定義了2個類,ServiceB依賴於ServiceA,ServiceB通過構造器注入ServiceA。
來個@Configuration類管理上面對象。
ConfigBean2
package com.javacode2018.lesson001.demo20;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ConfigBean2 {
@Bean
public ServiceA serviceA() {
System.out.println("調用serviceA()方法"); //@0
return new ServiceA();
}
@Bean
ServiceB serviceB1() {
System.out.println("調用serviceB1()方法");
ServiceA serviceA = this.serviceA(); //@1
return new ServiceB(serviceA);
}
@Bean
ServiceB serviceB2() {
System.out.println("調用serviceB2()方法");
ServiceA serviceA = this.serviceA(); //@2
return new ServiceB(serviceA);
}
}
上面通過@Bean注解,向容器中注冊了3個bean
注意@1和@2,通過this.serviceA()獲取需要注入的ServiceA對象。
上面每個方法第一行都輸出了一行日志。
重點關注一下@0這行日志會輸出幾次,大家先思考一下1次還是3次?
測試用例
@Test
public void test3() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConfigBean2.class);
for (String beanName : context.getBeanDefinitionNames()) {
//別名
String[] aliases = context.getAliases(beanName);
System.out.println(String.format("bean名稱:%s,別名:%s,bean對象:%s",
beanName,
Arrays.asList(aliases),
context.getBean(beanName)));
}
}
運行輸出
截取了幾行輸出如下:
調用serviceA()方法
調用serviceB1()方法
調用serviceB2()方法
bean名稱:configBean2,別名:[],bean對象:com.javacode2018.lesson001.demo20.ConfigBean2$$EnhancerBySpringCGLIB$$ffa0178@77f1baf5
bean名稱:serviceA,別名:[],bean對象:com.javacode2018.lesson001.demo20.ServiceA@41a2befb
bean名稱:serviceB1,別名:[],bean對象:ServiceB{serviceA=com.javacode2018.lesson001.demo20.ServiceA@41a2befb}
bean名稱:serviceB2,別名:[],bean對象:ServiceB{serviceA=com.javacode2018.lesson001.demo20.ServiceA@41a2befb}
分析結果
從輸出中可以看出
-
前三行可以看出,被@Bean修飾的方法都只被調用了一次,這個很關鍵
-
最后三行中可以看出都是同一個ServiceA對象,都是`ServiceA@41a2befb`這個實例
這是為什么?
被@Configuration修飾的類,spring容器中會通過cglib給這個類創建一個代理,代理會攔截所有被@Bean
修飾的方法,默認情況(bean為單例)下確保這些方法只被調用一次,從而確保這些bean是同一個bean,即單例的。
至於底層是如何實現的,可以去看一下公眾號里面這篇文章:詳解java中的動態代理和cglib代理。
現在各位應該感受到了,我寫的文章前后一般都是有依賴的,所以也建議大家按順序看,這樣知識就是貫通的。
不加的案例
我們再來看看將ConfigBean2上的的@Configuration去掉,效果如何,代碼就不寫了,直接上輸出結果:
調用serviceA()方法
調用serviceB1()方法
調用serviceA()方法
調用serviceB2()方法
調用serviceA()方法
bean名稱:configBean2,別名:[],bean對象:com.javacode2018.lesson001.demo20.ConfigBean2@6e171cd7
bean名稱:serviceA,別名:[],bean對象:com.javacode2018.lesson001.demo20.ServiceA@402bba4f
bean名稱:serviceB1,別名:[],bean對象:ServiceB{serviceA=com.javacode2018.lesson001.demo20.ServiceA@795cd85e}
bean名稱:serviceB2,別名:[],bean對象:ServiceB{serviceA=com.javacode2018.lesson001.demo20.ServiceA@59fd97a8}
結果分析
-
serviceA()方法被調用了3次
-
configBean2這個bean沒有代理效果了
-
最后3行可以看出,幾個ServiceA對象都是不一樣的
spring這塊的源碼
spring中用下面這個類處理@Configuration這個注解:
org.springframework.context.annotation.ConfigurationClassPostProcessor
這里面重點關注這幾個方法:
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
public void enhanceConfigurationClasses(ConfigurableListableBeanFactory beanFactory)
最后一個方法會創建cglib代理,大家可以設置斷點進去看看,有問題歡迎交流。
總結
-
到目前為止加不加@Configuration注解,有什么區別,大家估計比我都清楚了
-
@Configuration注解修飾的類,會被spring通過cglib做增強處理,通過cglib會生成一個代理對象,代理會攔截所有被@Bean注解修飾的方法,可以確保一些bean是單例的
-
不管@Bean所在的類上是否有@Configuration注解,都可以將@Bean修飾的方法作為一個bean注冊到spring容器中
案例源碼
鏈接:https://pan.baidu.com/s/1p6rcfKOeWQIVkuhVybzZmQ
提取碼:zr99
來源:https://mp.weixin.qq.com/s?__biz=MzA5MTkxMDQ4MQ==&mid=2648934137&idx=1&sn=3775d5d7a23c43616d1274b0b52a9f99&chksm=88621ec7bf1597d1b16d91cfb28e63bef485f10883c7ca30d09838667f65e3d214b9e1cebd47&token=1372043037&lang=zh_CN&scene=21#wechat_redirect