隨着SpringBoot的流行,基於注解式開發的熱潮逐漸覆蓋了基於XML純配置的開發,而作為Spring中最核心的bean當然也能夠使用注解的方式進行表示。所以本篇就來詳細的討論一下作為Spring中的Bean到底都有哪些用法。
@Bean 基礎聲明
Spring的@Bean注解用於告訴方法,產生一個Bean對象,然后這個Bean對象交給Spring管理。產生這個Bean對象的方法Spring只會調用一次,隨后這個Spring將會將這個Bean對象放在自己的IOC容器中。
SpringIOC 容器管理一個或者多個bean,這些bean都需要在@Configuration注解下進行創建,在一個方法上使用@Bean注解就表明這個方法需要交給Spring進行管理。
快速搭建一個maven項目並配置好所需要的Spring 依賴
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.13.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.3.13.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.3.13.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>4.3.13.RELEASE</version>
</dependency>
在src根目錄下創建一個AppConfig
的配置類,這個配置類也就是管理一個或多個bean 的配置類,並在其內部聲明一個myBean的bean,並創建其對應的實體類
@Configuration
public class AppConfig {
// 使用@Bean 注解表明myBean需要交給Spring進行管理
// 未指定bean 的名稱,默認采用的是 "方法名" + "首字母小寫"的配置方式
@Bean
public MyBean myBean(){
return new MyBean();
}
}
public class MyBean {
public MyBean(){
System.out.println("MyBean Initializing");
}
}
在對應的test文件夾下創建一個測試類SpringBeanApplicationTests
,測試上述代碼的正確性
public class SpringBeanApplicationTests {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
context.getBean("myBean");
}
}
輸出 : MyBean Initializing
隨着SpringBoot的流行,我們現在更多采用基於注解式的配置從而替換掉了基於XML的配置,所以本篇文章我們主要探討基於注解的@Bean以及和其他注解的使用。
@Bean 基本構成及其使用
在簡單介紹了一下如何聲明一個Bean組件,並將其交給Spring進行管理之后,下面我們來介紹一下Spring 的基本構成
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Bean {
@AliasFor("name")
String[] value() default {};
@AliasFor("value")
String[] name() default {};
Autowire autowire() default Autowire.NO;
String initMethod() default "";
String destroyMethod() default AbstractBeanDefinition.INFER_METHOD;
}
@Bean不僅可以作用在方法上,也可以作用在注解類型上,在運行時提供注冊。
value: name屬性的別名,在不需要其他屬性時使用,也就是說value 就是默認值
name: 此bean 的名稱,或多個名稱,主要的bean的名稱加別名。如果未指定,則bean的名稱是帶注解方法的名稱。如果指定了,方法的名稱就會忽略,如果沒有其他屬性聲明的話,bean的名稱和別名可能通過value屬性配置
autowire : 此注解的方法表示自動裝配的類型,返回一個Autowire
類型的枚舉,我們來看一下Autowire
枚舉類型的概念
// 枚舉確定自動裝配狀態:即,bean是否應該使用setter注入由Spring容器自動注入其依賴項。
// 這是Spring DI的核心概念
public enum Autowire {
// 常量,表示根本沒有自動裝配。
NO(AutowireCapableBeanFactory.AUTOWIRE_NO),
// 常量,通過名稱進行自動裝配
BY_NAME(AutowireCapableBeanFactory.AUTOWIRE_BY_NAME),
// 常量,通過類型進行自動裝配
BY_TYPE(AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE);
private final int value;
Autowire(int value) {
this.value = value;
}
public int value() {
return this.value;
}
public boolean isAutowire() {
return (this == BY_NAME || this == BY_TYPE);
}
}
autowire的默認值為No
,默認表示不通過自動裝配。
initMethod: 這個可選擇的方法在bean實例化的時候調用,InitializationBean
接口允許bean在合適的時機通過設置注解的初始化屬性從而調用初始化方法,InitializationBean 接口有一個定義好的初始化方法
void afterPropertiesSet() throws Exception;
Spring不推薦使用InitializationBean 來調用其初始化方法,因為它不必要地將代碼耦合到Spring。Spring推薦使用
@PostConstruct
注解或者為POJO類指定其初始化方法這兩種方式來完成初始化。
不推薦使用:
public class InitBean implements InitializingBean {
public void afterPropertiesSet() {}
}
destroyMethod: 方法的可選擇名稱在調用bean示例在關閉上下文的時候,例如JDBC的close()方法,或者SqlSession的close()方法。DisposableBean
接口的實現允許在bean銷毀的時候進行回調調用,DisposableBean 接口之后一個單個的方法
void destroy() throws Exception;
Spring不推薦使用DisposableBean 的方式來初始化其方法,因為它會將不必要的代碼耦合到Spring。作為替代性的建議,Spring 推薦使用
@PreDestory
注解或者為@Bean
注解提供 destroyMethod 屬性,
不推薦使用:
public class DestroyBean {
public void cleanup() {}
}
推薦使用:
public class MyBean {
public MyBean(){
System.out.println("MyBean Initializing");
}
public void init(){
System.out.println("Bean 初始化方法被調用");
}
public void destroy(){
System.out.println("Bean 銷毀方法被調用");
}
}
@Configuration
public class AppConfig {
// @Bean
@Bean(initMethod = "init", destroyMethod = "destroy")
public MyBean myBean(){
return new MyBean();
}
}
修改一下測試類,測試其初始化方法和銷毀方法在何時會被調用
public class SpringBeanApplicationTests {
public static void main(String[] args) {
// ------------------------------ 測試一 ------------------------------
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// context.getBean("myBean");
// 變體
context.getBean("myBean");
((AnnotationConfigApplicationContext) context).destroy();
// ((AnnotationConfigApplicationContext) context).close();
}
}
初始化方法在得到Bean的實例的時候就會被調用,銷毀方法在容器銷毀或者容器關閉的時候會被調用。
@Bean 注解與其他注解產生的火花
在上面的一個小節中我們了解到了@Bean注解的幾個屬性,但是對於@Bean注解的功能來講這有點太看不起bean了,@Bean另外一個重要的功能是能夠和其他注解產生化學反應,如果你還不了解這些注解的話,那么請繼續往下讀,你會有收獲的
這一節我們主要探討@profile,@scope,@lazy,@depends-on @primary等注解
@Profile 注解
@Profile的作用是把一些meta-data進行分類,分成Active和InActive這兩種狀態,然后你可以選擇在active 和在Inactive這兩種狀態下配置bean,在Inactive狀態通常的注解有一個!操作符,通常寫為:@Profile("!p"),這里的p是Profile的名字。
三種設置方式:
-
可以通過ConfigurableEnvironment.setActiveProfiles()以編程的方式激活
-
可以通過AbstractEnvironment.ACTIVE_PROFILES_PROPERTY_NAME (spring.profiles.active )屬性設置為
JVM屬性
-
作為環境變量,或作為web.xml 應用程序的Servlet 上下文參數。也可以通過@ActiveProfiles 注解在集成測試中以聲明方式激活配置文件。
作用域
- 作為類級別的注釋在任意類或者直接與@Component 進行關聯,包括@Configuration 類
- 作為原注解,可以自定義注解
- 作為方法的注解作用在任何方法
注意:
如果一個配置類使用了Profile 標簽或者@Profile 作用在任何類中都必須進行啟用才會生效,如果@Profile({"p1","!p2"}) 標識兩個屬性,那么p1 是啟用狀態 而p2 是非啟用狀態的。
現有一個POJO類為Subject學科類,里面有兩個屬性,一個是like(理科)屬性,一個是wenke(文科)屬性,分別有兩個配置類,一個是AppConfigWithActiveProfile
,一個是AppConfigWithInactiveProfile
,當系統環境是 "like"的時候就注冊 AppConfigWithActiveProfile ,如果是 "wenke",就注冊 AppConfigWithInactiveProfile,來看一下這個需求如何實現
Subject.java
// 學科
public class Subject {
// 理科
private String like;
// 文科
private String wenke;
get and set ...
@Override
public String toString() {
return "Subject{" +
"like='" + like + '\'' +
", wenke='" + wenke + '\'' +
'}';
}
}
AppConfigWithActiveProfile.java 注冊Profile 為like 的時候
@Profile("like")
@Configuration
public class AppConfigWithActiveProfile {
@Bean
public Subject subject(){
Subject subject = new Subject();
subject.setLike("物理");
return subject;
}
}
AppConfigWithInactiveProfile.java 注冊Profile 為wenke 的時候
@Profile("wenke")
@Configuration
public class AppConfigWithInactiveProfile {
@Bean
public Subject subject(){
Subject subject = new Subject();
subject.setWenke("歷史");
return subject;
}
}
修改一下對應的測試類,設置系統環境,當Profile 為like 和 wenke 的時候分別注冊各自對應的屬性
// ------------------------------ 測試 profile ------------------------------
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
// 激活 like 的profile
context.getEnvironment().setActiveProfiles("like");
context.register(AppConfigWithActiveProfile.class,AppConfigWithInactiveProfile.class);
context.refresh();
Subject subject = (Subject) context.getBean("subject");
System.out.println("subject = " + subject);
把context.getEnvironment().setActiveProfiles("wenke") 設置為wenke,觀察其對應的輸出內容發生了變化,這就是@Profile的作用,有一層可選擇性注冊的意味。
@Scope 注解
在Spring中對於bean的默認處理都是單例的,我們通過上下文容器.getBean方法拿到bean容器,並對其進行實例化,這個實例化的過程其實只進行一次,即多次getBean 獲取的對象都是同一個對象,也就相當於這個bean的實例在IOC容器中是public的,對於所有的bean請求來講都可以共享此bean。
那么假如我不想把這個bean被所有的請求共享或者說每次調用我都想讓它生成一個bean實例該怎么處理呢?
多例Bean
bean的非單例原型范圍會使每次發出對該特定bean的請求時都創建新的bean實例,也就是說,bean被注入另一個bean,或者通過對容器的getBean()方法調用來請求它,可以用如下圖來表示:
通過一個示例來說明bean的多個實例
新建一個AppConfigWithAliasAndScope
配置類,用來定義多例的bean,
@Configuration
public class AppConfigWithAliasAndScope {
/**
* 為myBean起兩個名字,b1 和 b2
* @Scope 默認為 singleton,但是可以指定其作用域
* prototype 是多例的,即每一次調用都會生成一個新的實例。
*/
@Bean({"b1","b2"})
@Scope("prototype")
public MyBean myBean(){
return new MyBean();
}
}
測試一下多例的情況:
// ------------------------------ 測試scope ------------------------------
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfigWithAliasAndScope.class);
MyBean myBean = (MyBean) context.getBean("b1");
MyBean myBean2 = (MyBean) context.getBean("b2");
System.out.println(myBean);
System.out.println(myBean2);
其他情況
除了多例的情況下,Spring還為我們定義了其他情況:
Scope | Descriptionn |
---|---|
singleton | 默認單例的bean定義信息,對於每個IOC容器來說都是單例對象 |
prototype | bean對象的定義為任意數量的對象實例 |
request | bean對象的定義為一次HTTP請求的生命周期,也就是說,每個HTTP請求都有自己的bean實例,它是在單個bean定義的后面創建的。僅僅在web-aware的上下文中有效 |
session | bean對象的定義為一次HTTP會話的生命周期。僅僅在web-aware的上下文中有效 |
application | bean對象的定義范圍在ServletContext生命周期內。僅僅在web-aware的上下文中有效 |
websocket | bean對象的定義為WebSocket的生命周期內。僅僅在web-aware的上下文中有效 |
singleton和prototype 一般都用在普通的Java項目中,而request、session、application、websocket都用於web應用中。
request、session、application、websocket的作用范圍
你可以體會到 request、session、application、websocket 的作用范圍在當你使用web-aware的ApplicationContext應用程序上下文的時候,比如XmlWebApplicationContext
的實現類。如果你使用了像是ClassPathXmlApplicationContext
的上下文環境時,就會拋出IllegalStateException
因為Spring不認識這個作用范圍。
@Lazy 注解
@Lazy
: 表明一個bean 是否延遲加載,可以作用在方法上,表示這個方法被延遲加載;可以作用在@Component (或者由@Component 作為原注解) 注釋的類上,表明這個類中所有的bean 都被延遲加載。如果沒有@Lazy注釋,或者@Lazy 被設置為false,那么該bean 就會急切渴望被加載;除了上面兩種作用域,@Lazy 還可以作用在@Autowired和@Inject注釋的屬性上,在這種情況下,它將為該字段創建一個惰性代理,作為使用ObjectFactory或Provider的默認方法。下面來演示一下:
@Lazy
@Configuration
@ComponentScan(basePackages = "com.spring.configuration.pojo")
public class AppConfigWithLazy {
@Bean
public MyBean myBean(){
System.out.println("myBean Initialized");
return new MyBean();
}
@Bean
public MyBean IfLazyInit(){
System.out.println("initialized");
return new MyBean();
}
}
- 修改測試類
public class SpringConfigurationApplication {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfigWithLazy.class);
// 獲取啟動過程中的bean 定義的名稱
for(String str : context.getBeanDefinitionNames()){
System.out.println("str = " + str);
}
}
}
輸出你會發現沒有關於bean的定義信息,但是當把@Lazy 注釋拿掉,你會發現輸出了關於bean的初始化信息
@DependsOn 注解
指當前bean所依賴的bean。任何指定的bean都能保證在此bean創建之前由IOC容器創建。在bean沒有通過屬性或構造函數參數顯式依賴於另一個bean的情況下很少使用,可能直接使用在任何直接或者間接使用 Component 或者Bean 注解表明的類上。來看一下具體的用法
新建三個Bean,分別是FirstBean、SecondBean、ThirdBean三個普通的bean,新建AppConfigWithDependsOn
並配置它們之間的依賴關系
public class FirstBean {
@Autowired
private SecondBean secondBean;
@Autowired
private ThirdBean thirdBean;
public FirstBean() {
System.out.println("FirstBean Initialized via Constuctor");
}
}
public class SecondBean {
public SecondBean() {
System.out.println("SecondBean Initialized via Constuctor");
}
}
public class ThirdBean {
public ThirdBean() {
System.out.println("ThirdBean Initialized via Constuctor");
}
}
@Configuration
public class AppConfigWithDependsOn {
@Bean("firstBean")
@DependsOn(value = {
"secondBean",
"thirdBean"
})
public FirstBean firstBean() {
return new FirstBean();
}
@Bean("secondBean")
public SecondBean secondBean() {
return new SecondBean();
}
@Bean("thirdBean")
public ThirdBean thirdBean() {
return new ThirdBean();
}
}
使用測試類進行測試,如下
// ------------------------------ 測試 DependsOn ------------------------------
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfigWithDependsOn.class);
context.getBean(FirstBean.class);
context.close();
輸出 :
SecondBean Initialized via Constuctor
ThirdBean Initialized via Constuctor
FirstBean Initialized via Constuctor
由於firstBean 的創建過程首先需要依賴secondBean
和 thirdBean
的創建,所以secondBean 首先被加載其次是thirdBean 最后是firstBean。
如果把@DependsOn 注解加在AppConfigWithDependsOn
類上則它們的初始化順序就會變為 firstBean、secondBean、thirdBean
@Primary 注解
指示當多個候選者有資格自動裝配依賴項時,應優先考慮bean。此注解在語義上就等同於在Spring XML中定義的bean 元素的primary屬性。注意: 除非使用component-scanning進行組件掃描,否則在類級別上使用@Primary不會有作用。如果@Primary 注解定義在XML中,那么@Primary 的注解元注解就會忽略,相反使用
@Primary 的兩種使用方式
- 與@Bean 一起使用,定義在方法上,方法級別的注解
- 與@Component 一起使用,定義在類上,類級別的注解
通過一則示例來演示一下:
新建一個AppConfigWithPrimary
類,在方法級別上定義@Primary注解
@Configuration
public class AppConfigWithPrimary {
@Bean
public MyBean myBeanOne(){
return new MyBean();
}
@Bean
@Primary
public MyBean myBeanTwo(){
return new MyBean();
}
}
上面代碼定義了兩個bean ,其中myBeanTwo 由@Primary 進行標注,表示它首先會進行注冊,使用測試類進行測試
// ------------------------------ 測試 Primary ------------------------------
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfigWithPrimary.class);
MyBean bean = context.getBean(MyBean.class);
System.out.println(bean);
你可以嘗試放開@Primary ,使用測試類測試的話會發現出現報錯信息,因為你嘗試獲取的是MyBean.class,而我們代碼中定義了兩個MyBean 的類型,所以需要@Primary 注解表明哪一個bean需要優先被獲取。
文章參考:
https://www.javaguides.net/2018/10/spring-dependson-annotation-example.html