本章內(nèi)容:
- Spring profile
- 條件化的bean聲明
- 自動(dòng)裝配與歧義性
- bean的作用域
- Spring表達(dá)式語(yǔ)言
Spring提供了多種技巧,借助它們可以實(shí)現(xiàn)更為高級(jí)的bean裝配功能。
環(huán)境與profile
開(kāi)發(fā)軟件,有一個(gè)很大的問(wèn)題就是將應(yīng)用程序從一個(gè)環(huán)境遷移到另外一個(gè)環(huán)境。開(kāi)發(fā)階段,有些環(huán)境相關(guān)做法并不適合遷移到生產(chǎn)環(huán)境中。比如數(shù)據(jù)庫(kù)配置、加密算法以及外部系統(tǒng)的集成。
比如,在開(kāi)發(fā)環(huán)境中,我們可能會(huì)使用嵌入式數(shù)據(jù)庫(kù),并預(yù)先加載測(cè)試數(shù)據(jù)。例如,在Spring配置類(lèi)中,我們可能會(huì)在一個(gè)帶有@Bean注解的方法上使用EmbeddedDatabaseBuilder:
@Bean(destroyMethod="shutdown")
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.addScript("classpath:schema.sql")
.addScript("classpath:test-data.sql")
.build()
}
使用EmbeddedDatabaseBuilder會(huì)搭建一個(gè)嵌入式的Hypersonic數(shù)據(jù)庫(kù),它的模式(schema)定義在schema.sql中,測(cè)試數(shù)據(jù)則是通過(guò)test-data.sql加載的。
當(dāng)你在開(kāi)發(fā)環(huán)境中運(yùn)行集成測(cè)試或者啟動(dòng)應(yīng)用進(jìn)行手動(dòng)測(cè)試的時(shí)候,這個(gè)DataSource很有用。每次啟動(dòng)它的時(shí)候,都能讓數(shù)據(jù)庫(kù)處于一個(gè)給定的狀態(tài)。
但是對(duì)于生產(chǎn)環(huán)境來(lái)說(shuō),這會(huì)是一個(gè)糟糕的選擇。在生產(chǎn)環(huán)境的配置中,如果會(huì)希望使用JNDI從容器中獲取一個(gè)DataSource。如下的@Bean方法會(huì)更加合適:
@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();
}
通過(guò)JNDI獲取Datasource能夠讓容器決定該如何創(chuàng)建這個(gè)DataSource,甚至包括切換為容器管理的連接池。即便如此,JNDI管理的DataSource更加適合于生產(chǎn)環(huán)境,對(duì)于簡(jiǎn)單的集成和開(kāi)發(fā)測(cè)試環(huán)境來(lái)說(shuō),這會(huì)帶來(lái)不必要的復(fù)雜性。
在QA環(huán)境中,你可以選擇完全不同的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;
}
三個(gè)版本的dataSource()方法互不相同。雖然它們都會(huì)生成一個(gè)類(lèi)型為javax.sql.DataSource的bean,但每個(gè)方法都使用了完全不同的策略來(lái)生成DataSource bean。
我們必須要有一種方法來(lái)配置DataSource,使其在每種環(huán)境下都會(huì)選擇最為合適的配置。
其中一種方式就是在單獨(dú)的配置類(lèi)(或XML文件)中配置每個(gè)bean,然后在構(gòu)建階段(可能會(huì)使用Maven的profiles)確定要將哪一個(gè)配置編譯到可部署的應(yīng)用中。這種方式的問(wèn)題在于**要為每種環(huán)境重新構(gòu)建應(yīng)用。
Spring所提供的解決方案并不需要重新構(gòu)建。
配置profile bean
Spring為環(huán)境相關(guān)的bean所提供的解決方案與構(gòu)建時(shí)的方案沒(méi)有太大的差別。在這個(gè)過(guò)程中需要根據(jù)環(huán)境決定該創(chuàng)建哪個(gè)bean和不創(chuàng)建哪個(gè)bean。Spring并不是在構(gòu)建的時(shí)候做出這樣的決策,而是等到運(yùn)行時(shí)再來(lái)確定。這樣同一個(gè)部署單元能夠適用于所有的環(huán)境,沒(méi)有必要進(jìn)行重新構(gòu)建。
Spring引入了bean profile的功能。要使用profile,首先要將所有不同的bean定義整理到一個(gè)或多個(gè)profile之中,在將應(yīng)用部署到每個(gè)環(huán)境的時(shí)候,要確保對(duì)應(yīng)的profile處于激活(active)的狀態(tài)。
Java配置profile
在Java配置中,可以使用@Profile注解指定某個(gè)bean屬于哪一個(gè)
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();
}
}
上面的實(shí)例的@Profile注解應(yīng)用在了類(lèi)級(jí)別上。它告訴Spring這個(gè)類(lèi)中的bean只有在dev profile激活時(shí)才會(huì)創(chuàng)建。如果
dev profile沒(méi)有激活的話(huà),那么對(duì)應(yīng)的帶有@Bean注解的方法都會(huì)被忽略掉。
同時(shí)可能還需要配置一個(gè)生產(chǎn)環(huán)境的配置。
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開(kāi)始,可以在方法級(jí)別上使用@Profile注解。這樣便可以將這兩個(gè)bean的聲明放到同一個(gè)配置類(lèi)中。
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
也可以通過(guò)<beans>元素的profile屬性,在XML中配置profile bean。例如,為了在XML中定義適用于開(kāi)發(fā)階段的嵌入式數(shù)據(jù)庫(kù)DataSourcebean,可以創(chuàng)建如下所示的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>
類(lèi)似地,也可以將profile屬性設(shè)置為prod,創(chuàng)建適用于生產(chǎn)環(huán)境的從JNDI獲取的DataSource bean。同樣可以創(chuàng)建基于連接池定義的DataSource bean,將其放在另外一個(gè)XML文件中,并標(biāo)注為qa profile。所有的配置文件都會(huì)放到部署單元之中
,但是只有profile屬性與當(dāng)前激活profile相匹配的配置文件才會(huì)被用到。
還可以在根<beans>元素中嵌套定義<beans>元素,而不需要為每個(gè)環(huán)境都創(chuàng)建一個(gè)profile XML文件。這能夠?qū)⑺械膒rofile bean定義放到同一個(gè)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>
這種配置方式與定義在單獨(dú)的XML文件中的實(shí)際效果是一樣的。這三個(gè)bean,類(lèi)型都是javax.sql.DataSource,并且ID都是dataSource。但是在運(yùn)行時(shí),只會(huì)創(chuàng)建一個(gè)bean,這取決于處于激活狀態(tài)的是哪個(gè)profile。
激活profile
Spring在確定哪個(gè)profile處于激活狀態(tài)時(shí),需要依賴(lài)兩個(gè)獨(dú)立的屬性:spring.profiles.active和spring.profiles.default。如果設(shè)置了spring.profiles.active屬性,那么它的值就會(huì)用來(lái)確定哪個(gè)profile是激活的。如果沒(méi)有設(shè)置spring.profiles.active屬性,Spring會(huì)查找spring.profiles.default的值。如果spring.profiles.active和spring.profiles.default均沒(méi)有設(shè)置。代表沒(méi)有激活profile,只會(huì)創(chuàng)建那些沒(méi)有定義在profile中的bean。
有多種方式來(lái)設(shè)置這兩個(gè)屬性:
- 作為DispatcherServlet的初始化參數(shù);
- 作為Web應(yīng)用的上下文參數(shù);
- 作為JNDI條目;
- 作為環(huán)境變量;
- 作為JVM的系統(tǒng)屬性;
- 在集成測(cè)試類(lèi)上,使用@ActiveProfiles注解設(shè)置
使用DispatcherServlet的初始化參數(shù)
使用DispatcherServlet的參數(shù)將spring.profiles.default設(shè)置為開(kāi)發(fā)環(huán)境的profile,會(huì)在Servlet上下文中進(jìn)行設(shè)置(為了兼顧到ContextLoaderListener)。在Web應(yīng)用中,設(shè)置spring.profiles.default的web.xml文件會(huì)如下所示:

按照這種方式設(shè)置spring.profiles.default,所有的開(kāi)發(fā)人員都能從版本控制軟件中獲得應(yīng)用程序源碼,并使用開(kāi)發(fā)環(huán)境的設(shè)置運(yùn)行代碼,不需要任何額外的配置。
當(dāng)應(yīng)用程序部署到QA、生產(chǎn)或其他環(huán)境之中時(shí),根據(jù)情況使用系統(tǒng)屬性、環(huán)境變量或JNDI設(shè)spring.profiles.active即可。
在spring.profiles.active和spring.profiles.default中,profile使用的都是復(fù)數(shù)形式。
意味著可以同時(shí)激活多個(gè)profile,這可以通過(guò)列出多個(gè)profile名稱(chēng),并以逗號(hào)分隔來(lái)實(shí)現(xiàn)。
使用profile進(jìn)行測(cè)試
運(yùn)行集成測(cè)試時(shí),通常會(huì)希望采用與生產(chǎn)環(huán)境相同的配置進(jìn)行測(cè)試。如果配置中的bean定義在了profile中,在運(yùn)行測(cè)試時(shí),就需要有一種方式來(lái)啟用合適的profile。
Spring提供了@ActiveProfiles注解,我們可以使用它來(lái)指定運(yùn)行測(cè)試時(shí)要激活哪個(gè)profile:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=DataSourceConfig.class)
@ActiveProfiles("dev")
public static class DevDataSourceTest {
...
}
profile機(jī)制中的條件要基于哪個(gè)profile處于激活狀態(tài)來(lái)判斷。Spring 4.0中提供了一種更為通用的機(jī)制來(lái)實(shí)現(xiàn)條件化的bean定義,這種機(jī)制中的條件完全由你來(lái)確定。
條件化的bean
暫時(shí)跳過(guò)
處理自動(dòng)裝配的歧義性
自動(dòng)裝配能夠提供很大的幫助,因?yàn)樗鼤?huì)減少裝配應(yīng)用程序組件時(shí)所需要的顯式配置的數(shù)量。
但僅有一個(gè)bean匹配所需的結(jié)果時(shí),自動(dòng)裝配才是有效的。如果不僅有一個(gè)bean能夠匹配結(jié)果的話(huà),這種歧義性會(huì)阻礙Spring自動(dòng)裝配屬性、構(gòu)造器參數(shù)或方法參數(shù)。
例如:
@Autowired
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
Dessert是一個(gè)接口,并且有三個(gè)類(lèi)實(shí)現(xiàn)了這個(gè)接口:
@Component
public class Cake implements Dessert { ... }
@Component
public class Cookies implements Dessert { ... }
@Component
public class Iceream implements Dessert { ... }
三個(gè)實(shí)現(xiàn)均使用了@Component注解,在組件掃描的時(shí)候,能夠發(fā)現(xiàn)它們并將其創(chuàng)建為Spring應(yīng)用上下文里面的bean。當(dāng)Spring試圖自動(dòng)裝配setDessert()中的Dessert參數(shù)時(shí),參數(shù)并沒(méi)有唯一、無(wú)歧義的可選值。Spring卻無(wú)法做出選擇。Spring會(huì)拋出NoUniqueBeanDefinitionException。
自動(dòng)裝配歧義性的問(wèn)題其實(shí)比你想象中的更為罕見(jiàn)。當(dāng)確實(shí)發(fā)生歧義性的時(shí)候,Spring提供了多種可選方案來(lái)解決這樣的問(wèn)題。你可以將可選bean中的某一個(gè)設(shè)為首選(primary)的bean,或者使用限定符(qualifier)來(lái)幫助Spring將可選的bean的范圍縮小到只有一個(gè)bean。
標(biāo)示首選的bean
聲明bean的時(shí)候,通過(guò)將其中一個(gè)可選的bean設(shè)置為首選(primary)bean能夠避免自動(dòng)裝配時(shí)的歧義性。當(dāng)遇到歧義性的時(shí)候,Spring會(huì)使用首選的bean。
@Primary能夠與@Component組合用在組件掃描的bean上,也可以與@Bean組合用在Java配置的bean聲明中。
@Component
@Primary
public class Iceream implements Dessert { ... }
@Bean
@Primary
public Dessert iceCream() {
return new IceCream();
}
如果使用XML配置bean,同樣可以實(shí)現(xiàn)這樣的功能,通過(guò)使用<bean>元素中的primary屬性指定首選的bean:
<bean id="iceCream"
class="com.desserteater.IceCream"
primary="true" />
就解決歧義性問(wèn)題而言,相較于使用首選標(biāo)示,限定符是一種更為強(qiáng)大的機(jī)制。
限定自動(dòng)裝配的bean
設(shè)置首選bean的局限性在于@Primary無(wú)法將可選方案的范圍限定到唯一一個(gè)無(wú)歧義性的選項(xiàng)中。它只能標(biāo)示一個(gè)優(yōu)先的可選方案。當(dāng)首選bean多于一個(gè),并沒(méi)有其他的方法縮小可選范圍。
相反的,Spring的限定符能夠在所有可選的bean上進(jìn)行縮小范圍的操作,最終達(dá)到只有一個(gè)bean滿(mǎn)足所規(guī)定的限制條件。如果將所有的限定符都用上后依然存在歧義性,可以繼續(xù)使用更多的限定符來(lái)縮小選擇范圍。
@Qualifier注解是使用限定符的主要方式??梢耘c@Autowired和@Inject協(xié)同使用,在注入的時(shí)候指定想要注入進(jìn)去的是哪個(gè)bean:
@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
為@Qualifier注解所設(shè)置的參數(shù)就是想要注入的bean的ID。
所有使用@Component注解聲明的類(lèi)都會(huì)創(chuàng)建為bean,并且bean的ID為首字母變?yōu)樾?xiě)的類(lèi)名。因此,@Qualifier("iceCream")指向的是組件掃描時(shí)所創(chuàng)建的IceCream類(lèi)的實(shí)例bean。
@Qualifier("iceCream")所引用的bean要具有String類(lèi)型的“iceCream”作為限定符。
基于默認(rèn)的bean ID作為限定符時(shí),限定符與要注入的bean的名稱(chēng)是緊耦合的,有可能會(huì)引入一些問(wèn)題。對(duì)類(lèi)名稱(chēng)的任意改動(dòng)都會(huì)導(dǎo)致限定符失效,無(wú)法匹配限定符。導(dǎo)致自動(dòng)裝配失敗。
創(chuàng)建自定義的限定符
我們可以為bean設(shè)置自己的限定符,而不是依賴(lài)于將bean ID作為限定符。所需要做的就是在bean聲明(@Component注解的類(lèi))上添加@Qualifier注解:
@Component
@Qualifier("cold")
public class IceCream implements Dessert { ... }
cold限定符分配給了IceCreambean。因?yàn)樗鼪](méi)有耦合類(lèi)名,因此可以隨意重構(gòu)IceCream的類(lèi)名,而不必?fù)?dān)心會(huì)破壞自動(dòng)裝配。
在注入的地方,只要引用cold限定符就可以了:
@Autowired
@Qualifier("cold")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
當(dāng)通過(guò)Java配置顯式定義bean時(shí),@Qualifier也可以與@Bean注解一起使用:
@Bean
@Qualifier("cold")
public Dessert iceCream() {
return new IceCream();
}
使用自定義的限定符注解
面向特性的限定符要比基于bean ID的限定符更好一些。但是,如果多個(gè)bean都具備相同特性的話(huà),這種做法也會(huì)出現(xiàn)問(wèn)題。
再引入一個(gè)新的Dessert bean:
@Component
@Qualifier("cold")
public class Popsicle implements Dessert { ... }
這樣,在自動(dòng)裝配Dessert bean的時(shí)候,再次遇到了歧義性的問(wèn)題,需要使用更多的限定符來(lái)將可選范圍限定到只有一個(gè)bean。
可能的解決方案是在注入點(diǎn)和bean定義的地方同時(shí)再添加另外一個(gè)@Qualifier注解:
@Component
@Qualifier("cold")
@Qualifier("creamy")
public class IceCream implements Dessert() { ... }
@Component
@Qualifier("cold")
@Qualifier("fruity")
public class Popsicle implements Dessert { ... }
在注入點(diǎn):
@Autowired
@Qualifier("cold")
@Qualifier("creamy")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
但Java不允許在同一個(gè)條目上重復(fù)出現(xiàn)相同類(lèi)型的多個(gè)注解。如果試圖這樣做,編譯器會(huì)提示錯(cuò)誤。
但是可以創(chuàng)建自定義的限定符注解,借助這樣的注解來(lái)表達(dá)bean所希望限定的特性。這里所需要做的就是創(chuàng)建一個(gè)注解,它本身使用@Qualifier注解來(lái)標(biāo)注。這樣可以不再使用@Qualifier("cold"),而是使用自定義的@Cold注解:
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold { }
同樣,你可以創(chuàng)建一個(gè)新的@Creamy注解來(lái)代替@Qualifier("creamy"):
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Creamy { }
通過(guò)在定義時(shí)添加@Qualifier注解,它們就具有了@Qualifier注解的特性。它們本身實(shí)際上就成為了限定符注解。
重新為IceCream添加@Cold和@Creamy注解:
@Component
@Cold
@Creamy
public class IceCream implements Dessert() { ... }
為Popsicle類(lèi)添加@Cold和@Fruity注解:
@Component
@Cold
@Fruity
public class Popsicle implements Dessert() { ... }
最終,在注入點(diǎn),我們使用必要的限定符注解進(jìn)行任意組合,從而將可選范圍縮小到只有一個(gè)bean滿(mǎn)足需求:
@Autowired
@Cold
@Creamy
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
通過(guò)聲明自定義的限定符注解,我們可以同時(shí)使用多個(gè)限定符,不會(huì)再有Java編譯器的限制或錯(cuò)誤。同時(shí),相對(duì)于使用原始的@Qualifier并借助String類(lèi)型來(lái)指定限定符,自定義的注解也更為類(lèi)型安全。
沒(méi)有在任何地方明確指定要將IceCream自動(dòng)裝配到setDessert()方法中。因此,setDessert()方法依然能夠與特定的Dessert實(shí)現(xiàn)保持解耦。任意滿(mǎn)足這些特征的bean都是可以的。
bean的作用域
默認(rèn)情況下,Spring應(yīng)用上下文中所有bean都是作為以單例(singleton)的形式創(chuàng)建的。也就是不管給定的一個(gè)bean被注入到其他bean多少次,每次所注入的都是同一個(gè)實(shí)例。
大多數(shù)情況下,單例bean是很理想的方案。初始化和垃圾回收對(duì)象實(shí)例所帶來(lái)的成本只留給一些小規(guī)模任務(wù),在這些任務(wù)中,讓對(duì)象保持無(wú)狀態(tài)并且在應(yīng)用中反復(fù)重用這些對(duì)象可能并不合理。
有時(shí)候,所使用的類(lèi)是易變的(mutable),它們會(huì)保持一些狀態(tài),因此重用是不安全的。在這種情況下,不應(yīng)該將class聲明為單例的bean,因?yàn)閷?duì)象會(huì)被污染,重用的時(shí)候會(huì)出現(xiàn)意想不到的問(wèn)題。
Spring定義了多種作用域,可以基于這些作用域創(chuàng)建bean:
- 單例(Singleton):在整個(gè)應(yīng)用中,只創(chuàng)建bean的一個(gè)實(shí)例。
- 原型(Prototype):每次注入或者通過(guò)Spring應(yīng)用上下文獲取的時(shí)候,都會(huì)創(chuàng)建一個(gè)新的bean實(shí)例。
- 會(huì)話(huà)(Session):在Web應(yīng)用中,為每個(gè)會(huì)話(huà)創(chuàng)建一個(gè)bean實(shí)例。
- 請(qǐng)求(Rquest):在Web應(yīng)用中,為每個(gè)請(qǐng)求創(chuàng)建一個(gè)bean實(shí)例。
單例是默認(rèn)的作用域,但是對(duì)于易變的類(lèi)型,并不合適。選擇其他的作用域,要使用@Scope注解,它可以與@Component或@Bean一起使用。
如果你使用組件掃描來(lái)發(fā)現(xiàn)和聲明bean,那么可以在bean的類(lèi)上使用@Scope注解,將其聲明為原型bean:
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class NotePad { ... }
這里,使用ConfigurableBeanFactory類(lèi)的SCOPE_PROTOTYPE常量設(shè)置了原型作用域。也可以使用@Scope("prototype"),但是使用SCOPE_PROTOTYPE常量更加安全并且不易出錯(cuò)。
如果想在Java配置中將Notepad聲明為原型bean,可以組合使用@Scope和@Bean來(lái)指定所需的作用域:
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTYPE)
public Notepad notepad() {
return new Notepad();
}
同樣,如果你使用XML來(lái)配置bean的話(huà),可以使用<bean>元素的scope屬性來(lái)設(shè)置作用域:
<bean id="notepad"
class="com.myapp.Notepad"
scope="prototype" />
使用會(huì)話(huà)和請(qǐng)求作用域
在Web應(yīng)用中,實(shí)例化在會(huì)話(huà)和請(qǐng)求范圍內(nèi)共享的bean,是非常有價(jià)值的事情。例如,在典型的電子商務(wù)應(yīng)用中,可能會(huì)有一個(gè)bean代表用戶(hù)的購(gòu)物車(chē)。如果購(gòu)物車(chē)是單例的話(huà),將會(huì)導(dǎo)致所有的用戶(hù)都會(huì)向同一個(gè)購(gòu)物車(chē)中添加商品。如果購(gòu)物車(chē)是原型作用域的,那么在應(yīng)用中某一個(gè)地方往購(gòu)物車(chē)中添加商品,,在應(yīng)用的另外一個(gè)地方可能就不可用了,因在這里注入的是另外一個(gè)原型作用域的購(gòu)物車(chē)。
對(duì)購(gòu)物車(chē)bean來(lái)說(shuō),會(huì)話(huà)作用域是最為合適的,因?yàn)樗c給定的用戶(hù)關(guān)聯(lián)性最大。指定會(huì)話(huà)作用域,它的使用方式與指定原型作用域是相同的:
@Bean
@Scope(
value=WebApplicationContext.SCOPE_SESSION,
proxyMode=ScopedProxyMode.INTERFACES)
public ShoppingCart cart() { ... }
這里,將value設(shè)置成了WebApplicationContext中的SCOPE_SESSION常量(它的值是session)。這會(huì)告訴Spring為Web應(yīng)用中的每個(gè)會(huì)話(huà)創(chuàng)建一個(gè)ShoppingCart。這會(huì)創(chuàng)建多個(gè)ShoppingCart bean的實(shí)例,但是對(duì)于給定的會(huì)話(huà)只會(huì)創(chuàng)建一個(gè)實(shí)例,在當(dāng)前會(huì)話(huà)相關(guān)的操作中,這個(gè)bean實(shí)際上相當(dāng)于單例的。
注意上面的@Scope同時(shí)還有一個(gè)proxyMode屬性,它被設(shè)置成了ScopedProxyMode.INTERFACES。這個(gè)屬性解決了將會(huì)話(huà)或請(qǐng)求作用域的bean注入到單例bean中所遇到的問(wèn)題。
假設(shè)我們要將ShoppingCart bean注入到單例StoreService bean的Setter方法中:
@Component
public class StoreService {
@Autowired
public void setShoppingCart(ShoppingCart shoppingCart) {
this.shoppingCart = shoppingCart;
}
...
}
因?yàn)镾toreService是一個(gè)單例的bean,會(huì)在Spring應(yīng)用上下文加載的時(shí)候創(chuàng)建。當(dāng)它創(chuàng)建的時(shí)候,Spring會(huì)試圖將ShoppingCart bean注入到setShoppingCart()方法中。但是ShoppingCart bean是會(huì)話(huà)作用域的,此時(shí)并不存在。直到某個(gè)用戶(hù)進(jìn)入系統(tǒng),創(chuàng)建了會(huì)話(huà)之后,才會(huì)出現(xiàn)ShoppingCart實(shí)例。
此外,系統(tǒng)中將會(huì)有多個(gè)ShoppingCart實(shí)例:每個(gè)用戶(hù)一個(gè)。我們并不想讓Spring注入某個(gè)固定的ShoppingCart實(shí)例到StoreService中。我們希望的是當(dāng)StoreService處理購(gòu)物車(chē)功能時(shí),它所使用的ShoppingCart實(shí)例恰好是當(dāng)前會(huì)話(huà)所對(duì)應(yīng)的那一個(gè)。
Spring不會(huì)將實(shí)際的ShoppingCart bean注入到StoreService中,Spring會(huì)注入一個(gè)到ShoppingCart bean的代理,如下圖所示:

這個(gè)代理會(huì)暴露于ShoppingCart相同的方法,所以StoreService會(huì)認(rèn)為它就是一個(gè)購(gòu)物車(chē)。當(dāng)StoreService調(diào)用ShoppingCart的方法時(shí),代理會(huì)對(duì)其進(jìn)行懶解析并將調(diào)用委托給會(huì)話(huà)作用域內(nèi)真正的ShoppingCart bean。
現(xiàn)在討論一下proxyMode屬性。如配置所示,proxyMode屬性被設(shè)置成了ScopedProxyMode.INTERFACES,這表明這個(gè)代理要實(shí)現(xiàn)ShoppingCart接口,并將調(diào)用委托給實(shí)現(xiàn)bean。
如果ShoppingCart是接口而不是類(lèi)的話(huà),這時(shí)最為理想的代理模式。但如果ShoppingCart是一個(gè)具體的類(lèi)的話(huà),Spring就沒(méi)有辦法創(chuàng)建基于接口的代理。此時(shí),它必須使用CGLib來(lái)生成基于類(lèi)的代理。所以,如果bean類(lèi)型是具體類(lèi)的話(huà),我們必須要將proxyMode屬性設(shè)置為ScopedProxyMode.TARGET_CLASS,以此來(lái)表明要以生成目標(biāo)類(lèi)擴(kuò)展的方式創(chuàng)建代理。
同樣的,請(qǐng)求作用域的bean也會(huì)面臨相同的裝配問(wèn)題。因此,請(qǐng)求作用域的bean應(yīng)該也以作用域代理的方式進(jìn)行注入。
在XML中聲明作用域代理
如果你需要使用XML來(lái)聲明會(huì)話(huà)或請(qǐng)求作用域的bean,就不能使用@Scope注解及其proxyMode屬性了。<bean>元素的scope屬性能夠設(shè)置bean的作用域,要設(shè)置代理模式,我們需要使用Spring aop命名空間的一個(gè)新元素:
<bean id="cart"
class="com.myapp.ShoppingCart"
scope="session">
<aop:scope-proxy />
</bean>
<aop:scoped-proxy>是與@Scope注解的proxyMode屬性功能相同的Spring XML配置元素。它會(huì)告訴Spring為bean創(chuàng)建一個(gè)作用域代理。默認(rèn)情況下,會(huì)使用CGLib創(chuàng)建目標(biāo)類(lèi)的代理。但是也可以將proxy-target-class屬性設(shè)置為false,進(jìn)而要求它生成基于接口的代理:
<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>
運(yùn)行時(shí)注入
討論依賴(lài)注入的時(shí)候,通常討論的是將一個(gè)bean引用注入到另一個(gè)bean的屬性或構(gòu)造器參數(shù)中。通常來(lái)講指的是將一個(gè)對(duì)象與另一個(gè)對(duì)象進(jìn)行關(guān)聯(lián)。
bean裝配的另外一個(gè)方面指的是將一個(gè)值注入到bean的屬性或者構(gòu)造器參數(shù)中。如果按照這樣的方式來(lái)組裝BlankDisc:

盡管這實(shí)現(xiàn)了需求,為BlankDisc bean設(shè)置title和artist,但它在實(shí)現(xiàn)的時(shí)候是將值硬編碼在配置類(lèi)中的。
有時(shí)想讓這些值在運(yùn)行時(shí)再確定。Spring提供了兩種在運(yùn)行時(shí)求值的方式:
- 屬性占位符(Property placeholder)。
- Spring表達(dá)式語(yǔ)言(SpEL)。
注入外部的值
Spring中,處理外部值的最簡(jiǎn)單方式就是聲明屬性源并通過(guò)Spring的Environment來(lái)檢索屬性。下面的程序展示了基本的Spring配置類(lèi),它使用外部的屬性來(lái)裝配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引用了類(lèi)路徑中一個(gè)名為app.properties的文件。這個(gè)屬性文件會(huì)加載到Spring的Environment中,稍后可以從這里檢索屬性。同時(shí),在disc()方法中,會(huì)創(chuàng)建一個(gè)新的BlankDisc,它的構(gòu)造器參數(shù)是從屬性文件中獲取的,這是通過(guò)調(diào)用getProperty()實(shí)現(xiàn)的。
深入學(xué)習(xí)Spring的Environment
getProperty()并非只有上面程序獲取屬性值的一種方法,getProperty()方法有四個(gè)重載的變種形式:
- 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()方法都會(huì)返回String類(lèi)型的值。你可以稍微對(duì)@Bean方法進(jìn)行一下修改,這樣在指定屬性不存在的時(shí)候,會(huì)使用一個(gè)默認(rèn)值:
@Bean
public BlankDisc blankDisc() {
return new BlankDisc(
env.getProperty("disc.title", "Rattle and Hum"),
env.getProperty("disc.artist", "U2"));
}
剩下的兩種getProperty()方法與前面的兩種非常類(lèi)似,但是它們不會(huì)將所有的值都視為String類(lèi)型。例如,你想要獲取的值所代表的含義是連接池中所維持的連接數(shù)量。如果使用重載形式的getProperty()的話(huà),就能非常便利地解決這個(gè)問(wèn)題:
int connectionCount = env.getProperty("db.connection.count", Integer.class, 30);
Environment還提供了幾個(gè)與屬性相關(guān)的方法,如果在使用getProperty()方法的時(shí)候沒(méi)有指定默認(rèn)值,并且這個(gè)屬性沒(méi)有定義的話(huà),獲取到的值是null。如果希望這個(gè)屬性必須要定義,可以使用getRequiredProperty()方法。
@Bean
public BlankDisc blankDisc() {
return new BlankDisc(
env.getRequiredProperty("disc.title"),
env.getRequiredProperty("disc.artist"));
}
如果disc.title或disc.artist屬性沒(méi)有定義的話(huà),將會(huì)拋出IllegalStateException異常。
想檢查一下某個(gè)屬性是否存在,可以調(diào)用Environment的containsProperty()方法:
boolean titleExists = env.containsProperty("disc.title");
如果想將屬性解析為類(lèi)的話(huà),可以使用getPropertyAsClass()方法:
Class<CompactDisc> cdClass =
env.getPropertyAsClass("disc.class",CompactDisc.class);
除了屬性相關(guān)功能以外,Environment還提供了一些方法來(lái)檢查哪些profile處于激活狀態(tài):
-
String[] getActiveProfiles():返回激活profile名稱(chēng)的
數(shù)組; -
String[] getDefaultProfiles():返回默認(rèn)profile名稱(chēng)的
數(shù)組; -
boolean acceptsProfiles(String... profiles):如
果environment支持給定profile的話(huà),就返回true。
除了直接從Environment中檢索屬性外,Spring也提供了通過(guò)占位符裝配屬性的方法,這些占位符的值會(huì)來(lái)源于一個(gè)屬性源。
解析屬性占位符
Spring支持將屬性定義到外部的屬性的文件中,并使用占位符值將其插入到Spring bean中。在Spring裝配中,占位符的形式為使用“${... }”包裝的屬性名稱(chēng)。比如可以在XML中按照如下的方式解析BlankDisc構(gòu)造器參數(shù):
<bean id="sgtPeppers"
class="com.soundsystem.BlankDisc"
c:_title = "${disc.title}"
c:_artist = "${disc.artist}"/>
可以看到,title構(gòu)造器參數(shù)所給定的值是從一個(gè)屬性中解析得到的,這個(gè)屬性的名稱(chēng)為disc.title。artist參數(shù)裝配的是名為disc.artist的屬性值。按照這種方式,XML配置沒(méi)有使用任何硬編碼的值,它的值是從配置文件以外的一個(gè)源中解析得到的。
如果我們依賴(lài)于組件掃描和自動(dòng)裝配來(lái)創(chuàng)建和初始化應(yīng)用組件的話(huà),那么就沒(méi)有指定占位符的配置文件或類(lèi)了。在這種情況下,可以使用@Value注解,它的使用方式與@Autowired注解非常相似。比如,在BlankDisc類(lèi)中,構(gòu)造器可以改成如下所示:
public BlankDisc(
@Value("${disc.title}") String title,
@Value("${disc.artist}") String artist) {
this.title = title;
this.artist = artist;
}
為了使用占位符,我們必須要配置一個(gè)PropertyPlaceholderConfigurer bean或PropertySourcesPlaceholderConfigurer bean。從Spring 3.1開(kāi)始,推薦使用PropertySourcesPlaceholderConfigurer,因?yàn)樗軌蚧?code>Spring Environment及其屬性源來(lái)解析占位符。如下的@Bean方法在Java中配置了PropertySourcesPlaceholderConfigurer:
@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer {
return new PropertySourcesPlaceholderConfigurer();
}
如果想使用XML配置的話(huà),Spring context命名空間中的
<context:propertyplaceholder>元素將會(huì)為你生
成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>
解析外部屬性能夠?qū)⒅档奶幚硗七t到運(yùn)行時(shí),但是它的關(guān)注點(diǎn)在于根據(jù)名稱(chēng)解析來(lái)自于Spring Environment和屬性源的屬性。而Spring表達(dá)式語(yǔ)言提供了一種更通用的方式在運(yùn)行時(shí)計(jì)算所要注入的值。
使用Spring表達(dá)式語(yǔ)言進(jìn)行裝配
Spring 3引入了Spring表達(dá)式語(yǔ)言(Spring Expression Language,SpEL),它能夠以一種強(qiáng)大簡(jiǎn)潔的方式將值裝配到bean屬性和構(gòu)造器參數(shù)中,在這個(gè)過(guò)程中所使用的表達(dá)式會(huì)在運(yùn)行時(shí)計(jì)算得到值。
SpEL擁有很多特性,包括:
- 使用bean的ID來(lái)引用bean;
- 調(diào)用方法和訪(fǎng)問(wèn)對(duì)象的屬性;
- 對(duì)值進(jìn)行算術(shù)、關(guān)系和邏輯運(yùn)算;
- 正則表達(dá)式匹配;
- 集合操作。
SpEL樣例
SpEL表達(dá)式要放到#{ ... }之中,這與屬性占位符有些類(lèi)似,屬性占位符需要放到${ ... }之中。
#{1}
上面的例子除去#{ ... }標(biāo)記之后,剩下的就是SpEL表達(dá)式體了,也就是一
個(gè)數(shù)字常量。這個(gè)表達(dá)式的計(jì)算結(jié)果就是數(shù)字1。
在實(shí)際的應(yīng)用程序中,我們可能會(huì)使用更加有意思的表達(dá)式,如:
#{T(System).currentTimeMillis()}
它的最終結(jié)果是計(jì)算表達(dá)式的那一刻當(dāng)前時(shí)間的毫秒數(shù)。T()表達(dá)式會(huì)將java.lang.System視為Java中對(duì)應(yīng)的類(lèi)型,因此可以調(diào)用其static修飾的currentTimeMillis()方法。
SpEL表達(dá)式也可以引用其他的bean或其他bean的屬性。例如,如下的表達(dá)式會(huì)計(jì)算得到ID為sgtPeppers的bean的artist屬性:
#{sgtPeppers.artist}
還可以通過(guò)systemProperties對(duì)象引用系統(tǒng)屬性:
#{systemProperties['disc.title']}
接下來(lái)看一下在bean裝配的時(shí)候如何使用這些表達(dá)式。
如果通過(guò)組件掃描創(chuàng)建bean的話(huà),在注入屬性和構(gòu)造器參數(shù)時(shí),可以使用@Value注解,下面的樣例展現(xiàn)了BlankDisc,它會(huì)從系統(tǒng)屬性中獲取專(zhuān)輯名稱(chēng)和藝術(shù)家的名字:
public BlankDisc(
@Value("#{systemProperties['disc.title']}") String title,
@Value("#{systemProperties['disc.artist']}") String artist) {
this.title = title;
this.artist = artist;
}
那現(xiàn)在就來(lái)學(xué)習(xí)一下SpEL所支持的基礎(chǔ)表達(dá)式。
表示字面值
浮點(diǎn)值
#{3.14159}
科學(xué)計(jì)數(shù)法
#{9.87E4}
String類(lèi)型字面值
#{'Hello'}
Boolean類(lèi)型值(true | false)
#{true}
引用bean、屬性和方法
使用SpEL可以將一個(gè)bean裝配到另外一個(gè)bean的屬性中
#{sgtPeppers}
在一個(gè)表達(dá)式中引用sgtPeppers的artist屬性
#{sgtPeppers.artist}
還可以調(diào)用bean上的方法。
#{artistSelector.selectArtist()}
對(duì)被調(diào)用的返回值同樣可以調(diào)用它的方法
#{artistSelector.selectArtist().toUpperCase()}
上面沒(méi)有考慮到返回值為null的情況,可以使用類(lèi)型安全的運(yùn)算符
#{artistSelector.selectArtist()?.toUpperCase()}
?.這個(gè)運(yùn)算符能夠在訪(fǎng)問(wèn)它右邊的內(nèi)容之前,確保它所對(duì)應(yīng)的元素不是null。如果selectArtist()的返回值是null的話(huà),那么SpEL將不會(huì)調(diào)用toUpperCase()方法。表達(dá)式的返回值會(huì)是null。
在表達(dá)式中使用類(lèi)型
如果要在SpEL中訪(fǎng)問(wèn)類(lèi)作用域的方法和常量的話(huà),要依賴(lài)T()這個(gè)運(yùn)算符。例如,為了在SpEL中表達(dá)Java的Math類(lèi),需要按照如下的方式使用T()運(yùn)算符:
T(java.lang.Math)
這里T()運(yùn)算符的結(jié)果會(huì)是一個(gè)Class對(duì)象,代表了java.lang.Math。如果需要的話(huà),甚至可以將其裝配到一個(gè)Class類(lèi)型的bean屬性中。但是T()運(yùn)算符的真正價(jià)值在于它能夠訪(fǎng)問(wèn)目標(biāo)類(lèi)型的靜態(tài)方法和常量。
例如需要將PI值裝配到bean屬性中
T(java.lang.Math).PI
類(lèi)似地,我們可以調(diào)用T()運(yùn)算符所得到類(lèi)型的靜態(tài)方法。如下樣例會(huì)計(jì)算得到一個(gè)0到1之間的隨機(jī)數(shù):
T(java.lang.Math).random()
SpEl運(yùn)算符
SpEL提供了多個(gè)運(yùn)算符,這些運(yùn)算符可以用在SpEL表達(dá)式的值上。
| 運(yùn)算符類(lèi)型 | 運(yùn)算符 |
|---|---|
| 算數(shù)運(yùn)算 | +、-、*、/、%、^ |
| 比較運(yùn)算 | <、>、==、>=、<=、lt、gt、eq、le、ge |
| 邏輯運(yùn)算 | and、or、not、| |
| 條件運(yùn)算 | ?:(ternart)、?:(Elvis) |
| 正則表達(dá)式 | matches |
作為使用上述運(yùn)算符的一個(gè)簡(jiǎn)單樣例,看一下下面這個(gè)SpEL表達(dá)式:
#{2 * T(java.lang.Math).PI * circle.radius}
SpEL還提供了查詢(xún)運(yùn)算符(.?[]),它會(huì)用來(lái)對(duì)集合進(jìn)行過(guò)濾,得到集合的一個(gè)子集。作為闡述的樣例,假設(shè)你希望得到jukebox中artist屬性為Aerosmith的所有歌曲。如下的表達(dá)式就使用查詢(xún)運(yùn)算符得到了Aerosmith的所有歌曲:
#{jukebox.songs.?[artist eq 'Aerosmith']}
SpEL還提供了另外兩個(gè)查詢(xún)運(yùn)算符:.^[]和.$[],它們分別用來(lái)在集合中查詢(xún)第一個(gè)匹配項(xiàng)和最后一個(gè)匹配項(xiàng)。例如,考慮下面的表達(dá)式,它會(huì)查找列表中第一個(gè)artist屬性為Aerosmith的歌曲:
#{jukebox.songs.^[artist eq 'Aerosmith']}
SpEL還提供了投影運(yùn)算符(.![]),它會(huì)從集合的每個(gè)成員中選擇特定的屬性放到另外一個(gè)集合中。作為樣例,假設(shè)我們不想要歌曲對(duì)象的集合,而是所有歌曲名稱(chēng)的集合。如下的表達(dá)式會(huì)將title屬性投影到一個(gè)新的String類(lèi)型的集合中:
#{jukebox.songs.![title]}
投影操作也可以與其他任意的SpEL運(yùn)算符一起使用。比如,我們可以使用如下的表達(dá)式獲得Aerosmith所有歌曲的名稱(chēng)列表:
#{jukebox.songs.^[artist eq 'Aerosmith'].![title]}