1.環境與Profile
在開發軟件的時候,有時候需要從一個環境遷移到另一個環境。比如在開發階段我們使用的是dev的環境,在測試階段使用的是product環境,這時我們就需要不同的配置。Spring同樣也提供了類似的解決方案(在Spring3.1中引入了bean profile功能)。如下所示,是一個使用@Profile註解來實現的實例。
首先需要準備兩個配置類:
// ProdConfiguration.java
@Configuration
@Profile("prod")
public class ProdConfiguration {
@Bean
public ProdE getProdE(){
ProdE prodE = new ProdE();
prodE.setContent("產品");
return prodE;
}
}
// DevConfiguration.java
@Configuration
@Profile("dev")
public class DevConfiguration {
@Bean
public DevE getDevE() {
DevE devE = new DevE();
devE.setContent("開發");
return devE;
}
}
再需要在初始化Servlet環境配置類(用於替換web.xml)中設置profile值(下劃線)
public class HelloWorldInitializer implements WebApplicationInitializer {
public void onStartup(ServletContext container) throws ServletException {
AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
ctx.register(HelloWorldConfiguration.class);
ctx.setServletContext(container);
ctx.getEnvironment().setActiveProfiles("dev");
ServletRegistration.Dynamic servlet = container.addServlet(
"dispatcher", new DispatcherServlet(ctx));
servlet.setLoadOnStartup(1);
servlet.addMapping("/");
}
}
最後用一個Controller來測試一下:
@Controller
@RequestMapping("/")
public class HelloWorldController {
@Autowired(required=false)
ProdE prodE;
@Autowired(required=false)
DevE devE;
@RequestMapping(method = RequestMethod.GET)
public String test(ModelMap model) {
if(null!=prodE){
System.out.println("******"+prodE.getContent()+"******");
}else{
System.out.println("******prodE is null******");
}
if(null!=devE){
System.out.println("******"+devE.getContent()+"******");
}else{
System.out.println("******devE is null******");
}
model.addAttribute("greeting", "Hello World from Spring 4 MVC");
return "welcome";
}
}
當然也可以使用XML配置,如下所示:
<beans profile="prod">
<!-- 在這裏配置bean -->
</beans>
<beans profile="dev">
<!-- 在這裏配置bean -->
</beans>
從Spring3.2開始已經支持在方法級別上使用@Profile註解,與@Bean註解一起使用,這樣就可以將這兩種聲明放置在同一個配置類中。
2.激活使用profile
如果不激活使用profile,你配置的profile也是不會被使用的,因爲Spring不能確定哪個profile是需要使用的。Spring提供了兩個獨立的屬性來構成一個比較合理的設置:spring.profiles.active和spring.profiles.default。如果設置了spring.profiles.active屬性,則Spring會設置本值,如果spring.profiles.active沒有設置,則使用spring.profiles.default的默認值。spring.profiles.active對應於註解中的ConfigurableEnvironment.setActiveProfiles(String... profiles);spring.profiles.default對應於註解中的ConfigurableEnvironment.setDefaultProfiles(String... profiles);我們可以有多種方式設置這兩種屬性:
- 作爲DispatcherServlet的初始化參數
- 作爲Web應用上下文條目參數
- 作爲JNDI條目
- 作爲環境變量
- 作爲JVM的系統變量
- 在集成測試類上,使用@ActiveProfiles註解設置
XML中的配置如下所示:
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="WebApp_ID" version="2.5">
<display-name>test</display-name>
<!-- - Location of the XML file that defines the root application context.
- Applied by ContextLoaderListener. -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/application-config.xml</param-value>
</context-param>
<!-- 爲上下文設置默認的profile -->
<context-param>
<param-name>spring.profiles.default</param-name>
<param-value>dev</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/mvc-config.xml</param-value>
</init-param>
<!-- 爲DispatcherServlet設置默認的profile -->
<init-param>
<param-name>spring.profiles.default</param-name>
<param-value>dev</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
3.在Springboot中使用Profile
在Springboot中它允許你通過命名約定按照一定格式來定義多個配置文件,文件格式如下:application-{profile}.properties,比如:application-dev.properties(開發環境)、application-test.properties(測試環境)、application-prod.properties(生產環境)。我們只需要在application.properties中指定spring.profiles.defualt屬性的值或者在jar包運行時增加--spring.profiles.active=XXX屬性即可(也可以在外部配置文件覆蓋內部application.properties文件中spring.profiles.defualt屬性),@Profile註解使用方式和以前相同。
4.條件註解@Conditional
在有些場景下,我們希望當要創建的Bean滿足一系列條件後才創建。Spring4中新增了@Conditional註解,它可以用到帶有@Bean註解的方法上,爲我們提供這一功能。當給定的條件滿足時,返回true,創建Bean;當給定的條件不滿足時,返回false,不創建Bean。樣例如下所示(以獲取本機操作系統來區分是否要加載配置爲例):
首先需要寫判定條件,實現Condition接口:
public class LinuxCondition implements Condition {
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 判斷當前操作系統是否是Linux
return context.getEnvironment().getProperty("os.name").contains("Linux");
}
}
public class WindowsCondition implements Condition {
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 判斷當前操作系統是否是windows
return context.getEnvironment().getProperty("os.name").contains("Windows");
}
}
再只需要在@Bean註解(配置類)過的方法上使用該註解即可:
@Bean
@Conditional(WindowsCondition.class)
public WindowsE getWin() {
WindowsE e = new WindowsE();
System.out.println("WindowsE Bean Is Created.");
return e;
}
@Bean
@Conditional(LinuxCondition.class)
public LinuxE getLinux() {
LinuxE e = new LinuxE();
System.out.println("LinuxE Bean Is Created.");
return e;
}
設置給@Conditional的類可以是任意實現了Condition接口的類型。這個接口,如下所示,只需實現matches()方法實現即可。matches()方法會使用ConditionContext和AnnotatedTypeMetadata對象來做決策。
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
4.1.ConditionContext條件判斷上下文
通過ConditionContext類,我們可以做到以下幾點:
藉助getRegistry()方法返回BeanDefinitionRegistry檢查Bean定義
藉助getBeanFactory()方法返回ConfigurableListableBeanFactory檢查Bean是否存在,甚至探查Bean的屬性
藉助getEnvironment()方法返回Environment檢查環境變量是否存在和值
藉助getResourceLoader()方法返回ResourceLoader所加載的資源
藉助getClassLoader()方法返回ClassLoader加載並檢查類是否存在
4.2.AnnotatedTypeMetadata獲取其他屬性註解
AnnotatedTypeMetadata的isannotated()方法可以判斷被@Bean註解的方法是否有其他註解屬性,通過getAllAnnotationAttibutes(...)方法獲取所有的註解屬性。除此之外,Spring 4中也對之前的@Profile註解進行的重寫,如下所示:(@Conditional註解調用了ProfileCondition條件)
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {
String[] value();
}
@Profile註解其實也是使用了@Conditional註解,並且引用了ProfileCondition作爲Condition的實現,如下所示。我們可以看到,ProfileCondition通過AnnotatedTypeMetadata獲取使用了@Profile註解的所以屬性。藉助該屬性,它會明確地檢查value屬性,該屬性包含bean的profile的value值,比如@Profile(“dev”)。然後,它通過CondidationContext得到的Environment來檢查該profile是否處於被激活狀態。
5.處理自動裝配的歧義性
雖然在實際編寫代碼中,很少有情況會遇到Bean裝配的歧義性,更多的情況是給定的類只有一個實現,這樣自動裝配就會很好的實現。但是當發生歧義性的時候,Spring提供了多種的可選解決方案。如下所示,我們有chinesefood和asanfood繼承自父類Food,裝配選擇哪種Food時會出現NoUniqueBeanDefinitionException:
如果我們首選阿三food,就如下所示,在類或者配置中bean配置的方法上添加@Primary註解即可:
@Component
@Primary
public class ASanFood implements Food {
public void context() {
System.out.println("ASanFood");
}
}
// ——————或者——————
@Bean
@Primary
public Food getFood(){
return new ASanFood();
}
注意,對於多可選擇項,只能有一個可以加上@Primary
當然可以使用另一種方式解決歧義性@Qualifier比如在類上需要裝配Food類。
@Autowired
@Qualifier("chineseFood")
Food food;
@Qualifier("chineseFood")指向的是掃描組件時創建的Bean,並且這個Bean是IceCream類的實例。事實上如果所有的Bean都沒有自己指定一個限定符(Qualifier),則會有一個默認的限定符(與Bean ID相同),我們可以在Bean的類上添加@Qualifier註解來自定義限定符,如下所示:
@Component
@Qualifier("asFood")
public class ASanFood implements Food {
public void context() {
System.out.println("ASanFood");
}
}
// 使用時:
@Autowired
@Qualifier("asFood")
Food food;
6.Bean作用域
默認情況下,Spring應用上下文中所有的Bean都是單例模式。在大多數情況下單例模式都是非常理想的方案。但是如果,你要注入或者裝配的Bean是易變的,他們會有一些特有的狀態。這種情況下單例模式就會容易被污染。Spring爲此定義了很多作用域,可以基於這些作用域創建Bean,包括:
單例(Singleton):在整個應用中,只創建Bean的一個實例
原型(Prototype):每次注入或者通過Spring應用上下文獲取的時候,都會創建一個新的Bean實例。這個相當於new的操作
會話(Session):在Web應用中,爲每個會話創建一個Bean實例。對於同一個接口的請求,如果使用不同的瀏覽器,將會得到不同的實例(Session不同)
請求(Request):在Web應用中,爲每個請求創建一個Bean實例
使用@Scope註解可以設置多種的作用域,可以放置在類上或者@Bean註解的方法上,如下所示,是一個基本的實例:
@Component
@Scope("prototype")
public class Car {
// 。。。。。。
}
// ——————或者——————
@Bean
@Scope("prototype")
public LinuxConfig getLinux() {
LinuxConfig config = new LinuxConfig();
return config;
}
6.1.使用會話和請求作用域
在實際企業級開發過程中,我們常用@Scope來定義Bean的作用域。如用戶的購物車信息,如果將購物車類聲明爲單例(Singleton),那麼每個用戶都向同一個購物車中添加商品,這樣勢必會造成混亂;你也許會想到使用原型模式聲明購物車,但這樣同一用戶在不同請求時,所獲得的購物車信息是不同的,這也是一種混亂。如下所示:
@Bean
@Scope(
value="session",
proxyMode=ScopedProxyMode.INTERFACES
)
public Cart getCart() {
// ......
}
在這裏我們要注意一下,屬性proxyMode。這個屬性解決了將會話或者請求作用域的Bean注入到單例Bean中所遇到的問題。假設我們要將Cart bean注入到單例StoreService bean的Setter方法中:
@Service
@Scope
public class StoreService {
Cart cart;
@Autowired
public void setCart(Cart cart) {
this.cart = cart;
}
// ......
}
StoreService是一個單例bean,會在Spring應用上下文加載的時候創建。 當它創建的時候, Spring會試圖將Cart bean注入到setCart()方法中。 但是Cart bean是會話作用域的, 此時並不存在。 直到某個用戶進入系統,創建了會話之後,纔會出現Cart實例。系統中將會有多個Cart實例: 每個用戶一個。 我們並不想讓Spring注入某個固定的Cart實例到StoreService中。 我們希望的是當StoreService處理購物車功能時, 它所使用的Cart實例恰好是當前會話所對應的那一個。Spring並不會將實際的Cart bean注入到StoreService中,Spring會注入一個到Cart bean的代理。這個代理會暴露與Cart相同的方法,所以StoreService會認爲它就是一個購物車。但是,當StoreService調用Cart的方法時, 代理會對其進行懶解析並將調用委託給會話作用域內真正的Cart bean。
proxyMode屬性被設置成了ScopedProxyMode.INTERFACES, 這表明這個代理要實現Cart接口,並將調用委託給實現bean。如果Cart是接口而不是類的話,這是可以的(也是最爲理想的代理模式)。但如果Cart是一個具體的類的話,Spring就沒有辦法創建基於接口的代理了。此時,它必須使用CGLib來生成基於類的代理。所以,如果bean類型是具體類的話,我們必須要將proxyMode屬性設置爲ScopedProxyMode.TARGET_CLASS,以此來表明要以生成目標類擴展的方式創建代理。
下面是使用XML來配置Bean的作用域:
<bean class="com.wxh.entity.Cart" scope="session">
<aop:scoped-proxy/>
</bean>
XML中的<aop:scoped-proxy/>代表的是註解中的proxyMode屬性,默認情況下是使用CGLIB代理,如果要使用接口代理,需要手動關閉,如下所示:
<bean class="com.wxh.entity.Cart" scope="session">
<aop:scoped-proxy proxy-target-class="false"/>
</bean>
7.運行時值注入
Bean的屬性注入的時候有時候硬編碼是可以的,但是有時候我們希望避免硬編碼值,而是想讓這些在運行時再確定。爲了實現這些功能,Spring提供了兩種在運行時的求值方式:
屬性佔位符(Property Placeholder)
Spring表示式語言(SpEL)
這兩種技術的用法是類似的,不過他們的目的和行爲是有所差別的。前者比較簡單,後者比較難。
7.1.注入外部值
在Spring中,處理外部值得最簡單方式就是聲明屬性源並通過Spring的Environment來檢索屬性。例如下面所示,展示了最基本的Spring配置:
先添加外部配置文件:
在配置類中引用這個外部配置文件,並使用:
@Configuration
// 聲明屬性源
@PropertySource("classpath:app.properties")
public class ExConfig {
@Autowired
Environment env;
@Bean
public Cart getCart() {
// 檢索配置的屬性值
env.getProperty("cart.context");
// ......
}
}
使用@PropertySource註解會將app.properties配置文件加載到Spring的Environment中,這裏我們將詳細講一下Environment這個接口(Environment實現自PropertyResolver接口)。主要的方法如下:
- String getProperty(String key):通過key獲取值
- String getProperty(String key, String defaultValue):通過key獲取值,如果沒有值,則使用默認值替換
- <T> T getProperty(String key, Class<T> targetType):通過key獲取值,但是可以返回非String類型的值,比如Integer等
- <T> T getProperty(String key, Class<T> targetType, T defaultValue):通過key獲取值,但是可以返回非String類型的值,比如Integer等。如果值不存在則使用默認值
- String getRequiredProperty(String key) throws IllegalStateException:獲取必要的屬性,如果不存在則報錯
- <T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException:同上,不過可以返回除String外的其他類型的值
- boolean containsProperty(String key):是否存在
- String[] getActiveProfiles():返回激活的profile名稱的數組
- String[] getDefaultProfiles():返回默認profile名稱和數組
- boolean acceptsProfiles(String... profiles):如果Environment支持給定profile的話,返回true
7.2.解析屬性佔位符
直接從Environment中檢索屬性是非常方便的,尤其是在Java配置中裝配bean的時候。但是Spring也提供了通過佔位符裝配屬性的方法,這些佔位符的值會來源於一個屬性源。Spring一直支持將屬性值定義到外部的屬性文件中,並使用佔位符將其插入到Spring Bean中。在Spring裝配中,佔位符的形式爲使用“${...}”包裝的屬性名。使用方式如下所示:
先需要配置一個PropertySourcesPlaceholderConfigurer類,因爲它能基於Spring Envrionment及其屬性源來解析佔位符。
@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
然後在配置類中使用@Value註解:
@Bean
public Cart getCart(@Value("${cart.context}") String context) {
// ......
}
當然也可以使用XML來配置,如下所示:
先加屬性佔位符解析類:
<context:property-placeholder/>
在使用XML配置或者@Value註解都可以
<bean class="com.wxh.entity.Cart">
<property name="context" value="${cart.context}"></property>
</bean>