《Spring in Action》讀書筆記和總結(jié)。Spring官網(wǎng): spring.io
Spring的底層功能依賴于兩個核心特性,依賴注入(dependency injection, DI)和面向切面編程(aspect-oriented programming, AOP)。Spring簡化了Java開發(fā),提供了輕量級的編程模型,增強了POJO(plain old Java object)的功能。DI和AOP都是為了實現(xiàn)接口和功能的松耦合(Loose Coupling),并且在實現(xiàn)上最大化的采用最小侵入性編程。

DI
實現(xiàn)接口上的松耦合。為了關(guān)聯(lián)各接口之間的調(diào)用和依賴,Spring采用裝配bean方式。建立應(yīng)用接口間依賴關(guān)系的行為稱為裝配(Wiring)。
Spring應(yīng)用上下文(Application Context)負(fù)責(zé)對象的創(chuàng)建和組裝。
Spring提供三種主要的裝配注入機制:
- 隱式的bean自動裝配機制;
- 顯示Java配置;
- 顯示XML配置。
自動化裝配bean
先定義需要的組件component,其次開啟組件掃描(Component scanning),最后調(diào)用其他組件。Spring會自動發(fā)現(xiàn)應(yīng)用上下文中創(chuàng)建的bean。
定義組件
@Component("beanName") //beanName不填寫,默認(rèn)為首字母小寫的類名
public class Impl implements Interface {
}
開啟組件掃描
@Configuration
@ComponentScan
public class AutoConfig {
}
默認(rèn)以配置類所在的包作為基礎(chǔ)包(base package)掃描組件。@ComponentScan(basePackages="pkgName")一個包、@ComponentScan(basePackages={"pkgName1", "pkgName2"})多個包、@ComponentScan(basePackageClasses={Interface1.class, Impl2.class})類或接口所在的包作為組件掃描的基礎(chǔ)包。
<context:component-scan base-package="pkg" />
調(diào)用其他組件
@Component
public class Impl implements Interface {
@Autowired
private InvokedClass ref;
}
Java Config
添加配置并聲明各接口調(diào)用依賴關(guān)系即可。
@Configuration
public class EatConfig {
@Bean
public Fruit fruit() {
return new Apple(System.out);
}
@Bean
public Person person() {
return new Man(fruit());
}
}
XML Config
構(gòu)造器注入
可以使用全稱<constructor-arg />或c-標(biāo)簽
<bean id="person" class="eat.Person">
<constructor-arg ref="fruit" /> <!-- bean引用注入 -->
</bean>
<bean id="fruit" class="eat.Fruit">
<constructor-arg value="#{T(System).out}" /> <!-- 字面量注入 -->
</bean>
<bean id="list" class="eat.Collection">
<constructor-arg>
<list> <!-- 集合注入 -->
<ref bean="one" />
<ref bean="two" />
<ref bean="three" />
</list>
</constructor-arg>
</bean>
<bean id="list" class="eat.Collection">
<constructor-arg>
<list> <!-- 集合注入 -->
<value>one</value>
<value>two</value>
<value>three</value>
</list>
</constructor-arg>
</bean>
作為一個通用規(guī)則,對強依賴使用構(gòu)造器注入,對弱依賴使用屬性注入。
屬性注入
<bean id="person" class="eat.Person">
<property name="fruit" ref="fruit" /> <!-- 引用注入 -->
</bean>
<bean id="fruit" class="eat.Fruit">
<property name="apple" value="I eating an apple" /> <!-- 字面量注入 -->
<property name="num">
<list> <!-- 集合注入 -->
<value>one</value>
<value>two</value>
<value>three</value>
</list>
</property>
</bean>
導(dǎo)入和混合配置
Java Config
@Configuration
@Import({OneConfig.class, TwoConfig.class})
@ImportResource("classpath:three-config.xml")
public class RootConfig {
}
XML Config
<bean class="package.OneConfig" />
<bean resource="three-config.xml" />
高級裝配
Profile
應(yīng)用程序在不同環(huán)境的遷移,如數(shù)據(jù)庫配置、加密算法以及與外部系統(tǒng)集成是否Mock是跨環(huán)境部署時會發(fā)生變化的幾個典型例子。
如果在XML配置文件中配置(Maven的profiles),在構(gòu)建階段確定將配置編譯部署,問題在于為每種環(huán)境重新構(gòu)建應(yīng)用,而Spring的profile是在運行時確定配置源。
定義profile
@Configuration
public class DataSourceConfig {
@Bean(destroyMethod = "shutdown")
@Profile("dev")
public DataSource embeddedDataSource() {
}
@Bean
@Profile("prod")
public DataSource jndiDataSource() {
}
}
<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="prod">
<jee:jndi-lookup id="dataSource"
lazy-init="true"
jndi-name="jdbc/myDatabase"
resource-ref="true"
proxy-interface="javax.sql.DataSource" />
</beans>
激活profile
有多種方式設(shè)置這兩個屬性,spring.profiles.active優(yōu)先spring.profiles.default。
- As initialization parameters on DispatcherServlet
- As context parameters of a web application
- As JNDI entries
- As environment variables
- As JVM system properties
- Using the @ActiveProfiles annotation on an integration test class
例如在Web應(yīng)用中,設(shè)置spring.profiles.default的web.xml文件
<web-app ...>
<!-- 為上下文設(shè)置默認(rèn)的profile -->
<context-param>
<param-name>spring.profiles.default</param-name>
<param-value>dev</param-value>
</context-param>
<!-- 為Servlet設(shè)置默認(rèn)的profile -->
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<init-param>
<param-name>spring.profiles.default</param-name>
<param-value>dev</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
</web-app>
Conditional beans
如果希望某個特定的bean創(chuàng)建后或環(huán)境變量設(shè)置后才會創(chuàng)建這個bean。Spring4引入了@Conditional注解。
@Configuration
public class MagicConfig {
@Bean
@Conditional(MagicExistsCondition.class)
public MagicBean magicBean() {
return new MagicBean();
}
}
設(shè)置給@Conditional的類需實現(xiàn)Condition接口,它會通過該接口進(jìn)行對比。
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
matches()返回true,會創(chuàng)建帶有@Conditional注解的類。否則反之。
PS: @Profile本身也使用了@Conditional注解,并引用ProfileCondition作為Condition實現(xiàn)??刹榭碨pring4以上源碼。
處理自動裝配歧義性
場景: 一個接口多個實現(xiàn)。
- 定義限定符
@Component //也可以是所有使用了@Component的注解,如: @Service、@Controller、@Ropository等
@Qualifier("one") //該注解可省略,默認(rèn)bean ID為首字母為小寫的實現(xiàn)類名字。
public class One implements Number {
...
}
- 使用限定符。
@Qualifier搭配@Autowired,@Autowired默認(rèn)按類型裝配
@Autowired
@Qualifier("one")
private Number num;
或者直接使用J2EE自帶的@Resource,默認(rèn)按名稱進(jìn)行裝配,減少了與spring的耦合(推薦)。
@Resource(name = "one")
private Number num;
bean的作用域
默認(rèn)情況下,Spring Application Context中所有bean都是以單例(singleton)創(chuàng)建的。也就是說,不管特定的bean被注入到其他bean多少次,每次注入的都是同一個實例。大多數(shù)情況下,單例bean是很理想的狀態(tài)。但有時候所用類是mutable,重用是不安全的。
Spring定義了四種作用域:
- 單例(singleton):在整個應(yīng)用中,只創(chuàng)建bean的一個實例。
- 原型(Prototype):每次注入或通過Spring應(yīng)用上下文獲取的時候,都會創(chuàng)建一個新的bean實例。
- 會話(Session):在Web應(yīng)用中,為每個會話創(chuàng)建一個bean實例。
- 請求(Request):在Web應(yīng)用中,為每次請求創(chuàng)建一個bean實例。
聲明bean為原型作用域
JavaConfig@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE);XMLConfigscope="prototype"。
聲明bean為會話和請求作用域
在Web應(yīng)用中,以購物車bean為例,單例和原型作用域就不適用,會話作用域是最合適的,因為它與特定用戶關(guān)聯(lián)性最大。
@Component
@Scope(
value=WebApplicationContext.SCOPE_SESSION,
proxyMode=ScopedProxyMode.INTERFACES)
public ShoppingCart cart() { ... }
在每個用戶購物完成后會調(diào)用保存訂單service,也就是會將會話級別的 ShoppingCart bean注入到單例級別的 StoreService bean中。proxyMode屬性解決了將會話或請求作用域的bean注入到單例bean的問題。

基于接口代理:proxyMode=ScopedProxyMode.INTERFACES or <aop:scoped-proxy proxy-target-class="false" />
基于實現(xiàn)類代理:proxyMode=ScopedProxyMode.TARGET_CLASS or <aop:scoped-proxy />
運行時值注入
避免將值硬編碼在配置類中,使其在運行時確定。
聲明屬性源并通過Spring的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")); //檢索屬性值
}
}
屬性占位符(Property placeholders)
為了使用占位符,需要配置一個 PropertySourcesPlaceholderConfigurer bean,它能夠基于Spring Environment及其屬性源來解析占位符。
@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
<beans>
<context:property-placeholder /> <!-- 自動生成PropertySourcesPlaceholderConfigurer -->
</beans>
使用解析參數(shù):
public BlankDisc(
@Value("${disc.title}") String title,
@Value("${disc.artist}") String artist) {
this.title = title;
this.artist = artist;
}
Spring表達(dá)式語言(The Spring Expression Language, SpEL)
SpEL表達(dá)式要放在"#{ ... }"之中。
- 引用bean、屬性和方法。例如:
#{sgtPeppers}、#{sgtPeppers.artist}、#{artistSelector.selectArtist()}、#{artistSelector.selectArtist().toUpperCase()} - 訪問Java類。'T()'運算符結(jié)果是一個class對象,能夠訪問目標(biāo)類型的靜態(tài)方法和常量。
#{T(System).currentTimeMillis()}、T(java.lang.Math).PI、T(java.lang.Math).random() - 對值進(jìn)行算術(shù)、關(guān)系和邏輯運算。
#{T(java.lang.Math).PI * circle.radius ^ 2}、#{disc.title + ' by ' + disc.artist}、#{scoreboard.score > 1000 ? "Winner!" : "Loser"} - 匹配正則表達(dá)式(Regular Expression, regex)。
matches運算符對String類型的文本(左邊參數(shù))應(yīng)用正則(右邊參數(shù))。#{admin.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.com'} - 計算集合。
#{jukebox.songs[4].title};查詢運算符".?[]"用來對集合進(jìn)行過濾得到集合子集,#{jukebox.songs.?[artist eq 'Aerosmith']};查詢第一個匹配項".^[]"和查詢最后一個匹配項".$[]";投影運算符".![]",如將title屬性投影到一個新的String類型集合中#{jukebox.songs.![title]}
PS: 盡可能讓表達(dá)式保持簡潔,不要讓表達(dá)式太智能復(fù)雜。
AOP
實現(xiàn)功能上的松耦合。把系統(tǒng)核心業(yè)務(wù)邏輯組件和額外功能如日志、事務(wù)管理和安全這樣的服務(wù)組件分離開來。
<aop:config>
<aop:aspect ref="asp">
<aop:pointcut id="pc"
expression="execution(* *.method(..))"/>
<aop:before pointcut-ref="pc"
method="doBeforePc"/>
<aop:after pointcut-ref="pc"
method="doAfterPc"/>
</aop:aspect>
</aop:config>
Spring容器(container)負(fù)責(zé)創(chuàng)建、裝配、配置并管理對象的整個生命周期,從生存到死亡。Spring容器分為兩種類型: BeanFactory(bean工廠)是最簡單的容器,提供基本的DI支持;Application Context(應(yīng)用上下文)基于BeanFactory構(gòu)建,提供應(yīng)用框架級別的服務(wù)。(推薦使用)
通知(Advice)
通知定義了切面是什么以及何時使用。Spring切面可以應(yīng)用5種類型的通知,并使用AspectJ注解來聲明通知方法:
- 前置通知-
@Before: The advice functionality takes place before the advised method is invoked. - 后置通知-
@After: The advice functionality takes place after the advised method completes, regardless of the outcome. - 返回通知-
@AfterReturning: The advice functionality takes place after the advised method successfully completes. - 異常通知-
@AfterThrowing: The advice functionality takes place after the advised method throws an exception. - 環(huán)繞通知-
@Around: The advice wraps the advised method, providing some functionality before and after the advised method is invoked.
切點(Pointcut)
切點定義在何處執(zhí)行動作。Spring AOP所支持的AspectJ切點指示器: args()、@args()、execution()、this()、target()、@target()、within()、@within()、@annotation。只有execution指示器是實際執(zhí)行匹配的,其他都是用來限制匹配的,所以execution指示器是編寫切點定義時最主要的指示器。


bean()指示器使用bean ID或name作為參數(shù)來限制切點只匹配特定的bean。execution(* concert.Performance.perform()) and bean('woodstock')。
基本流程
- 定義切面


XML Config:

- 啟用AspectJ注解的自動代理


創(chuàng)建環(huán)繞通知


通知中增加參數(shù)



切點表達(dá)式中的args(trackNumber)限定符表明傳遞給 playTrack() 方法的int類型參數(shù)也會傳遞到通知中去,參數(shù)的名稱trackNumber也與切點方法簽名中的參數(shù)相匹配,這樣就完成了從命名切點到通知方法的參數(shù)轉(zhuǎn)移。
通過注解引入新功能
不用直接修改對象或類的定義就能夠為對象或類增加新的方法。一種情況是設(shè)計上在原接口上增加通用方法對所有的實現(xiàn)并不適用,一種情況是使用第三方實現(xiàn)沒有源碼的時候。借助于AOP的引入功能,不必在設(shè)計上妥協(xié)或者侵入性地改變現(xiàn)有的實現(xiàn)。

@Aspect
public class EncoreableIntroducer {
@DeclareParents(value="concert.Performance+",
defaultImpl=DefaultEncoreable.class)
public static Encoreable encoreable;
}
<aop:aspect>
<aop:declare-parents
types-matching="concert.Performance+"
implement-interface="concert.Encoreable"
default-impl="concert.DefaultEncoreable"
/>
</aop:aspect>
通過@DeclareParents注解將新接口引入到現(xiàn)有的bean中。value屬性指定引入到哪個接口bean上,加號'+'表示該對象的子類型,而不是其本身;defaultImpl屬性指定了引入的功能;