《Spring in Action》讀書筆記和總結浓若。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)接口上的松耦合旨怠。為了關聯(lián)各接口之間的調(diào)用和依賴,Spring采用裝配bean方式蜈块。建立應用接口間依賴關系的行為稱為裝配(Wiring)鉴腻。
Spring應用上下文(Application Context)負責對象的創(chuàng)建和組裝。
Spring提供三種主要的裝配注入機制:
- 隱式的bean自動裝配機制疯趟;
- 顯示Java配置拘哨;
- 顯示XML配置。
自動化裝配bean
先定義需要的組件component信峻,其次開啟組件掃描(Component scanning),最后調(diào)用其他組件瓮床。Spring會自動發(fā)現(xiàn)應用上下文中創(chuàng)建的bean盹舞。
定義組件
@Component("beanName") //beanName不填寫产镐,默認為首字母小寫的類名
public class Impl implements Interface {
}
開啟組件掃描
@Configuration
@ComponentScan
public class AutoConfig {
}
默認以配置類所在的包作為基礎包(base package)掃描組件。@ComponentScan(basePackages="pkgName")
一個包踢步、@ComponentScan(basePackages={"pkgName1", "pkgName2"})
多個包癣亚、@ComponentScan(basePackageClasses={Interface1.class, Impl2.class})
類或接口所在的包作為組件掃描的基礎包。
<context:component-scan base-package="pkg" />
調(diào)用其他組件
@Component
public class Impl implements Interface {
@Autowired
private InvokedClass ref;
}
Java Config
添加配置并聲明各接口調(diào)用依賴關系即可获印。
@Configuration
public class EatConfig {
@Bean
public Fruit fruit() {
return new Apple(System.out);
}
@Bean
public Person person() {
return new Man(fruit());
}
}
XML Config
構造器注入
可以使用全稱<constructor-arg />
或c-
標簽
<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ī)則述雾,對強依賴使用構造器注入,對弱依賴使用屬性注入兼丰。
屬性注入
<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>
導入和混合配置
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
應用程序在不同環(huán)境的遷移玻孟,如數(shù)據(jù)庫配置、加密算法以及與外部系統(tǒng)集成是否Mock是跨環(huán)境部署時會發(fā)生變化的幾個典型例子鳍征。
如果在XML配置文件中配置(Maven的profiles)黍翎,在構建階段確定將配置編譯部署,問題在于為每種環(huán)境重新構建應用艳丛,而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
有多種方式設置這兩個屬性,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應用中碰酝,設置spring.profiles.default
的web.xml文件
<web-app ...>
<!-- 為上下文設置默認的profile -->
<context-param>
<param-name>spring.profiles.default</param-name>
<param-value>dev</param-value>
</context-param>
<!-- 為Servlet設置默認的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)境變量設置后才會創(chuàng)建這個bean。Spring4引入了@Conditional
注解戴差。
@Configuration
public class MagicConfig {
@Bean
@Conditional(MagicExistsCondition.class)
public MagicBean magicBean() {
return new MagicBean();
}
}
設置給@Conditional
的類需實現(xiàn)Condition
接口砰粹,它會通過該接口進行對比。
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
matches()
返回true造挽,會創(chuàng)建帶有@Conditional
注解的類碱璃。否則反之。
PS: @Profile
本身也使用了@Conditional
注解饭入,并引用ProfileCondition
作為Condition
實現(xiàn)嵌器。可查看Spring4以上源碼谐丢。
處理自動裝配歧義性
場景: 一個接口多個實現(xiàn)爽航。
- 定義限定符
@Component //也可以是所有使用了@Component的注解,如: @Service乾忱、@Controller讥珍、@Ropository等
@Qualifier("one") //該注解可省略,默認bean ID為首字母為小寫的實現(xiàn)類名字窄瘟。
public class One implements Number {
...
}
- 使用限定符衷佃。
@Qualifier
搭配@Autowired
,@Autowired默認按類型裝配
@Autowired
@Qualifier("one")
private Number num;
或者直接使用J2EE自帶的@Resource
蹄葱,默認按名稱進行裝配氏义,減少了與spring的耦合(推薦)锄列。
@Resource(name = "one")
private Number num;
bean的作用域
默認情況下,Spring Application Context中所有bean都是以單例(singleton)創(chuàng)建的惯悠。也就是說邻邮,不管特定的bean被注入到其他bean多少次,每次注入的都是同一個實例克婶。大多數(shù)情況下筒严,單例bean是很理想的狀態(tài)。但有時候所用類是mutable情萤,重用是不安全的鸭蛙。
Spring定義了四種作用域:
- 單例(singleton):在整個應用中,只創(chuàng)建bean的一個實例紫岩。
- 原型(Prototype):每次注入或通過Spring應用上下文獲取的時候规惰,都會創(chuàng)建一個新的bean實例。
- 會話(Session):在Web應用中歇万,為每個會話創(chuàng)建一個bean實例勋陪。
- 請求(Request):在Web應用中,為每次請求創(chuàng)建一個bean實例诅愚。
聲明bean為原型作用域
JavaConfig@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
;XMLConfigscope="prototype"
寒锚。
聲明bean為會話和請求作用域
在Web應用中违孝,以購物車bean為例,單例和原型作用域就不適用雌桑,會話作用域是最合適的喇喉,因為它與特定用戶關聯(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表達式語言(The Spring Expression Language, SpEL)
SpEL表達式要放在"#{ ... }"之中侣颂。
- 引用bean、屬性和方法藻肄。例如:
#{sgtPeppers}
拒担、#{sgtPeppers.artist}
从撼、#{artistSelector.selectArtist()}
、#{artistSelector.selectArtist().toUpperCase()}
- 訪問Java類婆翔。'T()'運算符結果是一個class對象啃奴,能夠訪問目標類型的靜態(tài)方法和常量气堕。
#{T(System).currentTimeMillis()}
、T(java.lang.Math).PI
揖膜、T(java.lang.Math).random()
- 對值進行算術壹粟、關系和邏輯運算。
#{T(java.lang.Math).PI * circle.radius ^ 2}
洪添、#{disc.title + ' by ' + disc.artist}
干奢、#{scoreboard.score > 1000 ? "Winner!" : "Loser"}
- 匹配正則表達式(Regular Expression, regex)盏袄。
matches
運算符對String類型的文本(左邊參數(shù))應用正則(右邊參數(shù))辕羽。#{admin.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.com'}
- 計算集合。
#{jukebox.songs[4].title}
绰寞;查詢運算符".?[]"用來對集合進行過濾得到集合子集滤钱,#{jukebox.songs.?[artist eq 'Aerosmith']}
枷踏;查詢第一個匹配項".^[]"和查詢最后一個匹配項".$[]";投影運算符".![]"停团,如將title屬性投影到一個新的String類型集合中#{jukebox.songs.![title]}
PS: 盡可能讓表達式保持簡潔佑稠,不要讓表達式太智能復雜旗芬。
AOP
實現(xiàn)功能上的松耦合疮丛。把系統(tǒng)核心業(yè)務邏輯組件和額外功能如日志、事務管理和安全這樣的服務組件分離開來履恩。
<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)負責創(chuàng)建切心、裝配、配置并管理對象的整個生命周期协屡,從生存到死亡肤晓。Spring容器分為兩種類型: BeanFactory(bean工廠)是最簡單的容器啼县,提供基本的DI支持季眷;Application Context(應用上下文)基于BeanFactory構建卷胯,提供應用框架級別的服務窑睁。(推薦使用)
通知(Advice)
通知定義了切面是什么以及何時使用。Spring切面可以應用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ù)
切點表達式中的args(trackNumber)限定符表明傳遞給 playTrack() 方法的int類型參數(shù)也會傳遞到通知中去颜说,參數(shù)的名稱trackNumber也與切點方法簽名中的參數(shù)相匹配员舵,這樣就完成了從命名切點到通知方法的參數(shù)轉(zhuǎn)移马僻。
通過注解引入新功能
不用直接修改對象或類的定義就能夠為對象或類增加新的方法。一種情況是設計上在原接口上增加通用方法對所有的實現(xiàn)并不適用措近,一種情況是使用第三方實現(xiàn)沒有源碼的時候女淑。借助于AOP的引入功能鸭你,不必在設計上妥協(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
屬性指定了引入的功能爷光;