目錄
IOC(控制反轉)是Spring的核心,可以說Spring是一種基於IOC容器編程的框架。因爲Spring Boot是基於註解的開發Spring IOC,所以我們使用全註解的方式講述Spring IOC技術。
1.IOC容器簡介
SpringIOC容器是一個管理Bean的容器,在Spring的定義中,它要求所有的IOC容器都需要實現接口BeanFactory,它是一個頂級的容器接口。BeanFactory的實現代碼如下;
package org.springframework.beans.factory;
import org.springframework.beans.BeansException;
import org.springframework.core.ResolvableType;
public interface BeanFactory {
//前綴
String FACTORY_BEAN_PREFIX = "&";
//多個getBean方法
Object getBean(String var1) throws BeansException;
<T> T getBean(String var1, Class<T> var2) throws BeansException;
<T> T getBean(Class<T> var1) throws BeansException;
Object getBean(String var1, Object... var2) throws BeansException;
<T> T getBean(Class<T> var1, Object... var2) throws BeansException;
//是否包含Bean
boolean containsBean(String var1);
//是否是單例
boolean isSingleton(String var1) throws NoSuchBeanDefinitionException;
//是否原型
boolean isPrototype(String var1) throws NoSuchBeanDefinitionException;
//是否類型匹配
boolean isTypeMatch(String var1, ResolvableType var2) throws NoSuchBeanDefinitionException;
boolean isTypeMatch(String var1, Class<?> var2) throws NoSuchBeanDefinitionException;
//獲取Bean類型
Class<?> getType(String var1) throws NoSuchBeanDefinitionException;
//獲取Bean別名
String[] getAliases(String var1);
}
Spring IOC容器中,默認的情況下,Bean都是以單例存在的,也就是說getBean方法返回的都是同一個對象。isSingleton方法則判斷Bean是否在Spring IOC中爲單例。與isSingleton相反的是isPrototype方法,如果它返回的是true,那麼我們使用getBean方法獲取Bean的時候,Spring IOC容器就會創建一個新的Bean返回給調用者。
由於BeanFactory的功能還不夠強大,因此Spring在BeanFactory的基礎上,設計了更爲高級的接口ApplicationContext。它是BeanFactory的子接口之一,在Spring的體系中BeanFactory和ApplicationContext是最重要的兩個接口。在Spring中,我們使用的大部分的Spring IOC容器都是ApplicationContext接口的實現類,它們的關係如下所示:
ApplicationContext接口通過繼承上級接口,進而繼承BeanFactory接口,但是在BeanFactory的基礎上,擴展了消息國際化接口、環境可配置接口、應用事件發佈接口和資源模式解析接口,所以它的功能會更爲強大。
爲了貼近Spring Boot的需要,這裏主要介紹一個基於註解的IOC容器:AnnotationConfigApplicationContext,它是一個基於註解的IOC容器。Spring Boot裝配和獲取Bean的方法和該容器如出一轍。首先看如下簡單的實例:
定義一個pojo:
package com.martin.config.chapter3.pojo;
import lombok.Data;
/**
* @author: martin
* @date: 2019/10/27 17:33
* @description:
*/
@Data
public class User {
private Long id;
private String userName;
private String note;
}
定義Java配置文件:
package com.martin.config.chapter3.config;
import com.martin.config.chapter3.pojo.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author: martin
* @date: 2019/10/27 17:39
* @description:
*/
@Configuration
public class AppConfig {
@Bean("user")
public User initUser() {
User user = new User();
user.setId(1L);
user.setUserName("martin");
user.setNote("note_1");
return user;
}
}
這裏@Configuration代表這是一個Java配置文件,Spring的容器會根據它來生成IOC容器去裝配Bean;@Bean代表將initUser方法返回的POJO裝配到IOC容器中,而其屬性name定義了這個Bean的名稱,如果沒有配置它,則將方法名稱“initUser”作爲Bean的名稱保存到Spring IOC容器中。最後,我們使用AnnotationConfigApplicationContext來構建自己的IOC容器,代碼清單如下:
package com.martin.config.chapter3;
import com.martin.config.chapter3.config.AppConfig;
import com.martin.config.chapter3.pojo.User;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
/**
* @author: martin
* @date: 2019/10/27 18:57
* @description:
*/
public class IocTest {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
User user = ctx.getBean(User.class);
System.out.println(user.getId());
}
}
2.裝配Bean
2.1 通過掃描裝配Bean
除了使用註解@Bean注入Spring IOC容器,我們還可以使用掃描裝配Bean到IOC容器中,對於掃描裝配而言使用的註解是@Component和@ComponentScan。@Component是標明哪個類被掃描進入Spring IOC容器,而@ComponentScan則是標明採用何種策略去掃描裝配Bean。User使用@Component的實現代碼如下:
package com.martin.config.chapter3.pojo;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* @author: martin
* @date: 2019/10/27 17:33
* @description:
*/
@Data
@Component("user")
public class User {
@Value("1")
private Long id;
@Value("martin")
private String userName;
@Value("note")
private String note;
}
這裏的註解@Component表明這個類將被Spring IOC容器掃描裝配,其中配置的“user”則是作爲Bean的名稱。如果不配置這個字符串,IOC容器會把類名的第一個字母作爲小寫,其他不變作爲Bean名稱放入到IOC容器中;註解@Value則指定具體的值,使得Spring IOC給與對應的屬性注入對應的值。
爲了讓IOC容器能裝配User這個類,需要改造AppConfig,代碼如下:
package com.martin.config.chapter3.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
* @author: martin
* @date: 2019/10/27 17:39
* @description:
*/
@Configuration
@ComponentScan
public class AppConfig {
}
這裏加入了@ComponentScan,意味着它會進行自動掃描,但是它只會掃描類AppConfig所在的當前包和其子包,User類不屬於AppConfig的包或子包中的類,因此無法掃描到。如果想被掃描到,可以自定義掃描的包,可以通過basePackages定義掃描的包名,還可以通過basePackageClasses定義掃描的類。代碼如下:
@ComponentScan("com.martin.config.chapter3.pojo.*")
@ComponentScan(basePackages = "com.martin.config.chapter3.pojo.*")
@ComponentScan(basePackageClasses = User.class)
按照以上的配置策略,User類會被掃描到Spring IOC容器中。
2.2 自定義第三方Bean
現實的Java的應用往往需要引入許多來自第三方的包,並且希望把第三方包的類對象也放入到Spring IOC容器中,這時@Bean註解就可以發揮作用了。例如我們定義DHCP數據源,POM文件如下:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
這樣DHCP和數據庫驅動就被加入到了項目中,接着將使用它提供的機制來生成數據源。代碼如下:
package com.martin.config.chapter3.config;
import org.apache.commons.dbcp2.BasicDataSourceFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.util.Properties;
/**
* @author: martin
* @date: 2019/10/27 17:39
* @description:
*/
@Configuration
public class AppConfig {
@Bean("dataSource")
public DataSource getDataSource() {
Properties properties = new Properties();
properties.setProperty("driver", "com.mysql.jdbc.Driver");
properties.setProperty("url", "jdbc:mysql://localhost:3306/chapter3");
properties.setProperty("username", "root");
properties.setProperty("password", "123456");
try {
return BasicDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
這裏通過@Bean定義了其配置項name爲dataSource,IOC容器就會把它作爲對象名保存起來。
3.依賴注入
不同Bean之間的依賴被稱爲依賴注入,依賴注入主要使用@Autowired註解。例如我們注入一個user屬性:
@Autowired
private User user;
@Autowired註解也可以用到方法上:
@Autowired
public void setUser(User user) {
this.user = user;
}
@Autowired是一個默認必須找到對應Bean的註解,如果不能確定其標註屬性一定會存在並且允許這個被標註的屬性爲null,那麼可以配置@Autowired屬性required爲false。
@Autowired(required = false)
private User user;
如果User的實現類有多個,比如管理員admin,普通用戶general ,此時單純使用@Autowired註解,IOC容器時無法區分採用哪個Bean實例注入。我們可以使用@Quelifier註解,與@Autowired組合在一起,通過類型和名稱一起找到Bean。
@Autowired
@Qualifier("admin")
private User user;
Spring IOC將會以類型和名稱一起去尋找對應的Bean進行注入。
4.生命週期
Bean的聲明週期過程,大致分爲Bean定義、Bean的初始化、Bean的生存期和Bean的銷燬4個部分,其中Bean定義過程大致如下:
- Spring通過配置,如@ComponentScan定義的掃描路徑去找到帶有@Component的類,這個過程就是一個資源定位的過程
- 一旦找到資源,那麼就開始解析,並且將定義的信息保存起來。注意,此時還沒有初始化Bean,也沒有Bean的實例,僅僅有Bean的定義
- 把Bean定義發佈到Spring IOC容器中。
完成了這3步只是一個資源定位並將Bean的定義發佈到IOC容器的過程,還沒有Bean實例的生成,更沒有完成依賴注入。在默認情況下,接下來的步驟是完成Bean的實例化和依賴注入,這樣就能從IOC容器中獲取到一個Bean實例。如果我們設置lazyInit(懶加載)的值爲true,那麼Spring並不會在發佈Bean定義後馬上爲我們完成實例化和依賴注入,而是要等到真正使用到的時候纔開始實例化和依賴注入。
Spring在完成依賴注入之後,還提供了一系列的接口和配置來完成Bean的初始化過程,整個流程如下:
Spring IOC容器最低的要求是實現BeanFactory接口,而不是實現ApplicationContext接口。對於那些沒有實現ApplicationContext接口的容器,在生命週期對應的ApplicationContextAware定義的方法也不會調用,只有實現了ApplicationContext接口的容器,纔會在生命週期調用ApplicationContextAware所定義的setApplicationContext方法。爲了測試生命週期我們定義一個BussinessPerson類,代碼如下:
package com.martin.config.chapter3.service;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
/**
* @author: martin
* @date: 2019/10/28 22:09
* @description:
*/
@Component
public class BussinessPerson implements BeanNameAware, BeanFactoryAware, ApplicationContextAware, InitializingBean, DisposableBean {
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
System.out.println("調用BeanFactoryAware的setBeanFactory");
}
@Override
public void setBeanName(String name) {
System.out.println("調用BeanNameAware的setBeanName方法:" + name);
}
@Override
public void destroy() throws Exception {
System.out.println("調用DisposableBean的destroy");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("調用InitializingBean的afterPropertiesSet");
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
System.out.println("調用ApplicationContextAware的setApplicationContext");
}
@PostConstruct
public void init() {
System.out.println("調用@PostConstruct標識的方法");
}
@PreDestroy
public void preDestroy() {
System.out.println("調用@PreDestroy標識的方法");
}
}
這樣,BussinessPerson 這個Bean就實現了生命週期中單個Bean可以實現的所有接口。爲了測試Bean的後置處理器,我們創建一個類BeanPostProcessorExample,該後置處理器將對所有的Bean都有效,代碼如下:
package com.martin.config.chapter3.service;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
/**
* @author: martin
* @date: 2019/10/28 22:31
* @description:
*/
@Component
public class BeanPostProcessorTest implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("BeanPostProcessor調用before" + beanName);
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("BeanPostProcessor調用After" + beanName);
return bean;
}
}
運行Spring Boot應用程序,輸出結果如下:
調用BeanNameAware的setBeanName方法:bussinessPerson
調用BeanFactoryAware的setBeanFactory
調用ApplicationContextAware的setApplicationContext
BeanPostProcessor調用beforebussinessPerson
調用@PostConstruct標識的方法
調用InitializingBean的afterPropertiesSet
BeanPostProcessor調用AfterbussinessPerson
BeanPostProcessor調用beforedataSource
BeanPostProcessor調用AfterdataSource
對於Bean後置處理器(BeanPostProcessor)而言,它對所有的Bean都起作用,而其他的接口則是對於單個Bean起作用。有時候Bean的定義可能使用的是第三方的類,此時可以使用註解@Bean來配置自定義初始化和銷燬方法,代碼如下:
@Bean(initMethod='init',destroyMethod='destroy')
5.使用屬性文件
在Spring Boot中使用屬性文件,可以採用默認的application.properties,也可以使用自定義的配置文件。在Spring Boot中,我們在Maven中添加依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
有了該依賴,就可以直接使用application.properties文件爲我們工作了。配置文件如下:
database.driverName=com.mysql.jdbc.Driver
database.url=jdbc:mysql://localhost:3306/test
database.username=root
database.password=123456
我們使用Spring表達式的方式獲取配置的屬性值:
package com.martin.config.chapter3.pojo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* @author: martin
* @date: 2019/11/2 17:23
* @description:
*/
@Component
public class DataBaseProperties {
@Value("${database.driverName}")
private String driverName;
@Value("${database.url}")
private String url;
private String password;
private String userName;
public String getDriverName() {
return driverName;
}
public void setDriverName(String driverName) {
this.driverName = driverName;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getPassword() {
return password;
}
@Value("${database.password}")
public void setPassword(String password) {
this.password = password;
}
public String getUserName() {
return userName;
}
@Value("${database.username}")
public void setUserName(String userName) {
this.userName = userName;
}
}
通過Spring表達式@Value註解,讀取配置在屬性文件的內容。@Value註解既可以加載屬性,也可以加在方法上。
我們也可以使用註解@ConfigurationProperties來配置屬性,代碼如下:
package com.martin.config.chapter3.pojo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @author: martin
* @date: 2019/11/2 17:23
* @description:
*/
@Component
@ConfigurationProperties("database")
public class DataBaseProperties {
private String driverName;
private String url;
private String password;
private String userName;
public String getDriverName() {
return driverName;
}
public void setDriverName(String driverName) {
this.driverName = driverName;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
這裏在@ConfigurationProperties中配置的字符串database,將與POJO的屬性名稱組成屬性屬性的全限定名去配置文件裏查找,這樣就能將對應的屬性讀取到POJO當中。
有時候我們會覺得如果把所有的屬性配置到放置到application.properties中,這個文件將會有很多屬性內容。爲了更好的配置,我們將數據庫的屬性配置到jdbc.properties中,然後使用@PropertySource去定義對應的屬性文件,把它加載到Spring的上下文中。代碼如下所示:
package com.martin.config;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.PropertySource;
/**
* 配置啓動類
*
* @author martin
* @create 2019-01-03 下午 11:20
**/
@SpringBootApplication
@PropertySource(value = {"classpath:jdbc.properties"}, ignoreResourceNotFound = true)
public class SpringBootConfigApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootConfigApplication.class, args);
}
}
value可以配置多個配置文件,使用classpath前綴,意味着去類文件路徑下找到屬性文件;ignoreResourceNotFound則是是否忽略配置文件找不到的問題,默認值是false,也就是找不到屬性文件就會報錯。
6.條件裝配Bean
有時候我們希望IOC容器在某些條件滿足下才去裝配Bean,例如我們要求在數據庫的配置中,驅動名、url、賬號和密碼都存在的情況下才去連接數據庫。此時我們需要使用@Conditional註解和實現Condition接口的類。代碼如下:
package com.martin.config.chapter3.config;
import org.apache.commons.dbcp2.BasicDataSourceFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.util.Properties;
/**
* @author: martin
* @date: 2019/10/27 17:39
* @description:
*/
@Configuration
public class AppConfig {
@Bean("dataSource")
@Conditional(DatabaseConditional.class)
public DataSource getDataSource(@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);
try {
return BasicDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
加入了@Conditionnal註解,並且配置了類DatabaseConditional,那麼這個類就必須實現Condition接口。代碼如下:
package com.martin.config.chapter3.config;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* @author: martin
* @date: 2019/11/2 20:08
* @description:
*/
public class DatabaseConditional implements Condition {
/**
* 數據庫裝配條件
*
* @param context 條件上下文
* @param metadata 註釋類型的元數據
* @return true裝配Bean,否則不裝配
*/
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment environment = context.getEnvironment();
//判斷屬性文件是否存在對應的數據庫配置
return environment.containsProperty("database.driverName")
&& environment.containsProperty("database.url")
&& environment.containsProperty("database.username")
&& environment.containsProperty("database.password");
}
}
maches方法讀取上下文環境,判斷是否已經配置了對應的數據庫信息。當都配置好了以後返回true,Spring會裝配數據庫連接池的Bean,否則不裝配。
7.Bean的作用域
IOC容器最頂級接口BeanFactory中,可以看到isSingleton和isPrototype兩個方法。其中,如果isSingleton方法如果返回true,則Bean在IOC容器中以單例存在,這也是Spring IOC容器的默認值。如果isPrototype方法返回true,則當我們每次獲取Bean的時候,IOC容器都會創建一個新的Bean。除了這些,Bean還存在其他類型的作用域:
對於application作用域,完全可以使用單例來代替。
作用域的定義使用@Scope註解,實例代碼如下:
package com.martin.config.chapter3.service;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;
/**
* @author: martin
* @date: 2019/11/2 22:25
* @description:
*/
@Component
@Scope(WebApplicationContext.SCOPE_REQUEST)
public class ScopeBean {
}
8.@Profile
在企業開發的過程中,項目往往需要面臨開發環境,測試環境,愈發環境和生產環境的切換,而每一套環境的上下文是不一樣的。例如,它們會有各自的數據庫資源,這樣就需要我們再不同的數據庫之前進行切換。爲了方便,Spring提供了Profile機制,使得我們可以方便地實現各個環境之間的切換。
在Spring中存在兩個配置參數可以提供給我們修改啓動Profile機制,一個是spring.profiles.active,另外一個是spring.profiles.default。在兩個屬性參數都沒有配置的情況下,Spring將不會啓動Profile機制。Spring先判斷是否存在spring.profiles.active配置之後,再去查找spring.profiles.default配置,所以spring.profiles.active優先級大於spring.profiles.default。
在Java項目啓動時,我們配置如下就能夠啓動Profile機制:
JAVA_OPTS=“-Dspring.profiles.active=dev”
在Spring Boot的規則中,假設把選項-Dspring.profiles.active配置的值記爲{profile},它就會用application-{profile}.properties文件去代替原來默認的application.properties文件,然後啓動Spring Boot的程序。
9.引入XML配置的Bean
儘管Spring Boot建議使用註解和掃描配置Bean,但是它並不拒絕使用XML配置Bean。如果我們想在Spring Boot中使用XML對Bean進行配置,可以使用註解@ImportResource,通過它可以引入對應的XML文件,用以加載Bean。實例代碼如下:
Configuration
@ImportResource(value = {"classpath:spring-job.xml"})
public class AppConfig {
}
這樣就可以引入對應的XML,從而將XML定義的Bean裝配到IOC容器中。
10.Spring EL表達式
在前面的代碼中,我們是在沒有任何運算規則的情況下裝配Bean的。爲了更加靈活,Spring EL表達式爲我們提供了強大的運算規則來更好的裝配Bean。
EL表達式最常用的是讀取配置屬性文件中的值,例如:
@Value("${database.driverName}")
除此之外,它還能夠調用方法,例如,記錄Bean的初始化時間:
@Value("#{T(System).currentTimeMillis()}")
private Long time;
這裏採用#{.......}代表啓用Spring表達式,它將具有運算的功能;T(......)代表的是引入類;System是java.lang.*包的類,這是java 默認加載的包,可以不用寫全限定名。如果是其他的包,需要寫出全限定名才能引用類。此外EL表達式支持多種賦值方式,代碼如下:
package com.martin.config.chapter3.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* @author: martin
* @date: 2019/11/2 23:06
* @description:
*/
@Component
public class SpringEl {
@Value("${database.driverName}")
private String driver;
@Value("#{T(System).currentTimeMillis()}")
private Long time;
/**
* 使用字符串直接賦值
*/
@Value("#{'使用Spring EL賦值字符串'}")
private String str;
/**
* 科學計數法賦值
*/
@Value("#{9.3E3}")
private Double d;
/**
* 賦值浮點數
*/
@Value("#{3.14}")
private float pi;
/**
* 使用其他Bean的屬性來賦值
*/
@Value("#{beanName.str}")
private String otherBeanProp;
/**
* 使用其他Bean的屬性來賦值,轉換爲大寫
*/
@Value("#{beanName.str?.toUpperCase()}")
private String otherBeanPropUpperCase;
/**
* 數學運算
*/
@Value("#{1+2}")
private int run;
/**
* 浮點數比較運算
*/
@Value("#{beanName.pi == 3.14f}")
private boolean piFlag;
}