前言
對于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.AnnotationConfigA[email protected], 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的配置檔案。