本章內容:
- Spring profile
- 條件化的bean聲明
- 自動裝配與歧義性
- bean的作用域
- Spring表達式語言
Spring提供了多種技巧,借助它們可以實現更為高級的bean裝配功能。
環境與profile
開發軟件,有一個很大的問題就是將應用程序從一個環境遷移到另外一個環境。開發階段,有些環境相關做法并不適合遷移到生產環境中。比如數據庫配置、加密算法以及外部系統的集成。
比如,在開發環境中,我們可能會使用嵌入式數據庫,并預先加載測試數據。例如,在Spring配置類中,我們可能會在一個帶有@Bean注解的方法上使用EmbeddedDatabaseBuilder:
@Bean(destroyMethod="shutdown")
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.addScript("classpath:schema.sql")
.addScript("classpath:test-data.sql")
.build()
}
使用EmbeddedDatabaseBuilder會搭建一個嵌入式的Hypersonic數據庫,它的模式(schema)定義在schema.sql中,測試數據則是通過test-data.sql加載的。
當你在開發環境中運行集成測試或者啟動應用進行手動測試的時候,這個DataSource很有用。每次啟動它的時候,都能讓數據庫處于一個給定的狀態。
但是對于生產環境來說,這會是一個糟糕的選擇。在生產環境的配置中,如果會希望使用JNDI從容器中獲取一個DataSource。如下的@Bean方法會更加合適:
@Bean
public DataSource dataSource() {
JndiObjectFactoryBean jndiIbjectFactoryBean = new JndiObjectFactoryBean();
jndiIbjectFactoryBean.setJndiName("jdbc/myDS");
jndiIbjectFactoryBean.setResourceRef(true);
jndiIbjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
return {DataSource} jndiIbjectFactoryBean.getObject();
}
通過JNDI獲取Datasource能夠讓容器決定該如何創建這個DataSource,甚至包括切換為容器管理的連接池。即便如此,JNDI管理的DataSource更加適合于生產環境,對于簡單的集成和開發測試環境來說,這會帶來不必要的復雜性。
在QA環境中,你可以選擇完全不同的DataSource配置,可以配置為Commons DBCP連接池,如下所示:
@Bean(destroyMethod="close")
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setUrl("jdbc:h2:tcp://dbserver/~/test");
dataSource.setDriverClassName("org.h2.Driver");
dataSource.setUsername("sa");
dataSource.setPassword("password");
dataSource.setInitialSize(20);
dataSource.setMaxActive(30);
return dataSource;
}
三個版本的dataSource()方法互不相同。雖然它們都會生成一個類型為javax.sql.DataSource的bean,但每個方法都使用了完全不同的策略來生成DataSource bean。
我們必須要有一種方法來配置DataSource,使其在每種環境下都會選擇最為合適的配置。
其中一種方式就是在單獨的配置類(或XML文件)中配置每個bean,然后在構建階段(可能會使用Maven的profiles)確定要將哪一個配置編譯到可部署的應用中。這種方式的問題在于**要為每種環境重新構建應用。
Spring所提供的解決方案并不需要重新構建。
配置profile bean
Spring為環境相關的bean所提供的解決方案與構建時的方案沒有太大的差別。在這個過程中需要根據環境決定該創建哪個bean和不創建哪個bean。Spring并不是在構建的時候做出這樣的決策,而是等到運行時再來確定。這樣同一個部署單元能夠適用于所有的環境,沒有必要進行重新構建。
Spring引入了bean profile
的功能。要使用profile,首先要將所有不同的bean
定義整理到一個或多個profile
之中,在將應用部署到每個環境的時候,要確保對應的profile
處于激活(active
)的狀態。
Java配置profile
在Java配置中,可以使用@Profile
注解指定某個bean
屬于哪一個
profile
。
package com.myapp;
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
@Configuration
@Profile("dev")
public class DevelopmentProfileConfig {
@Bean(destroyMethod = "shutdown")
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:schema.sql")
.addScript("classpath:test-data.sql")
.build();
}
}
上面的實例的@Profile
注解應用在了類級別上。它告訴Spring這個類中的bean只有在dev profile激活時才會創建。如果
dev profile
沒有激活的話,那么對應的帶有@Bean
注解的方法都會被忽略掉。
同時可能還需要配置一個生產環境的配置。
package com.myapp;
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.jndi.JndiObjectFactoryBean;
@Profile("prod")
@Configuration
public class ProductionProfileConfig {
@Bean
public DataSource dataSource() {
JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
jndiObjectFactoryBean.setJndiName("jdbc/myDS");
jndiObjectFactoryBean.setResourceRef(true);
jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
return (DataSource) jndiObjectFactoryBean.getObject();
}
}
從Spring 3.2開始,可以在方法級別上使用@Profile注解。這樣便可以將這兩個bean的聲明放到同一個配置類中。
package com.myapp;
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.jndi.JndiObjectFactoryBean;
@Configuration
public class DataSourceConfig {
@Bean(destroyMethod = "shutdown")
@Profile("dev")
public DataSource embeddedDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:schema.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(javax.sql.DataSource.class);
return (DataSource) jndiObjectFactoryBean.getObject();
}
}
在XML中配置profile
也可以通過<beans>元素的profile屬性,在XML中配置profile bean。例如,為了在XML中定義適用于開發階段的嵌入式數據庫DataSourcebean,可以創建如下所示的XML文件:
<?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:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="
http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd"
profile="dev">
<jdbc:embedded-database id="dataSource"
<jdbc:script location="classpath:schema.sql" />
<jdbc:script location="classpath:test-data.sql" />
</jdbc:embedded-database>
</beans>
類似地,也可以將profile屬性設置為prod,創建適用于生產環境的從JNDI獲取的DataSource bean。同樣可以創建基于連接池定義的DataSource bean,將其放在另外一個XML文件中,并標注為qa profile。所有的配置文件都會放到部署單元之中
,但是只有profile屬性與當前激活profile相匹配的配置文件才會被用到。
還可以在根<beans>元素中嵌套定義<beans>元素,而不需要為每個環境都創建一個profile XML文件。這能夠將所有的profile bean定義放到同一個XML文件中,如下所示:
<?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:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jee="http://www.springframework.org/schema/jee" xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="
http://www.springframework.org/schema/jee
http://www.springframework.org/schema/jee/spring-jee.xsd
http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<beans profile="dev">
<jdbc:embedded-database id="dataSource" type="H2">
<jdbc:script location="classpath:schema.sql" />
<jdbc:script location="classpath:test-data.sql" />
</jdbc:embedded-database>
</beans>
<beans profile="qa">
<bean id="dataSource"
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"
p:url="jdbc:h2:tcp://dbserver/~/test"
p:driverClassName="org.h2.Driver"
p:username="sa"
p:password="password"
p:initialSize="20"
p:maxAcitve="30" />
</beans>
<beans profile="prod">
<jee:jndi-lookup id="dataSource"
lazy-init="true"
jndi-name="jdbc/myDatabase"
resource-ref="true"
proxy-interface="javax.sql.DataSource" />
</beans>
</beans>
這種配置方式與定義在單獨的XML文件中的實際效果是一樣的。這三個bean,類型都是javax.sql.DataSource,并且ID都是dataSource。但是在運行時,只會創建一個bean,這取決于處于激活狀態的是哪個profile。
激活profile
Spring在確定哪個profile處于激活狀態時,需要依賴兩個獨立的屬性:spring.profiles.active
和spring.profiles.default
。如果設置了spring.profiles.active
屬性,那么它的值就會用來確定哪個profile
是激活的。如果沒有設置spring.profiles.active
屬性,Spring會查找spring.profiles.default
的值。如果spring.profiles.active
和spring.profiles.default
均沒有設置。代表沒有激活profile,只會創建那些沒有定義在profile中的bean。
有多種方式來設置這兩個屬性:
- 作為DispatcherServlet的初始化參數;
- 作為Web應用的上下文參數;
- 作為JNDI條目;
- 作為環境變量;
- 作為JVM的系統屬性;
- 在集成測試類上,使用@ActiveProfiles注解設置
使用DispatcherServlet的初始化參數
使用DispatcherServlet
的參數將spring.profiles.default
設置為開發環境的profile
,會在Servlet上下文中進行設置(為了兼顧到ContextLoaderListener
)。在Web應用中,設置spring.profiles.default
的web.xml文件會如下所示:
按照這種方式設置spring.profiles.default,所有的開發人員都能從版本控制軟件中獲得應用程序源碼,并使用開發環境的設置運行代碼,不需要任何額外的配置。
當應用程序部署到QA、生產或其他環境之中時,根據情況使用系統屬性、環境變量或JNDI設spring.profiles.active即可。
在spring.profiles.active和spring.profiles.default中,profile使用的都是復數形式。
意味著可以同時激活多個profile,這可以通過列出多個profile名稱,并以逗號分隔來實現。
使用profile進行測試
運行集成測試時,通常會希望采用與生產環境相同的配置進行測試。如果配置中的bean定義在了profile中,在運行測試時,就需要有一種方式來啟用合適的profile。
Spring提供了@ActiveProfiles注解,我們可以使用它來指定運行測試時要激活哪個profile:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=DataSourceConfig.class)
@ActiveProfiles("dev")
public static class DevDataSourceTest {
...
}
profile機制中的條件要基于哪個profile處于激活狀態來判斷。Spring 4.0中提供了一種更為通用的機制來實現條件化的bean定義,這種機制中的條件完全由你來確定。
條件化的bean
暫時跳過
處理自動裝配的歧義性
自動裝配能夠提供很大的幫助,因為它會減少裝配應用程序組件時所需要的顯式配置的數量。
但僅有一個bean匹配所需的結果時,自動裝配才是有效的。如果不僅有一個bean能夠匹配結果的話,這種歧義性會阻礙Spring自動裝配屬性、構造器參數或方法參數。
例如:
@Autowired
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
Dessert是一個接口,并且有三個類實現了這個接口:
@Component
public class Cake implements Dessert { ... }
@Component
public class Cookies implements Dessert { ... }
@Component
public class Iceream implements Dessert { ... }
三個實現均使用了@Component注解,在組件掃描的時候,能夠發現它們并將其創建為Spring應用上下文里面的bean。當Spring試圖自動裝配setDessert()中的Dessert參數時,參數并沒有唯一、無歧義的可選值。Spring卻無法做出選擇。Spring會拋出NoUniqueBeanDefinitionException。
自動裝配歧義性的問題其實比你想象中的更為罕見。當確實發生歧義性的時候,Spring提供了多種可選方案來解決這樣的問題。你可以將可選bean中的某一個設為首選(primary)的bean,或者使用限定符(qualifier)來幫助Spring將可選的bean的范圍縮小到只有一個bean。
標示首選的bean
聲明bean的時候,通過將其中一個可選的bean設置為首選(primary)bean能夠避免自動裝配時的歧義性。當遇到歧義性的時候,Spring會使用首選的bean。
@Primary能夠與@Component組合用在組件掃描的bean上,也可以與@Bean組合用在Java配置的bean聲明中。
@Component
@Primary
public class Iceream implements Dessert { ... }
@Bean
@Primary
public Dessert iceCream() {
return new IceCream();
}
如果使用XML配置bean,同樣可以實現這樣的功能,通過使用<bean>元素中的primary屬性指定首選的bean:
<bean id="iceCream"
class="com.desserteater.IceCream"
primary="true" />
就解決歧義性問題而言,相較于使用首選標示,限定符是一種更為強大的機制。
限定自動裝配的bean
設置首選bean的局限性在于@Primary無法將可選方案的范圍限定到唯一一個無歧義性的選項中。它只能標示一個優先的可選方案。當首選bean多于一個,并沒有其他的方法縮小可選范圍。
相反的,Spring的限定符能夠在所有可選的bean上進行縮小范圍的操作,最終達到只有一個bean滿足所規定的限制條件。如果將所有的限定符都用上后依然存在歧義性,可以繼續使用更多的限定符來縮小選擇范圍。
@Qualifier注解是使用限定符的主要方式??梢耘c@Autowired和@Inject協同使用,在注入的時候指定想要注入進去的是哪個bean:
@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
為@Qualifier注解所設置的參數就是想要注入的bean的ID。
所有使用@Component注解聲明的類都會創建為bean,并且bean的ID為首字母變為小寫的類名。因此,@Qualifier("iceCream")指向的是組件掃描時所創建的IceCream類的實例bean。
@Qualifier("iceCream")所引用的bean要具有String類型的“iceCream”作為限定符。
基于默認的bean ID作為限定符時,限定符與要注入的bean的名稱是緊耦合的,有可能會引入一些問題。對類名稱的任意改動都會導致限定符失效,無法匹配限定符。導致自動裝配失敗。
創建自定義的限定符
我們可以為bean設置自己的限定符,而不是依賴于將bean ID作為限定符。所需要做的就是在bean聲明(@Component注解的類)上添加@Qualifier注解:
@Component
@Qualifier("cold")
public class IceCream implements Dessert { ... }
cold限定符分配給了IceCreambean。因為它沒有耦合類名,因此可以隨意重構IceCream的類名,而不必擔心會破壞自動裝配。
在注入的地方,只要引用cold限定符就可以了:
@Autowired
@Qualifier("cold")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
當通過Java配置顯式定義bean時,@Qualifier也可以與@Bean注解一起使用:
@Bean
@Qualifier("cold")
public Dessert iceCream() {
return new IceCream();
}
使用自定義的限定符注解
面向特性的限定符要比基于bean ID的限定符更好一些。但是,如果多個bean都具備相同特性的話,這種做法也會出現問題。
再引入一個新的Dessert bean:
@Component
@Qualifier("cold")
public class Popsicle implements Dessert { ... }
這樣,在自動裝配Dessert bean的時候,再次遇到了歧義性的問題,需要使用更多的限定符來將可選范圍限定到只有一個bean。
可能的解決方案是在注入點和bean定義的地方同時再添加另外一個@Qualifier注解:
@Component
@Qualifier("cold")
@Qualifier("creamy")
public class IceCream implements Dessert() { ... }
@Component
@Qualifier("cold")
@Qualifier("fruity")
public class Popsicle implements Dessert { ... }
在注入點:
@Autowired
@Qualifier("cold")
@Qualifier("creamy")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
但Java不允許在同一個條目上重復出現相同類型的多個注解。如果試圖這樣做,編譯器會提示錯誤。
但是可以創建自定義的限定符注解,借助這樣的注解來表達bean所希望限定的特性。這里所需要做的就是創建一個注解,它本身使用@Qualifier注解來標注。這樣可以不再使用@Qualifier("cold"),而是使用自定義的@Cold注解:
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold { }
同樣,你可以創建一個新的@Creamy注解來代替@Qualifier("creamy"):
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Creamy { }
通過在定義時添加@Qualifier注解,它們就具有了@Qualifier注解的特性。它們本身實際上就成為了限定符注解。
重新為IceCream添加@Cold和@Creamy注解:
@Component
@Cold
@Creamy
public class IceCream implements Dessert() { ... }
為Popsicle類添加@Cold和@Fruity注解:
@Component
@Cold
@Fruity
public class Popsicle implements Dessert() { ... }
最終,在注入點,我們使用必要的限定符注解進行任意組合,從而將可選范圍縮小到只有一個bean滿足需求:
@Autowired
@Cold
@Creamy
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
通過聲明自定義的限定符注解,我們可以同時使用多個限定符,不會再有Java編譯器的限制或錯誤。同時,相對于使用原始的@Qualifier并借助String類型來指定限定符,自定義的注解也更為類型安全。
沒有在任何地方明確指定要將IceCream自動裝配到setDessert()方法中。因此,setDessert()方法依然能夠與特定的Dessert實現保持解耦。任意滿足這些特征的bean都是可以的。
bean的作用域
默認情況下,Spring應用上下文中所有bean都是作為以單例(singleton)的形式創建的。也就是不管給定的一個bean被注入到其他bean多少次,每次所注入的都是同一個實例。
大多數情況下,單例bean是很理想的方案。初始化和垃圾回收對象實例所帶來的成本只留給一些小規模任務,在這些任務中,讓對象保持無狀態并且在應用中反復重用這些對象可能并不合理。
有時候,所使用的類是易變的(mutable),它們會保持一些狀態,因此重用是不安全的。在這種情況下,不應該將class聲明為單例的bean,因為對象會被污染,重用的時候會出現意想不到的問題。
Spring定義了多種作用域,可以基于這些作用域創建bean:
- 單例(Singleton):在整個應用中,只創建bean的一個實例。
- 原型(Prototype):每次注入或者通過Spring應用上下文獲取的時候,都會創建一個新的bean實例。
- 會話(Session):在Web應用中,為每個會話創建一個bean實例。
- 請求(Rquest):在Web應用中,為每個請求創建一個bean實例。
單例是默認的作用域,但是對于易變的類型,并不合適。選擇其他的作用域,要使用@Scope注解,它可以與@Component或@Bean一起使用。
如果你使用組件掃描來發現和聲明bean,那么可以在bean的類上使用@Scope注解,將其聲明為原型bean:
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class NotePad { ... }
這里,使用ConfigurableBeanFactory類的SCOPE_PROTOTYPE常量設置了原型作用域。也可以使用@Scope("prototype"),但是使用SCOPE_PROTOTYPE常量更加安全并且不易出錯。
如果想在Java配置中將Notepad聲明為原型bean,可以組合使用@Scope和@Bean來指定所需的作用域:
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTYPE)
public Notepad notepad() {
return new Notepad();
}
同樣,如果你使用XML來配置bean的話,可以使用<bean>元素的scope屬性來設置作用域:
<bean id="notepad"
class="com.myapp.Notepad"
scope="prototype" />
使用會話和請求作用域
在Web應用中,實例化在會話和請求范圍內共享的bean,是非常有價值的事情。例如,在典型的電子商務應用中,可能會有一個bean代表用戶的購物車。如果購物車是單例的話,將會導致所有的用戶都會向同一個購物車中添加商品。如果購物車是原型作用域的,那么在應用中某一個地方往購物車中添加商品,,在應用的另外一個地方可能就不可用了,因在這里注入的是另外一個原型作用域的購物車。
對購物車bean來說,會話作用域是最為合適的,因為它與給定的用戶關聯性最大。指定會話作用域,它的使用方式與指定原型作用域是相同的:
@Bean
@Scope(
value=WebApplicationContext.SCOPE_SESSION,
proxyMode=ScopedProxyMode.INTERFACES)
public ShoppingCart cart() { ... }
這里,將value設置成了WebApplicationContext中的SCOPE_SESSION常量(它的值是session)。這會告訴Spring為Web應用中的每個會話創建一個ShoppingCart。這會創建多個ShoppingCart bean的實例,但是對于給定的會話只會創建一個實例,在當前會話相關的操作中,這個bean實際上相當于單例的。
注意上面的@Scope同時還有一個proxyMode屬性,它被設置成了ScopedProxyMode.INTERFACES。這個屬性解決了將會話或請求作用域的bean注入到單例bean中所遇到的問題。
假設我們要將ShoppingCart bean注入到單例StoreService bean的Setter方法中:
@Component
public class StoreService {
@Autowired
public void setShoppingCart(ShoppingCart shoppingCart) {
this.shoppingCart = shoppingCart;
}
...
}
因為StoreService是一個單例的bean,會在Spring應用上下文加載的時候創建。當它創建的時候,Spring會試圖將ShoppingCart bean注入到setShoppingCart()方法中。但是ShoppingCart bean是會話作用域的,此時并不存在。直到某個用戶進入系統,創建了會話之后,才會出現ShoppingCart實例。
此外,系統中將會有多個ShoppingCart實例:每個用戶一個。我們并不想讓Spring注入某個固定的ShoppingCart實例到StoreService中。我們希望的是當StoreService處理購物車功能時,它所使用的ShoppingCart實例恰好是當前會話所對應的那一個。
Spring不會將實際的ShoppingCart bean注入到StoreService中,Spring會注入一個到ShoppingCart bean的代理,如下圖所示:
這個代理會暴露于ShoppingCart相同的方法,所以StoreService會認為它就是一個購物車。當StoreService調用ShoppingCart的方法時,代理會對其進行懶解析并將調用委托給會話作用域內真正的ShoppingCart bean。
現在討論一下proxyMode屬性。如配置所示,proxyMode屬性被設置成了ScopedProxyMode.INTERFACES,這表明這個代理要實現ShoppingCart接口,并將調用委托給實現bean。
如果ShoppingCart是接口而不是類的話,這時最為理想的代理模式。但如果ShoppingCart是一個具體的類的話,Spring就沒有辦法創建基于接口的代理。此時,它必須使用CGLib來生成基于類的代理。所以,如果bean類型是具體類的話,我們必須要將proxyMode屬性設置為ScopedProxyMode.TARGET_CLASS
,以此來表明要以生成目標類擴展的方式創建代理。
同樣的,請求作用域的bean也會面臨相同的裝配問題。因此,請求作用域的bean應該也以作用域代理的方式進行注入。
在XML中聲明作用域代理
如果你需要使用XML來聲明會話或請求作用域的bean,就不能使用@Scope注解及其proxyMode屬性了。<bean>元素的scope屬性能夠設置bean的作用域,要設置代理模式,我們需要使用Spring aop命名空間的一個新元素:
<bean id="cart"
class="com.myapp.ShoppingCart"
scope="session">
<aop:scope-proxy />
</bean>
<aop:scoped-proxy>
是與@Scope注解的proxyMode屬性功能相同的Spring XML配置元素。它會告訴Spring為bean創建一個作用域代理。默認情況下,會使用CGLib創建目標類的代理。但是也可以將proxy-target-class屬性設置為false,進而要求它生成基于接口的代理:
<bean id="cart"
class="com.myapp.ShoppingCart"
scope="session">
<aop:scope-proxy proxy-target-class="false" />
</bean>
為了使用<aop:scoped-proxy>元素,必須在XML配置中聲明Spring的aop命名空間:
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
...
</beans>
運行時注入
討論依賴注入的時候,通常討論的是將一個bean引用注入到另一個bean的屬性或構造器參數中。通常來講指的是將一個對象與另一個對象進行關聯。
bean裝配的另外一個方面指的是將一個值注入到bean的屬性或者構造器參數中。如果按照這樣的方式來組裝BlankDisc:
盡管這實現了需求,為BlankDisc bean設置title和artist,但它在實現的時候是將值硬編碼在配置類中的。
有時想讓這些值在運行時再確定。Spring提供了兩種在運行時求值的方式:
- 屬性占位符(Property placeholder)。
- Spring表達式語言(SpEL)。
注入外部的值
Spring中,處理外部值的最簡單方式就是聲明屬性源并通過Spring的Environment來檢索屬性。下面的程序展示了基本的Spring配置類,它使用外部的屬性來裝配BlankDisc bean
。
package com.soundsystem;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
@Configuration
@PropertySource("classpath:/com/soundsystem/app.properties")
public class EnvironmentConfig {
@Autowired
Environment env;
@Bean
public BlankDisc blankDisc() {
return new BlankDisc(
env.getProperty("disc.title"),
env.getProperty("disc.artist"));
}
}
@PropertySource
引用了類路徑中一個名為app.properties
的文件。這個屬性文件會加載到Spring的Environment中,稍后可以從這里檢索屬性。同時,在disc()
方法中,會創建一個新的BlankDisc,它的構造器參數是從屬性文件中獲取的,這是通過調用getProperty()實現的。
深入學習Spring的Environment
getProperty()
并非只有上面程序獲取屬性值的一種方法,getProperty()
方法有四個重載的變種形式:
- String getProperty(String key)
- String getProperty(String key, String defaultValue)
- T getProperty(String key, Class<T> type)
- T getProperty(String key, Class<T> rype, T defaultValue)
前兩種形式的getProperty()方法都會返回String類型的值。你可以稍微對@Bean方法進行一下修改,這樣在指定屬性不存在的時候,會使用一個默認值:
@Bean
public BlankDisc blankDisc() {
return new BlankDisc(
env.getProperty("disc.title", "Rattle and Hum"),
env.getProperty("disc.artist", "U2"));
}
剩下的兩種getProperty()
方法與前面的兩種非常類似,但是它們不會將所有的值都視為String類型。例如,你想要獲取的值所代表的含義是連接池中所維持的連接數量。如果使用重載形式的getProperty()的話,就能非常便利地解決這個問題:
int connectionCount = env.getProperty("db.connection.count", Integer.class, 30);
Environment還提供了幾個與屬性相關的方法,如果在使用getProperty()
方法的時候沒有指定默認值,并且這個屬性沒有定義的話,獲取到的值是null
。如果希望這個屬性必須要定義,可以使用getRequiredProperty()
方法。
@Bean
public BlankDisc blankDisc() {
return new BlankDisc(
env.getRequiredProperty("disc.title"),
env.getRequiredProperty("disc.artist"));
}
如果disc.title或disc.artist屬性沒有定義的話,將會拋出IllegalStateException
異常。
想檢查一下某個屬性是否存在,可以調用Environment的containsProperty()
方法:
boolean titleExists = env.containsProperty("disc.title");
如果想將屬性解析為類的話,可以使用getPropertyAsClass()
方法:
Class<CompactDisc> cdClass =
env.getPropertyAsClass("disc.class",CompactDisc.class);
除了屬性相關功能以外,Environment
還提供了一些方法來檢查哪些profile處于激活狀態:
-
String[] getActiveProfiles()
:返回激活profile名稱的
數組; -
String[] getDefaultProfiles()
:返回默認profile名稱的
數組; -
boolean acceptsProfiles
(String... profiles):如
果environment支持給定profile的話,就返回true。
除了直接從Environment中檢索屬性外,Spring也提供了通過占位符裝配屬性的方法,這些占位符的值會來源于一個屬性源。
解析屬性占位符
Spring支持將屬性定義到外部的屬性的文件中,并使用占位符值將其插入到Spring bean中。在Spring裝配中,占位符的形式為使用“${... }”包裝的屬性名稱。比如可以在XML中按照如下的方式解析BlankDisc構造器參數:
<bean id="sgtPeppers"
class="com.soundsystem.BlankDisc"
c:_title = "${disc.title}"
c:_artist = "${disc.artist}"/>
可以看到,title
構造器參數所給定的值是從一個屬性中解析得到的,這個屬性的名稱為disc.title。artist
參數裝配的是名為disc.artist
的屬性值。按照這種方式,XML配置沒有使用任何硬編碼的值,它的值是從配置文件以外的一個源中解析得到的。
如果我們依賴于組件掃描和自動裝配來創建和初始化應用組件的話,那么就沒有指定占位符的配置文件或類了。在這種情況下,可以使用@Value
注解,它的使用方式與@Autowired
注解非常相似。比如,在BlankDisc
類中,構造器可以改成如下所示:
public BlankDisc(
@Value("${disc.title}") String title,
@Value("${disc.artist}") String artist) {
this.title = title;
this.artist = artist;
}
為了使用占位符,我們必須要配置一個PropertyPlaceholderConfigurer
bean或PropertySourcesPlaceholderConfigurer
bean。從Spring 3.1開始,推薦使用PropertySourcesPlaceholderConfigurer
,因為它能夠基于Spring Environment
及其屬性源來解析占位符。如下的@Bean
方法在Java中配置了PropertySourcesPlaceholderConfigurer
:
@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer {
return new PropertySourcesPlaceholderConfigurer();
}
如果想使用XML配置的話,Spring context命名空間中的
<context:propertyplaceholder>
元素將會為你生
成PropertySourcesPlaceholderConfigurer
bean:
<?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: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-4.0.xsd">
<context:property-placeholder />
</beans>
解析外部屬性能夠將值的處理推遲到運行時,但是它的關注點在于根據名稱解析來自于Spring Environment
和屬性源的屬性。而Spring表達式語言提供了一種更通用的方式在運行時計算所要注入的值。
使用Spring表達式語言進行裝配
Spring 3引入了Spring表達式語言(Spring Expression Language,SpEL),它能夠以一種強大簡潔的方式將值裝配到bean屬性和構造器參數中,在這個過程中所使用的表達式會在運行時計算得到值。
SpEL擁有很多特性,包括:
- 使用bean的ID來引用bean;
- 調用方法和訪問對象的屬性;
- 對值進行算術、關系和邏輯運算;
- 正則表達式匹配;
- 集合操作。
SpEL樣例
SpEL表達式要放到#{ ... }
之中,這與屬性占位符有些類似,屬性占位符需要放到${ ... }
之中。
#{1}
上面的例子除去#{ ... }
標記之后,剩下的就是SpEL表達式體了,也就是一
個數字常量。這個表達式的計算結果就是數字1。
在實際的應用程序中,我們可能會使用更加有意思的表達式,如:
#{T(System).currentTimeMillis()}
它的最終結果是計算表達式的那一刻當前時間的毫秒數。T()表達式會將java.lang.System
視為Java中對應的類型,因此可以調用其static
修飾的currentTimeMillis()
方法。
SpEL表達式也可以引用其他的bean或其他bean的屬性。例如,如下的表達式會計算得到ID為sgtPeppers的bean的artist屬性:
#{sgtPeppers.artist}
還可以通過systemProperties對象引用系統屬性:
#{systemProperties['disc.title']}
接下來看一下在bean裝配的時候如何使用這些表達式。
如果通過組件掃描創建bean的話,在注入屬性和構造器參數時,可以使用@Value
注解,下面的樣例展現了BlankDisc,它會從系統屬性中獲取專輯名稱和藝術家的名字:
public BlankDisc(
@Value("#{systemProperties['disc.title']}") String title,
@Value("#{systemProperties['disc.artist']}") String artist) {
this.title = title;
this.artist = artist;
}
那現在就來學習一下SpEL所支持的基礎表達式。
表示字面值
浮點值
#{3.14159}
科學計數法
#{9.87E4}
String類型字面值
#{'Hello'}
Boolean類型值(true | false)
#{true}
引用bean、屬性和方法
使用SpEL可以將一個bean裝配到另外一個bean的屬性中
#{sgtPeppers}
在一個表達式中引用sgtPeppers的artist屬性
#{sgtPeppers.artist}
還可以調用bean上的方法。
#{artistSelector.selectArtist()}
對被調用的返回值同樣可以調用它的方法
#{artistSelector.selectArtist().toUpperCase()}
上面沒有考慮到返回值為null的情況,可以使用類型安全的運算符
#{artistSelector.selectArtist()?.toUpperCase()}
?.
這個運算符能夠在訪問它右邊的內容之前,確保它所對應的元素不是null
。如果selectArtist()
的返回值是null
的話,那么SpEL將不會調用toUpperCase()
方法。表達式的返回值會是null
。
在表達式中使用類型
如果要在SpEL中訪問類作用域的方法和常量的話,要依賴T()
這個運算符。例如,為了在SpEL中表達Java的Math類,需要按照如下的方式使用T()運算符:
T(java.lang.Math)
這里T()
運算符的結果會是一個Class
對象,代表了java.lang.Math
。如果需要的話,甚至可以將其裝配到一個Class類型的bean屬性中。但是T()
運算符的真正價值在于它能夠訪問目標類型的靜態方法和常量。
例如需要將PI值裝配到bean屬性中
T(java.lang.Math).PI
類似地,我們可以調用T()
運算符所得到類型的靜態方法。如下樣例會計算得到一個0到1之間的隨機數:
T(java.lang.Math).random()
SpEl運算符
SpEL提供了多個運算符,這些運算符可以用在SpEL表達式的值上。
運算符類型 | 運算符 |
---|---|
算數運算 | +、-、*、/、%、^ |
比較運算 | <、>、==、>=、<=、lt、gt、eq、le、ge |
邏輯運算 | and、or、not、| |
條件運算 | ?:(ternart)、?:(Elvis) |
正則表達式 | matches |
作為使用上述運算符的一個簡單樣例,看一下下面這個SpEL表達式:
#{2 * T(java.lang.Math).PI * circle.radius}
SpEL還提供了查詢運算符(.?[]
),它會用來對集合進行過濾,得到集合的一個子集。作為闡述的樣例,假設你希望得到jukebox
中artist
屬性為Aerosmith
的所有歌曲。如下的表達式就使用查詢運算符得到了Aerosmith
的所有歌曲:
#{jukebox.songs.?[artist eq 'Aerosmith']}
SpEL還提供了另外兩個查詢運算符:.^[]
和.$[]
,它們分別用來在集合中查詢第一個匹配項和最后一個匹配項。例如,考慮下面的表達式,它會查找列表中第一個artist屬性為Aerosmith的歌曲:
#{jukebox.songs.^[artist eq 'Aerosmith']}
SpEL還提供了投影運算符(.![]
),它會從集合的每個成員中選擇特定的屬性放到另外一個集合中。作為樣例,假設我們不想要歌曲對象的集合,而是所有歌曲名稱的集合。如下的表達式會將title屬性投影到一個新的String類型的集合中:
#{jukebox.songs.![title]}
投影操作也可以與其他任意的SpEL運算符一起使用。比如,我們可以使用如下的表達式獲得Aerosmith所有歌曲的名稱列表:
#{jukebox.songs.^[artist eq 'Aerosmith'].![title]}