讀Spring實戰(第四版)概括—高級裝配

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>
發佈了60 篇原創文章 · 獲贊 78 · 訪問量 21萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章