前言
對於Spring框架,其中IoC(控制翻轉)和AOP(面向切面編程)都是比較重要的概念,而在Spring Boot中主要使用全註解的方式來實現IoC和AOP的功能,因此本文以Spring Boot框架爲基礎,對其中全註解下的IoC和AOP的相關基礎性概念和使用進行介紹,以爲後續Spring Boot學習打下基礎。
Spring Boot中全註解下的Spring IoC
IoC容器簡介
IoC(Inversion of Control,控制反轉)是一種通過描述來生成或者獲取對象的技術,它不是Spring中獨有的。它的作用可以理解爲:開始學java創建對象時常使用的就是new關鍵字來創建對象,而在Spring中則是通過描述(XML或註解)來創建對象,而對象的創建和管理是由IoC容器(類似於工廠方法)來負責。而一個系統中不可能只有一個對象,而且對象之間還具有依賴的關係,這時就需要依賴注入的方式,通過描述來管理各個對象之間的關聯關係。
Spring中將每個需要管理的對象稱爲Bean,而Spring管理這些Bean的容器,被稱爲Spring IoC容器(簡稱爲IoC容器),IoC容器具有兩個基本功能;
- 通過描述來管理Bean,包括創建和獲取Bean。
- 通過描述來完成Bean之間的依賴關係的建立。
在Spring的定義中,要求所有的IoC容器都需要實現BeanFactory頂級容器接口,該接口的源碼如下:
package org.springframework.beans.factory;
import org.springframework.beans.BeansException;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;
public interface BeanFactory {
// IoC容器管理的Bean名稱的前綴
String FACTORY_BEAN_PREFIX = "&";
// 一系列獲取Bean的方法
Object getBean(String var1) throws BeansException;
<T> T getBean(String var1, Class<T> var2) throws BeansException;
Object getBean(String var1, Object... var2) throws BeansException;
<T> T getBean(Class<T> var1) throws BeansException;
<T> T getBean(Class<T> var1, Object... var2) throws BeansException;
<T> ObjectProvider<T> getBeanProvider(Class<T> var1);
<T> ObjectProvider<T> getBeanProvider(ResolvableType var1);
// IoC容器中是否包含Bean
boolean containsBean(String var1);
// Bean是否爲單例
boolean isSingleton(String var1) throws NoSuchBeanDefinitionException;
// Bean是否爲原型
boolean isPrototype(String var1) throws NoSuchBeanDefinitionException;
// 是否匹配類型
boolean isTypeMatch(String var1, ResolvableType var2) throws NoSuchBeanDefinitionException;
boolean isTypeMatch(String var1, Class<?> var2) throws NoSuchBeanDefinitionException;
// 獲取Bean的類型
@Nullable
Class<?> getType(String var1) throws NoSuchBeanDefinitionException;
@Nullable
Class<?> getType(String var1, boolean var2) throws NoSuchBeanDefinitionException;
// 獲取Bean的別名
String[] getAliases(String var1);
}
在這個頂級接口中對相關的方法進行了註釋。同時可以看到上面的代碼中包含了很多getBean的重載方法,包括類型(byType),根據名稱(byName)獲取Bean,這意味着在IoC容器中可以使用Bean類型或者名稱來獲取相應的Bean,這也是後面依賴注入(Dependency Injection,DI)的基礎。
isSingleton方法是判斷Bean在容器中是否爲單例。在IoC容器中,Bean默認爲代理存在的,也就是使用getBean方法返回的Bean都是同一個。相對應的如果isPrototype方法返回爲true,則表示以原型模式的方法創建Bean,那麼在使用getBean方法獲取Bean時,IoC容器會新建Bean進行返回。
由於BeanFactory爲IoC容器的頂層接口,Spring在此基礎上設計了更高級的接口ApplicationContext,現實中我們使用的大部分Spring IoC容器是ApplicationContext接口的實現類。在Spring Boot中,bean的裝備主要是通過註解來實現的,因此下面使用AnnotationConfigApplicationContext(基於註解的IoC容器)來簡單實現一個Bean的裝配與使用,這種過程實際上和Spring Boot實現的Bean的裝配和獲取的方法是一致的。
簡單的POJO:
@Data
public class UserBean {
private Long id;
private String userName;
private Integer sex;
private String note;
public UserBean() {
}
public UserBean(Long id, String userName, Integer sex, String note) {
this.id = id;
this.userName = userName;
this.sex = sex;
this.note = note;
}
}
定義Spring的配置類文件:
@Configuration
public class AppConfig {
@Bean(name = "UserBean")
public UserBean getInit() {
return new UserBean(1L, "yitian", 1, "none");
}
}
@Configuration註解表示這是一個Java配置文件,Spring容器會根據它來生成IoC容器去裝配Bean。
@Bean註解表示該方法返回的對象將作爲Bean裝配到IoC容器中,name屬性指明該Bean的名稱,如果沒有配置,則默認爲方法名。
使用AnnotationConfigApplicationContext:
public class IoCTest {
private static Logger logger = LoggerFactory.getLogger(IoCTest.class);
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserBean userBean = context.getBean(UserBean.class);
logger.info(userBean.getUserName());
}
}
運行該main方法,可以看到如下日誌輸出:
15:14:10.871 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@29444d75
15:14:10.886 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalConfigurationAnnotationProcessor'
15:14:10.974 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerProcessor'
15:14:10.976 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerFactory'
15:14:10.977 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalAutowiredAnnotationProcessor'
15:14:10.978 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalCommonAnnotationProcessor'
15:14:10.984 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'appConfig'
15:14:10.988 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'UserBean'
15:14:11.018 [main] INFO cn.zyt.springbootlearning.ioc.IoCTest - yitian
將Bean裝配到IoC容器中
在傳統Spring框架中使用XML的方式來裝配Bean,而Spring Boot中主要使用全註解的方式將Bean裝配到IoC容器中。可以使用如下幾個不同的註解方式。
使用@Component或@ComponentScan
@Component註解指明哪個類被掃描到Spring IoC容器中,而@ComponentScan表示採用何種方式去掃描裝配Bean。例如對上述UserBean類,加入@Component註解,其所在package路徑爲:cn.zyt.springbootlearning.ioc.config.UserBean。
@Data
@Component("UserBean")
public class UserBean {
@Value("1")
private Long id;
@Value("yitian")
private String userName;
@Value("1")
private Integer sex;
@Value("none")
private String note;
//...
}
此時爲了使Spring IoC容器裝配該類,改造AppConfig(所在package爲:cn.zyt.springbootlearning.ioc.config.AppConfig)如下:
@Configuration
@ComponentScan("cn.zyt.springbootlearning.ioc.*")
public class AppConfig {
// @Bean(name = "UserBean")
// public UserBean getInit() {
// return new UserBean(1L, "yitian", 1, "none");
// }
}
@ComponentScan註解標註了指定的掃描包和子包來裝備其中的所有Bean。如果不進行設置,則默認掃描該類當前所在的包和子包。對於指定要掃描的包,@ComponentScan註解還有其他其個屬性,可以從該註解的源碼中進行查看:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
// 定義掃描的包
@AliasFor("basePackages")
String[] value() default {};
// 同上
@AliasFor("value")
String[] basePackages() default {};
// 定義掃描的類
Class<?>[] basePackageClasses() default {};
Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class;
ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT;
String resourcePattern() default "**/*.class";
boolean useDefaultFilters() default true;
// 當滿足過濾條件時掃描
ComponentScan.Filter[] includeFilters() default {};
// 當不滿足過濾條件時掃描
ComponentScan.Filter[] excludeFilters() default {};
// 是否延遲初始化
boolean lazyInit() default false;
// 定義過濾器
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Filter {
FilterType type() default FilterType.ANNOTATION;
@AliasFor("classes")
Class<?>[] value() default {};
@AliasFor("value")
Class<?>[] classes() default {};
String[] pattern() default {};
}
}
其中常用的屬性使用註釋進行了說明。配置向basePackages定義掃描的包名,basePackageClasses定義掃描的類名,所以上面的@ComponentScan註解還可以使用如下方式進行配置。此時運行IoCTest中的main方法,依然得到上述一樣的日誌輸出。
@ComponentScan(basePackages = "cn.zyt.springbootlearning.ioc.*")
或
@ComponentScan(basePackageClasses = {UserBean.class})
而上述的方式會將指定包下的所有Bean裝配到IoC容器中,而如果對於該指定包下的一些其他Bean不想裝配,則可以使用@ComponentScan註解中的includeFilters和excludeFilters屬性,例如,在上述cn.zyt.springbootlearning.ioc包下除了config包,還包括一個service包,其中定義了UserService類,該類使用了@Service註解,默認情況下也會被裝配到IoC容器中。如果此時僅想裝配UserBean而不裝配UserService,則可以使用excludeFilters屬性:
@ComponentScan(value = "cn.zyt.springbootlearning.ioc.*",
excludeFilters = {@ComponentScan.Filter(classes = {Service.class})})
這樣就可以僅掃描UserBean,而過濾掉@Service定義的Bean。
裝配第三方的Bean
在現實中的應用中,常需要引入第三方的包來進行操作,此時需要將第三方包的對象裝配到IoC容器中進行使用,此時就可以使用@Bean註解完成這項功能。例如,在maven中引入如下的第三方依賴:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.42</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
</dependency>
此時需要根據引入的依賴創建數據源來連接數據庫,將如下代碼加入到AppConfig類中:
@Bean("DbcpDataSource")
public DataSource getDataSource() {
Properties properties = new Properties();
properties.setProperty("driver", "com.mysql.jdbc.Driver");
properties.setProperty("url", "jdbc:mysql://localhost:3306/springboot_dev");
properties.setProperty("username", "root");
properties.setProperty("password", "123456");
DataSource dataSource = null;
try {
dataSource = BasicDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printStackTrace();
}
return dataSource;
}
通過@Bean就可以將這裏創建的DataSource對象裝配到IoC容器中了。
依賴注入
IoC容器的另一個作用爲管理Bean之間的依賴,下面以Person對象依賴Animal對象來實現一些服務的例子,說明IoC容器中Bean之間的依賴注入。首先創建Person接口和其實現類,Animal接口和其實現類。
public interface Person {
/**
* 人依賴於動物提供服務
*/
void service();
/**
* 設置依賴的動物
*/
void setAnimal(Animal animal);
}
public interface Animal {
/**
* 動物可以提供的服務
*/
void use();
}
上面兩個接口的實現類如下:
@Component
public class BusinessPerson implements Person {
@Autowired
private Animal animal;
@Override
public void service() {
this.animal.use();
}
@Override
public void setAnimal(Animal animal) {
this.animal = animal;
}
}
@Component
public class Dog implements Animal {
@Override
public void use() {
System.out.println("DOG can save person!");
}
}
注意BusinessPerson類中使用的@Autowired註解 ,它會根據屬性的類型(buType)找到對應的Bean進行注入,因爲Dog爲Animal的一種,所以這裏IoC容器會將Dog的Bean注入到BusinessPerson對象中,這樣BusinessPerson對象就可以通過注入的Dog對象進行相關的服務了。可以使用如下的代碼進行運行測試:
@Test
public void testPerson() {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
Person person = context.getBean(BusinessPerson.class);
person.service();
}
運行輸出如下:
...
17:50:49.712 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'appConfig'
17:50:49.745 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'dog'
DOG can save person!
@Autowired註解
下面進一步探索一下@Autowired註解的使用,上面創建了Dog類,下面可以創建一個Cat同樣是實現了Animal接口:
@Component
public class Cat implements Animal {
@Override
public void use() {
System.out.println("CAT can catch the mice!");
}
}
此時在運行上面的測試代碼,發現會得到如下異常:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'businessPerson': Unsatisfied dependency expressed through field 'animal'; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'cn.zyt.springbootlearning.ioc.injection.Animal' available: expected single matching bean but found 2: cat,dog
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:643)
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:116)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:399)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1422)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:594)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:517)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:323)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:321)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:879)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:878)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:550)
at org.springframework.context.annotation.AnnotationConfigApplicationContext.<init>(AnnotationConfigApplicationContext.java:89)
at cn.zyt.springbootlearning.ioc.IoCTest.testPerson(IoCTest.java:30)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'cn.zyt.springbootlearning.ioc.injection.Animal' available: expected single matching bean but found 2: cat,dog
at org.springframework.beans.factory.config.DependencyDescriptor.resolveNotUnique(DependencyDescriptor.java:220)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1265)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1207)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:640)
... 36 more
根據異常的信息,可以看出來這裏是因爲Animal有兩個實現類並都裝配成了Bean,@Autowired註解是根據byType的方式進行查找並注入Bean的,因此在IoC容器對Animal對象進行注入的時候就不知道注入哪一個了。
解決該問題其實很簡單,@Autowired註解首先是根據byType的方式getBean,如果找不到則會使用byName的方式進行查找。因此如果這裏需要使用的animal實際上是Dog,則將BusinessPerson中的屬性改爲如下即可:
@Component
public class BusinessPerson implements Person {
@Autowired
private Animal dog;
@Override
public void service() {
this.dog.use();
}
@Override
public void setAnimal(Animal animal) {
this.dog = animal;
}
}
此時運行測試代碼,得到的輸出和上述的輸出一樣,當想使用Cat對象進行服務的時候,則將屬性名稱該爲cat即可。需要注意的是@Autowired註解是一個默認必須找到對應Bean的註解,如果先後根據byType和byName的方式都沒有找到相應的Bean,則會拋出異常。如果允許某個Bean可以爲null,那麼可以爲@Autowired註解設置required=false屬性。
@Primary和Quelifier註解
此外,除了上面依賴於@Autowired註解根據bytype和byName先後順序進行Bean查找的方式,還有其他的方式可以解決上述的歧義性。@Primary註解可以修改Bean的優先級,當爲Cat類加上該註解時,說明Cat的優先級大於Dog,則這是IoC容器會優先將Cat對象進行注入,因此不會產生歧義:
@Component
@Primary
public class Cat implements Animal {
@Override
public void use() {
System.out.println("CAT can catch the mice!");
}
}
此時將BusinessPerson類中的屬性名改回animal,並運行測試方法可以恢復正常。但如果此時Dog類中也加入了@Primary註解,則歧義問題將會仍存在。
這時就需要使用@Qualifier註解,使用該註解可以設置其value屬性,爲需要注入的Bean的名稱,這樣IoC容器就會同時根據byTypeandName的方式來尋找相應的Bean並注入。此時也就是依賴於BeanFactory類中的如下重載方法:
<T> T getBean(String var1, Class<T> var2) throws BeansException;
在保留Cat類中的@Primary註解,並在BussinessPerson類中進行如下設置時:
@Autowired
@Qualifier("dog")
private Animal animal;
運行測試方法沒有異常,並且輸出:
DOG can save person!
因此說明,該註解作用的優先級是大於@Primary註解的。
Bean的生命週期
上述的內容僅是正確的將Bean裝配到IoC容器中,而沒有關心Bean如何創建和銷燬的過程。有時需要自定義Bean的初始化和銷燬方法,以滿足一些特殊Bean的要求。此時就需要對Bean的生命週期進行了解。
Spring IoC初始化和銷燬Bean的過程大致分爲如下四個步驟:
- Bean的定義。該過程包括如下幾個步驟。(1)資源定位,使用@ComponentScan註解定義的掃描路徑將帶有@Component的類找到。(2)通過找到的資源對Bean進行解析,此時還沒有初始化Bean,僅是解析的過程。(3)將Bean的定義發佈到IoC容器中,此時IoC容器中具有了Bean的定義,依然沒有Bean實例的創建。
- Bean的初始化。開始創建的Bean的實例,在默認情況下,Spring IoC容器會在容器初始化的時候執行Bean的實例化並進行依賴注入,也就是在獲取一個Bean之前,Bean的實例已經創建完成了。如果需要延遲初始化,可以在@ComponentScan註解中使用lazyInit屬性進行配置,該屬性默認爲false,改爲true即可以設置爲延遲初始化。
- Bean的生存期。
- Bean的銷燬。
Spring Bean的聲明週期過程圖:
將上述的BusinessPerson修改爲如下,對Bean的生命週期進行測試:
@Component
public class BusinessPerson implements Person, BeanNameAware, BeanFactoryAware,
ApplicationContextAware, InitializingBean, DisposableBean {
private Animal animal;
@Override
public void service() {
this.animal.use();
}
@Override
@Autowired
@Qualifier("dog")
public void setAnimal(Animal animal) {
System.out.println("延遲依賴注入測試");
this.animal = animal;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
System.out.println("【" + this.getClass().getSimpleName() + "】調用BeanFactoryAware的setBeanFactory方法");
}
@Override
public void setBeanName(String s) {
System.out.println("【" + this.getClass().getSimpleName() + "】調用BeanNameAware的setBeanName方法");
}
@PostConstruct
public void initCustom() {
System.out.println("【" + this.getClass().getSimpleName() + "】調用@PostConstruct的initCustom方法");
}
@PreDestroy
public void destroyCustom() {
System.out.println("【" + this.getClass().getSimpleName() + "】調用@PreDestroy的destroyCustom方法");
}
@Override
public void destroy() throws Exception {
System.out.println("【" + this.getClass().getSimpleName() + "】調用DisposableBean的destroy方法");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("【" + this.getClass().getSimpleName() + "】調用InitializingBean的afterPropertiesSet方法");
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
System.out.println("【" + this.getClass().getSimpleName() + "】調用ApplicationContextAware的setApplicationContext方法");
}
}
這樣找個Bean就試下了生命週期中單個Bean可以實現的所有接口,並且通過註解@PostConstruct定義了初始化方法,@PreDestroy定義了銷燬方法。爲了測試Bean的後置處理器,這裏創建一個BeanProcessorExample示例類:
@Component
public class BeanPostProcessorExample implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("BeanPostProcessor調用postProcessBeforeInitialization方法,參數【"
+ bean.getClass().getSimpleName() + "】【" + beanName + "】");
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("BeanPostProcessor調用postProcessAfterInitialization方法,參數【"
+ bean.getClass().getSimpleName() + "】【" + beanName + "】");
return null;
}
}
運行如下代碼:
@Test
public void beanLifecycleTest() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
context.close();
}
得到的日誌輸出如下:
BeanPostProcessor調用postProcessBeforeInitialization方法,參數【AppConfig$$EnhancerBySpringCGLIB$$32ff2f1c】【appConfig】
BeanPostProcessor調用postProcessAfterInitialization方法,參數【AppConfig$$EnhancerBySpringCGLIB$$32ff2f1c】【appConfig】
21:19:06.041 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'UserBean'
BeanPostProcessor調用postProcessBeforeInitialization方法,參數【UserBean】【UserBean】
BeanPostProcessor調用postProcessAfterInitialization方法,參數【UserBean】【UserBean】
21:19:06.052 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'businessPerson'
21:19:06.086 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'dog'
BeanPostProcessor調用postProcessBeforeInitialization方法,參數【Dog】【dog】
BeanPostProcessor調用postProcessAfterInitialization方法,參數【Dog】【dog】
延遲依賴注入測試
【BusinessPerson】調用BeanNameAware的setBeanName方法
【BusinessPerson】調用BeanFactoryAware的setBeanFactory方法
【BusinessPerson】調用ApplicationContextAware的setApplicationContext方法
BeanPostProcessor調用postProcessBeforeInitialization方法,參數【BusinessPerson】【businessPerson】
【BusinessPerson】調用@PostConstruct的initCustom方法
【BusinessPerson】調用InitializingBean的afterPropertiesSet方法
BeanPostProcessor調用postProcessAfterInitialization方法,參數【BusinessPerson】【businessPerson】
21:19:06.087 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'cat'
BeanPostProcessor調用postProcessBeforeInitialization方法,參數【Cat】【cat】
BeanPostProcessor調用postProcessAfterInitialization方法,參數【Cat】【cat】
21:19:06.088 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'DbcpDataSource'
BeanPostProcessor調用postProcessBeforeInitialization方法,參數【BasicDataSource】【DbcpDataSource】
BeanPostProcessor調用postProcessAfterInitialization方法,參數【BasicDataSource】【DbcpDataSource】
21:19:06.125 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@402f32ff, started on Thu Feb 20 21:19:05 CST 2020
【BusinessPerson】調用@PreDestroy的destroyCustom方法
【BusinessPerson】調用DisposableBean的destroy方法
通過日誌可以看到,對於Bean的後置處理器BeanProcessor而言,其中的方法對所有的Bean生效。並且在BusinessPerson類中的方法執行順序與上述生命週期過程圖一致。
對於Bean生命週期的底層詳解可以參考:https://www.jianshu.com/p/1dec08d290c1
使用屬性配置文件
在默認application.properties配置文件中使用自定義屬性
Spring Boot項目中默認使用的是application.properties配置文件,其中添加的已經被定義的屬性會被Spring Boot自動讀取到上下文中。如果需要在application.properties配置文件中添加自定義的屬性,並在項目中引用,則需要首先加入如下的依賴:
<!-- 自定義屬性配置依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
這樣在application.properties文件中添加的自定義屬性就可以在項目中引用了。例如在application.properties配置文件中定義瞭如下屬性:
# 自定義的一些屬性
database.drivername=com.mysql.jdbc.Driver
database.url=jdbc:mysql://localhost:3306/springboot_dev
database.username=root
database.password=123456
使用Spring EL表達式將配置文件中的內容賦值到類屬性中:
@Component
public class DatabaseProperties {
@Value("${database.drivername}")
private String driverName;
@Value("${database.url}")
private String url;
private String username;
private String password;
public String getDriverName() {
return driverName;
}
public void setDriverName(String driverName) {
System.out.println(driverName);
this.driverName = driverName;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
System.out.println(url);
this.url = url;
}
public String getUsername() {
return username;
}
@Value("${database.username}")
public void setUsername(String username) {
System.out.println(username);
this.username = username;
}
public String getPassword() {
return password;
}
@Value("${database.password}")
public void setPassword(String password) {
System.out.println(password);
this.password = password;
}
}
啓動Spring Boot項目可以看到日誌輸出如下:
root
123456
此外,如果需要將自定義的屬性配置在其他配置文件中,例如將上述數據庫連接的屬性配置在了jdbc.properties文件中,那麼可以使用@PropertySource註解在Spring Boot啓動類中定義需要加載的自定義屬性文件,將他加載到Spring上下文中,例如:
@SpringBootApplication(scanBasePackages = {"cn.zyt.springbootlearning.*"})
@MapperScan(basePackages = "cn.zyt.springbootlearning.*", annotationClass = Repository.class)
@PropertySource(value = {"classpath:jdbc.properties"}, ignoreResourceNotFound = true)
public class SpringbootLearningApplication {
// ...
}
此時已然可以找到在jdbc.properties配置文件中的自定義屬性並進行加載。而其中的ignoreResourceNotFound屬性設置爲true, 表示在該配置文件找不到時進行忽略而不會報錯。
application.yml默認配置文件
Spring Boot中除了默認的application.properties配置文件之外,還有一個application.yml配置文件,如果使用後者作爲配置文件可以將application.properties刪除,新建名爲application.yml文件即可。yml格式的配置文件支持樹型的配置格式,例如如下:
server:
port: 8080
session-timeout: 30
tomcat.max-threads: 0
tomcat.uri-encoding: UTF-8
spring:
datasource:
url : jdbc:mysql://localhost:3306/springboot
username : root
password : root
driverClassName : com.mysql.jdbc.Driver
它是將application.properties文件中的配置項,使用樹形結構表示,比較清晰。
條件裝配Bean
@Conditional註解支持Spring Boot中Bean的條件裝配,也就是在IoC容器在裝配Bean會對指定的一些條件進行檢查,從而避免一些Bean裝配的異常。例如如下代碼,在裝配DataSource Bean之前,先檢查是否存在相應的配置項:
@Bean
@Conditional(DatabaseConditional.class)
public DataSource getDataSourceWithConditional(
@Value("${database.drivername}") String driver,
@Value("${database.url}") String url,
@Value("${database.username}") String username,
@Value("${database.password}") String password) {
Properties properties = new Properties();
properties.setProperty("driver", driver);
properties.setProperty("url", url);
properties.setProperty("username", username);
properties.setProperty("password", password);
DataSource dataSource = null;
try {
dataSource = BasicDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printStackTrace();
}
return dataSource;
}
相應的DatabaseConditional類如下:
public class DatabaseConditional implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
// 獲取環境配置
Environment environment = conditionContext.getEnvironment();
// 檢查配置文件中是否包含相應的屬性
return environment.containsProperty("database.drivername")
&& environment.containsProperty("database.url")
&& environment.containsProperty("database.username")
&& environment.containsProperty("database.password");
}
}
在裝配Bean之前matches方法首先讀取其上下文環境,判斷配置文件中是否包含該Bean需要的屬性。如果返回爲false,則不裝配該Bean。
Bean的作用域
在一般的容器中,Bean存在單例(singleton)和原型(prototype)兩種作用域。而在Web容器中,則存在如下四種作用域:
- page,頁面
- request,請求
- session,會話
- application,應用
其中對於page,是針對JSP當前頁面的作用域,Spring是無法支持的。爲了滿足各類的作用域,在Spring的作用域中就存在瞭如下的幾種類型:
Bean的作用域類型 | 使用範圍 | 描述 |
---|---|---|
singleton | 所有Spring應用 | 默認作用域,IoC容器中只存在單例 |
prototype | 所有Spring應用 | 每當從IoC容器中取出一個Bean時,則新創建一個Bean |
session | Spring Web應用 | HTTP會話 |
application | Spring Web應用 | Web工程聲明週期 |
request | Spring Web應用 | Web工程單次請求(request) |
globalSession | Spring Web應用 | 在一個全局HTTP session中,一個Bean定義對應一個勝利。基本不使用 |
以上6周Bean的作用域中,常用的爲前四種,下面對單例和原型兩種作用域進行一些測試。
首先創建一個Bean,由於沒有任何作用域的設置,所以此時爲默認的作用域singleton:
@Component
public class ScopeBean {
}
使用如下測試代碼,先後從容器中獲取兩個該類型的Bean,然後進行比較:
@Test
public void beanScopeTest() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
ScopeBean scopeBean = context.getBean(ScopeBean.class);
ScopeBean scopeBean1 = context.getBean(ScopeBean.class);
System.out.println(scopeBean == scopeBean1);
}
運行得到的結果爲true,說明先後兩次得到的ScopeBean類型的Bean指向了同一個實例對象,所以默認在ioC容器中Bean只有一個。下面將ScopeBean修改爲:
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ScopeBean {
}
運行同樣的測試,得到的結果爲false,說明在對個該Bean的作用域設置爲原型後,每次取出的Bean都是一個新創建的實例。
上面使用的爲ConfigurableBeanFactory,其只支持單例和原型兩種範圍,如果使用Spring MVC環境中的WebApplicationContext來定義作用域,其支持SCOPE_REQUEST, SCOPE_SESSION, SCOPE_APPLICATION的作用域。
使用@Profile
在企業開發的過程中,項目往往會存在開發環境,測試環境,預發環境,和生產環境不同的應用環境,而每套環境中的上下文等是不同的,例如他們會有各自的數據源和相應的數據庫,這樣就需要在不同的數據庫之間進行切換。Spring @Profile註解爲此提供了支持。
下面假設存在dev和test兩個數據庫,可以使用@Profile註解來定義兩個不同的Bean:
@Bean("DevDataSource")
@Profile("dev")
public DataSource getDevDataSource() {
Properties properties = new Properties();
properties.setProperty("driver", "com.mysql.jdbc.Driver");
properties.setProperty("url", "jdbc:mysql://localhost:3306/dev");
properties.setProperty("username", "root");
properties.setProperty("password", "123456");
DataSource dataSource = null;
try {
dataSource = BasicDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printStackTrace();
}
return dataSource;
}
@Bean("TestDataSource")
@Profile("test")
public DataSource getTestDataSource() {
Properties properties = new Properties();
properties.setProperty("driver", "com.mysql.jdbc.Driver");
properties.setProperty("url", "jdbc:mysql://localhost:3306/test");
properties.setProperty("username", "root");
properties.setProperty("password", "123456");
DataSource dataSource = null;
try {
dataSource = BasicDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printStackTrace();
}
return dataSource;
}
在Spring中存在兩個參數來啓動並修改對應的profile,分別是spring.profiles.active和spring.profiles.default。這兩個屬性在默認沒有配置的情況下,Spring將不會啓動profile機制,這意味着被@Profile標註的Bean不會被裝配到IoC容器中。Spring會先判斷是否存在spring.profiles.active配置後,在去查找spring.profiles.default。
在Java啓動項目中,只需要加入如下的啓動配置,就能夠啓動相應環境的bean:
JAVA_OPTS="-Dspring.profiles.active=dev"
在IDEA中啓動時,可以在運行參數中對其進行設置(這裏是Java Application):
在IoCTest類中的main方法進行測試:
public static void main(String[] args) throws SQLException {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
context.getBean(DataSource.class);
}
可以看到如下的日誌輸出:
12:05:35.209 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'DevDataSource'
此時將DevDataSource Bean進行了裝配,而沒有裝配TestDataSource。
此外,Spring Boot中支持在不同環境中配置文件的切換,對於dev環境下的配置文件可以新增application-dev.properties,然後在啓動參數中設置 -Dspring.profiles.active=dev時,就會對應的加載application-dev.properties的配置文件。