認(rèn)識Spring
Spring是一個開源框架,目的是為了簡化Java開發(fā)。
為了降低Java開發(fā)的復(fù)雜性,Spring采取了以下4種策略:
- 基于POJO的輕量級和最小侵入性編程;
- 通過依賴注入和面向接口實(shí)現(xiàn)松耦合;
- 基于切面和慣例進(jìn)行聲明式編程;
- 通過切面和模板減少樣板式代碼;
POJO
POJO 全稱是Plain Ordinary Java Object,翻譯過來即普通Java類。普通的一個類為什么要用POJO來稱呼那?直接說一個類不就完了嘛。POJO主要用來指代那些沒有遵從特定的Java對象模型、約定或框架的Java對象,強(qiáng)調(diào)的是不受約束。
依賴注入(DI)
當(dāng)一個類A中需要用到另一個類B的時,如下面所示:
public class A {
private B b;
public A() {
b = new B();
}
}
這里類A與類B就存在了耦合,為避免這種耦合,我們不應(yīng)該在類A中創(chuàng)建B的實(shí)例,而是交給第三方,把對B的控制權(quán)叫出來,所以稱之為控制反轉(zhuǎn)(IOC)。那既然類B不是在類A中創(chuàng)建,那么如何才能把類B的實(shí)例交給類A那?要么通過構(gòu)造,要么通過set方法,而這就是依賴注入(DI)。
class A {
private B b;
public A(B b) {
this.b = b;
}
}
依賴注入實(shí)現(xiàn)了控制反轉(zhuǎn),實(shí)現(xiàn)了松耦合。但是也導(dǎo)致要寫更多的代碼,例如我們要是上面的類A,可能需要這樣寫:
B b = new B();
A a = new A(b);
這是最簡單的情況,如果類A依賴的類很多,則需要一個個實(shí)例化被依賴的類,然后注入到類A中。而Spring可以幫我們省下這些代碼。通過容器管理bean,也即是類A,類B等。
Spring容器
Spring容器可以歸納為兩種:BeanFactory和ApplicationContext。通常我們會選擇ApplicationContext,它提供了應(yīng)用框架級別的服務(wù),例如從屬性文件解析文本信息,以及發(fā)布應(yīng)用事件。
ApplicationContext有多種實(shí)現(xiàn):
- AnnotationConfigApplicationContext:從一個或多個基于Java的配置類中加載Spring應(yīng)用上下文。
- AnnotationConfigWebApplicationContext:從一個或多個基于Java的配置類中加載Spring Web應(yīng)用上下文。
- ClassPathXmlApplicationContext:從類路徑下的一個或多個xml配置文件中加載上下文定義,把應(yīng)用上下文的定義文件作為類資源。
- FileSystemXmlApplicationContext:從文件系統(tǒng)下的一個或多個xml配置文件中加載上下文定義。
- XmlWebApplicationContext:從web應(yīng)用下的一個或多個xml配置文件中加載上下文定義。
其實(shí)區(qū)別也就是從不同的地方加載bean的配置文件。
裝配bean
Spring容器負(fù)責(zé)創(chuàng)建應(yīng)用程序中的bean并通過DI來協(xié)調(diào)這些對象之間的關(guān)系。而我們要做事情則是告訴Spring容器,哪些是bean。我們有三種裝配機(jī)制可以選擇:
- 在xml中進(jìn)行顯示配置;
- 在Java中進(jìn)行顯示配置;
- 隱式的bean發(fā)現(xiàn)機(jī)制和自動裝配;
準(zhǔn)備
在裝配之前,我們先來準(zhǔn)備幾個POJO類。
一個cd接口:
public interface CD {
void play();
}
一個cd接口的實(shí)現(xiàn)類:
public class SgtPeppers implements CD {
@Override
public void play() {
System.out.println("Playing Sgt. Pepper's Lonely Heart Club Band by The Beatles");
}
}
一個cd播放器用于播放cd:
public class CDPlayer {
private CD cd;
public CDPlayer(CD cd) {
this.cd = cd;
}
public void play() {
cd.play();
}
}
自動裝配bean
我們通過@Configuration注解一個類表示這個類為Spring的配置類。并且通過@ComponentScan注解開啟自動掃描bean:
@Configuration
@ComponentScan
public class AutoConfig {
}
@ComponentScan默認(rèn)會掃描與配置類同級以及子級包中所有帶有@Component注解的類,自動創(chuàng)建為一個bean。 我們在需要裝配的bean上添加注解:
@Component
public class SgtPeppers implements CD {
...
}
@Component("player")
public class CDPlayer {
...
}
接下來,我們可以AnnotationConfigApplicationContext類加載Spring配置類,看下SgtPeppers是否被自動掃描并創(chuàng)建了bean:
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AutoConfig.class);
System.out.println("----------------------------------------------");
String[] names = applicationContext.getBeanDefinitionNames();
System.out.println(Arrays.toString(names));
System.out.println("----------------------------------------------");
CD cd = (CD) applicationContext.getBean("sgtPeppers");
System.out.println(cd);
CDPlayer cdPlayer = (CDPlayer) applicationContext.getBean("player");
System.out.println(cdPlayer);
cdPlayer.play();
輸出的是一個內(nèi)存地址值,SgtPeppers已經(jīng)被自動掃描發(fā)現(xiàn),并創(chuàng)建??梢钥吹轿覀兪峭ㄟ^sgtPeppers來找到類SgtPeppers的bean,這也是默認(rèn)的id——類名(首字母小寫),我們也可以通過@Component("key1") 來手動指定該bean的id為key1 。
你可能注意到CDPlayer的構(gòu)造需要一個CD類型的參數(shù),Spring會自動查找裝配的bean是否有符合該參數(shù)的類型,如果發(fā)現(xiàn)有則自動傳入,如果沒有查找到符合類型的bean則會拋出NoSuchBeanDefinitionException異常。
設(shè)置組件掃描的基礎(chǔ)包
有時候bean和config類可能并不在同級包中的話,那就需要設(shè)置掃描的基礎(chǔ)包:
@Configuration
@ComponentScan("com.hubert")
public class AutoConfig { }
如果有多個地方需要掃描也可以這樣定義:
@Configuration
@ComponentScan(basePackages = {"com.hubert", "music"})
public class AutoConfig { }
除了用String這種硬編碼的聲明,也可以傳入class對象,即將class對象所在的包作為基礎(chǔ)包。
@Configuration
@ComponentScan(basePackageClasses = ComponentPackageMaker.class)//掃描自動裝載
public class AutoConfig {
這里的ComponentPackageMaker類是一個空接口,用來標(biāo)識基礎(chǔ)包的位置。
目前我們實(shí)現(xiàn)了將bean放入Spring容器,除了bean之間構(gòu)造參數(shù)的強(qiáng)制依賴關(guān)系會自動注入bean之外,我們也可以通過@Autowired 注解在方法或?qū)傩陨蠈?shí)現(xiàn)bean的自動注入。例如這里有另一個cd播放器,它通過set方法實(shí)現(xiàn)注入:
@Component
public class OtherPlayer {
private CD cd;
@Autowired
public void setCd(CD cd) {
this.cd = cd;
}
public void play() {
cd.play();
}
}
我們可以驗(yàn)證下否正確注入:
OtherPlayer otherPlayer = (OtherPlayer) applicationContext.getBean("otherPlayer");
System.out.println(otherPlayer);
otherPlayer.play();
注意這里與構(gòu)造參數(shù)注入不同,構(gòu)造是強(qiáng)制的,就算沒有添加@Autowired 注解,也必須依賴相對應(yīng)的bean,而set方法注入如果沒有添加@Autowired 注解則不會調(diào)用該方法注入,因此不要忘記添加注解。
如果沒有匹配到bean,在創(chuàng)建Context的時候Spring會拋出異常。為了避免異常,可以修改為@Autowired(required = false) 表示不是必須的bean,當(dāng)然這樣做之后你就得考慮為null的情況了。
通過Java代碼裝配bean
這里我們還是一個@Configuration 注解的配置類:
@Configuration
public class JavaConfig {
}
接著可以在配置類中聲明bean了:
@Bean
public CD cd() {
return new SgtPeppers();
}
這種方式聲明的bean默認(rèn)id是方法名,這里就是"cd",也可以通過@Bean注解的name屬性指定id:
@Bean(name = "myCd")
public CD cd() {
return new SgtPeppers();
}
CDPlayer的構(gòu)造需要一個CD作為參數(shù),這個時候我們可以把需要依賴的bean設(shè)置為方法參數(shù),這樣在創(chuàng)建cdPlayer這個bean的時候,容器會去自動查找匹配參數(shù)的bean自動裝配。
@Bean
public CDPlayer cdPlayer(CD cd) {
return new CDPlayer(cd);
}
同樣的,我們可以通過AnnotationConfigApplicationContext加載配置類驗(yàn)證bean的裝載情況。
通過xml裝配bean
最初的時候xml是Spring配置的主要方式,雖然相比于JavaConfig顯得過于繁瑣。但在無法在代碼中添加@bean等Spirng注解的時候(如第三方庫中),使用xml也是不錯的選擇。
首先我們需要一個xml文件,并且其中以<beans>元素作為根節(jié)點(diǎn):
<?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.xsd">
</beans>
xmlns 是命名空間,表示聲明xml中使用的標(biāo)簽來源,也方便IDE提示驗(yàn)證xml文件的正確性。
聲明一個bean
我們在xml中聲明一個cd:
<bean id="cd" class="com.hubert.spring.component.SgtPeppers"/>
對于有構(gòu)造參數(shù)的bean需要這樣聲明:
<bean id="cdPlayer" class="com.hubert.spring.component.CDPlayer">
<constructor-arg ref="cd"/>
</bean>
屬性注入:
<bean id="otherPlayer" class="com.hubert.spring.component.OtherPlayer">
<property name="cd" ref="sgtPeppers"/>
</bean>
我們使用ClassPathXmlApplicationContext來加載xml配置文件裝載bean:
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("my-beans.xml");
System.out.println("----------------------------------------------");
String[] names = applicationContext.getBeanDefinitionNames();
System.out.println(Arrays.toString(names));
System.out.println("----------------------------------------------");
CD cd = (CD) applicationContext.getBean("sgtPeppers");
System.out.println(cd);
CDPlayer cdPlayer = (CDPlayer) applicationContext.getBean("player");
System.out.println(cdPlayer);
cdPlayer.play();
OtherPlayer otherPlayer = (OtherPlayer) applicationContext.getBean("otherPlayer");
System.out.println(otherPlayer);
otherPlayer.play();
Spring3之后引入了c命名空間,來簡化構(gòu)造聲明:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="sgtPeppers" class="com.hubert.spring.component.SgtPeppers"/>
<bean id="player" class="com.hubert.spring.component.CDPlayer"
c:cd-ref="sgtPeppers"/>
</beans>
啟動需要新增了一個命名空間:xmlns:c="http://www.springframework.org/schema/c"
我們通過c:cd-ref="cd"聲明CdPlayer的構(gòu)造參數(shù),其中c:是命名空間前綴,cd是構(gòu)造參數(shù)名,-ref表示引用類型,="sgtPeppers"指向id為sgtPeppers的bean。
與c命名空間類似的還有p命名空間,用于簡化屬性聲明:
<bean id="otherPlayer" class="com.hubert.spring.component.OtherPlayer"
p:cd-ref="sgtPeppers"/>
同樣需要新增一個命名空間:xmlns:p="http://www.springframework.org/schema/p"
混合使用
假設(shè)我們有兩個以上的Spring配置,其中有JavaConfig也有xml配置。我們可以使用import將所有的config歸并到一起。
在JavaConfig中可以使用@Import注解來導(dǎo)入其他配置:
@Configuration
@Import(AutoConfig.class)//導(dǎo)入其他JavaConfig
@ImportResource("my-beans.xml")//導(dǎo)入xml配置
public class JavaConfig {
在xml中同樣使用<import>標(biāo)簽導(dǎo)入其他配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:c="http://www.springframework.org/schema/c"
xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!--自動掃描-->
<context:component-scan base-package="com.hubert.spring.component"/>
<!--導(dǎo)入其他配置-->
<import resource="other-beans.xml"/>
<!--導(dǎo)入java配置-->
<bean class="com.hubert.spring.component.JavaConfig"/>
</beans>
注意這里導(dǎo)入Java配置的方式并不是用import標(biāo)簽,而是用bean表示。
不管是使用JavaConfig還是xml進(jìn)行裝配,通常都會創(chuàng)建一個根配置,根配置不裝配具體的bean,而是用于組合多個其他配置。
處理自動裝配的歧義性
前面我們講到可以通過@Autowired 注解自動注入對應(yīng)的bean,但有時候,可能注冊了多個相同類型的bean,這時候就會發(fā)生歧義,因?yàn)镾pring容器不知道應(yīng)該使用哪個bean進(jìn)行注入,例如下面這種情況:
@Autowired
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
Dessert是一個接口,并且我們有三個類實(shí)現(xiàn)了這個接口:
@Component
public class Cake implements Dessert {...}
@Component
public class Cookies implements Dessert {...}
@Component
public class IceCream implements Dessert {...}
因?yàn)檫@個三個實(shí)現(xiàn)類都使用了@Component 注解,組件掃描的時候能夠發(fā)現(xiàn)并創(chuàng)建為bean。但是在試圖自動裝配setDessert 時無法選擇唯一的值,會拋出NoUniqueBeanDefinitationException 。
Spring提供了多種可選方案來解決這樣的問題:
- 將某一個bean設(shè)置為首選(primary);
- 使用限定符(qualifier)縮小bean的范圍到只有一個bean;
Primary
我們可以使用@Primary 注解來標(biāo)記首選bean:
@Component
@Primary
public class Cake implements Dessert {...}
首選消除了歧義性,使得自動裝配能夠正確執(zhí)行。需要注意首選標(biāo)記的唯一性,如果存在有個Dessert實(shí)現(xiàn)類的bean都標(biāo)記了@Primary ,那首選也就失去了作用。
Qualifier
我們也可以使用@Qualifier來限定注入的bean,下面是直接限定了bean的id:
@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
環(huán)境與Profile
在開發(fā)中通常都會存在不同的環(huán)境使用不同的配置,如Database。Spring提供了Profile來指定bean所屬的環(huán)境,只有相應(yīng)的環(huán)境才會裝配該bean。
在Java配置中,可以使用@Profile 注解指定bean所屬的環(huán)境:
@Configuration
public class DataSourceConfig {
@Bean(destroyMethod = "shutdown")
@Profile("dev")
public DataSource embeddedDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:scheme.sql")
.addScript("classpath:test-data.sql")
.build();
}
@Bean
@Profile("prod")
public DataSource jndiDataSource() {
JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
jndiObjectFactoryBean.setJndiName("jdbc/myDS");
jndiObjectFactoryBean.setResourceRef(true);
jndiObjectFactoryBean.setProxyInterface(DataSource.class);
return (DataSource) jndiObjectFactoryBean.getObject();
}
}
@Profile 也可以與@Configuration 同時注解Config類,表示該配置類中所有bean都屬于該環(huán)境。
標(biāo)明bean所屬的環(huán)境,接下來就是激活profile。Spring首先讀取spring.profiles.active 屬性獲取指定激活profile,如果沒有指定,則使用spring.profiles.default屬性指定的默認(rèn)profile。如果spring.profiles.default屬性也沒有指定,則只裝配沒有被profile標(biāo)記的bean。
條件化的bean
假設(shè)你希望一個或多個bean只有在應(yīng)用的類路徑下包含特定的庫時才創(chuàng)建。這種依賴于某種條件的情況下才裝配bean的情形在Spring4之后得到了支持。我們可以使用@Conditional 注解設(shè)置條件,如果給定的條件滿足則會創(chuàng)建這個bean,否則不會裝配。
@Bean
@Conditional(MyCondition.class)
public CD cd() {
return new SgtPeppers();
}
@Conditional 注解需要一個Condition接口的實(shí)現(xiàn)類作為參數(shù):
package org.springframework.context.annotation;
import org.springframework.core.type.AnnotatedTypeMetadata;
public interface Condition {
boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}
實(shí)現(xiàn)Condition接口并實(shí)現(xiàn)matches方法,返回true表示滿足條件,返回false表示不滿足條件。
我們可以借助ConditionContext判斷各種情況:
- 借助
getRegistry()返回的BeanDefinitionRegistry檢查bean定義; - 借助
getBeanFactory()返回的ConfigurableListableBeanFactory檢查bean是否存在,甚至探查bean的屬性; - 借助
getEnvironment()返回的Environment檢查環(huán)境變量是否存在以及它的值是什么; - 讀取并探查
getResourceLoader()返回的ResourceLoader所加載的資源; - 借助
getClassLoader()返回的ClassLoader加載并檢查類是否存在;
AnnotatedTypeMetadata則能夠讓我們檢查帶有@Bean 注解的方法上還有什么其他的注解。
bean的作用域
Spring定義了多種作用域:
- 單例(Singleton):在整個應(yīng)用中,只創(chuàng)建bean的一個實(shí)例。
- 原型(Prototype):每次注入或者通過Spring應(yīng)用上下文獲取的實(shí)例,都會創(chuàng)建一個新的bean實(shí)例。
- 會話(Session):在Web應(yīng)用中,為每個會話創(chuàng)建一個bean實(shí)例。
- 請求(Request):在Web應(yīng)用中,為每個請求創(chuàng)建一個bean實(shí)例。
默認(rèn)情況下,Spring應(yīng)用中所有的bean都是以單例(singleton)的形式創(chuàng)建的。我們可以使用@Scope 注解改變默認(rèn)作用域:
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Notepad {...}
ConfigurableBeanFactory.SCOPE_PROTOTYPE的值是字符串"prototype" , 你也可以直接使用這個字符串,但用常量不容易出現(xiàn)拼寫錯誤。
在Web應(yīng)用中通常會使用會話和請求范圍內(nèi)共享的bean,例如購物車bean:
@Bean
@Score(value=WebApplicationContext.SCOPE_SESSION,
proxyMode=ScopedProxyMode.INTERFACES)
public ShoppingCart cart() { ... }
這里我們將ShoppingCart的聲明周期設(shè)置為session,對于同一個會話只會創(chuàng)建一個ShoppingCart實(shí)例。要注意這里還有另一個proxyMode屬性,這個屬性解決的是一個短生命周期的bean注入到長生命周期bean中的問題。
假設(shè)我們要將ShoppingCart的bean注入到單例StoreService中:
@Component
public class StoreService {
@Autowired
public void setShoppingCart(ShoppingCart shoppingCart) {
this.shoppingCart = shoppingCart;
}
}
StoreService是一個單例的bean,當(dāng)它創(chuàng)建的時候,Spring會試圖將ShoppingCart注入到setShoppingCart()方法中。但是ShoppingCart是會話作用域的,此時并不存在,直到某個用戶進(jìn)入系統(tǒng),創(chuàng)建了會話之后,才會出現(xiàn)ShoppingCart實(shí)例。另外,系統(tǒng)中將會存在多個ShoppingCart實(shí)例,我們不想讓Spring注入某個固定的ShoppingCart實(shí)例到StoreService中。我們希望的是當(dāng)StoreService處理購物車功能時,它所使用的ShoppingCart實(shí)例恰好是當(dāng)前會話所對應(yīng)的那一個。
所以Spring并不會將實(shí)際的ShoppingCart bean注入到StoreService中,Spring會注入到一個ShoppingCart bean的代理。這個代理會暴露與ShoppingCart相同的方法,所以StoreService會認(rèn)為它就是一個購物車。當(dāng)StoreService調(diào)用ShoppingCart的方法時,代理會對其進(jìn)行懶解析并將調(diào)用委托給會話作用域內(nèi)真正的ShoppingCart bean。
proxyMode屬性聲明了代理的方式,ScopedProxyMode.INTERFACES 表明這個代理要實(shí)現(xiàn)ShoppingCart接口。但如果注入的bean是一個類不是接口,Spring就沒有辦法創(chuàng)建基于接口的代理了。這時候則需要設(shè)置proxyMode屬性為ScopedProxyMode.TARGET_CLASS ,以此表明要以生成目標(biāo)類擴(kuò)展的方式創(chuàng)建代理。
運(yùn)行時注入
在Spring中處理外部值的最簡單方式就是聲明屬性源并通過Spring的Environment來檢索屬性。
我們先在resource文件夾中創(chuàng)建一個app.properties聲明屬性值,內(nèi)容是=鏈接的鍵值對。
cd.title=this is cd title
cd.author=hubert
然后在config中通過@PropertySource 注解引入app.properties。
@Configuration
@PropertySource("app.properties")
public class PropertiesConfig {
private Environment env;
@Autowired
public PropertiesConfig(Environment env) {
this.env = env;
}
@Bean
public BlackDisc disc() {
return new BlackDisc(
env.getProperty("cd.title"),
env.getProperty("cd.author"));
}
}
BlackDisc的構(gòu)造需要兩個String類型的title和author,這里通過Environment的getProperty 方法獲取我們在外部聲明的屬性。getProperty 方法還有幾個重載方法,可以傳入默認(rèn)值或者轉(zhuǎn)換目標(biāo)類型(Class<T>)。
getProperty 方法在沒有傳入默認(rèn)值的情況下,如果屬性沒有定義,則獲取到null。如果你希望該屬性是必須的,可以使用getRequiredProperty()方法。使用該方法獲取屬性,如果屬性沒有定義,則會拋出IllegalStateException異常。