【Spring實戰】高級裝配

本章內容:

  • 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.activespring.profiles.default。如果設置了spring.profiles.active屬性,那么它的值就會用來確定哪個profile是激活的。如果沒有設置spring.profiles.active屬性,Spring會查找spring.profiles.default的值。如果spring.profiles.activespring.profiles.default均沒有設置。代表沒有激活profile,只會創建那些沒有定義在profile中的bean。

有多種方式來設置這兩個屬性:

  • 作為DispatcherServlet的初始化參數;
  • 作為Web應用的上下文參數;
  • 作為JNDI條目;
  • 作為環境變量;
  • 作為JVM的系統屬性;
  • 在集成測試類上,使用@ActiveProfiles注解設置
使用DispatcherServlet的初始化參數

使用DispatcherServlet的參數將spring.profiles.default設置為開發環境的profile,會在Servlet上下文中進行設置(為了兼顧到ContextLoaderListener)。在Web應用中,設置spring.profiles.default的web.xml文件會如下所示:

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的代理,如下圖所示:

作用域代理能夠延遲注入請求和會話作用域的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

盡管這實現了需求,為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還提供了查詢運算符(.?[]),它會用來對集合進行過濾,得到集合的一個子集。作為闡述的樣例,假設你希望得到jukeboxartist屬性為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]}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容