?????上篇我們介紹了Spring中有關(guān)高級依賴關(guān)系配置的內(nèi)容,也可以調(diào)用任意方法的返回值作為屬性注入的值,它解決了Spring配置文件的動態(tài)性不足的缺點。而本篇,我們將介紹Spring的又一大核心思想,AOP,也就是面向切面編程。這是對面向?qū)ο缶幊痰囊粋€擴展,即便問世不長,但是已經(jīng)成為當(dāng)下最流行的編程思想之一。本篇主要涉及以下內(nèi)容:
- Spring中的后置處理器
- "零配置"實現(xiàn)Bean的配置
- Spring AOP
一、后置處理器
為了實現(xiàn)良好的擴展性,Spring允許我們擴展它的IOC容器,它提供了兩種后置處理器來支持我們對容器進行擴展。
- Bean后置處理器:該處理器會對容器中的bean進行增強
- 容器后置處理器:該處理器針對容器,對容器進行額外增強
1、Bean后置處理器
Bean后置處理器需要繼承接口BeanPostProcessor,并實現(xiàn)它的如下兩個方法:
- public Object postProcessBeforeInitialization(Object o, String s):在當(dāng)前bean實例初始化屬性注入的之前回調(diào)
- public Object postProcessAfterInitialization(Object o, String s):在當(dāng)前bean實例初始化屬性注入之后回調(diào)
當(dāng)整個容器在加載的時候,會掃描整個容器中的bean,如果發(fā)現(xiàn)有bean實現(xiàn)了接口BeanPostProcessor,那么就將該bean注冊為Bean后置處理器,一旦其他bean實例化完成之后,將逐個調(diào)用后置處理器進行bean實例的增強。
在這兩個方法中,傳入了同樣的兩個參數(shù),第一個參數(shù)表示即將被后處理的bean實例,第二個參數(shù)是該實例在容器中的 id屬性。postProcessBeforeInitialization方法指明在容器創(chuàng)建實例之后,但是在實際初始化屬性之前回調(diào),此處傳過來的bean實例是一個完整的實例,它包含還未實際初始化的屬性的值信息,該方法的返回值就是最終保存在容器中的實例。
postProcessAfterInitialization方法是類似的,它會在容器初始化屬性結(jié)束后回調(diào),在該方法中,我們也可以修改該bean的信息,最終返回的bean將作為容器中真實存在的bean,對應(yīng)于傳入的該bean的引用,相當(dāng)于修改了該bean實例。
我們簡單看個例子:
//定義兩個類,并定義兩個屬性name和age
//容器中配置兩個bean實例
<bean id="programmer" class="Test_spring.Programmer" p:name="single" p:age="22" />
<bean id="coder" class="Test_spring.Coder" p:name="walker" p:age="21" />
//定義Bean后置處理器
public class MyBeanProcess implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object o, String s) throws BeansException {
//屬性初始化之前
if(o instanceof Programmer){
Programmer programmer = (Programmer) o;
programmer.setAge(50);
}else if(o instanceof Coder) {
Coder coder = (Coder) o;
coder.setAge(60);
}
return o;
}
@Override
public Object postProcessAfterInitialization(Object o, String s) throws BeansException {
//屬性初始化之后
if(o instanceof Programmer){
Programmer programmer = (Programmer) o;
System.out.println(programmer.getName() + "," + programmer.getAge());
}else if(o instanceof Coder) {
Coder coder = (Coder) o;
System.out.println(coder.getName() + "," + coder.getAge());
}
return o;
}
}
//將該Bean后置處理器實例配置在容器中
<bean class="Test_spring.MyBeanProcess" />
我們看最終的輸出結(jié)果:
程序很簡單,在實際初始化屬性之前,我們分別修改兩個bean實例的age屬性,又在屬性初始化結(jié)束時,打印了他們的信息。Bean的后處理器一般就這么用,當(dāng)然此處的例子有點大材小用了,根據(jù)實際情況適時選擇使用即可。
2、容器后置處理器
Bean后置處理器負責(zé)增強處理所有的bean實例,而容器后置處理器則只負責(zé)處理容器本身。容器后置處理器必須實現(xiàn)接口 BeanFactoryPostProcessor,并實現(xiàn)其一個方法:
void postProcessBeanFactory(ConfigurableListableBeanFactory var1)
通過實現(xiàn)該方法體,我們可以做到對Spring容器進行擴展,通常來說,我們也很少自己去擴展Spring容器,畢竟難度有點大。我們會使用Spring為我們內(nèi)置的容器后處理器,例如:屬性占位符配置器。
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<!--多個屬性文件都可以一起進行讀取-->
<value>db.properties</value>
</list>
</property>
</bean>
<bean id="Dbcon" class="Test_spring.DbCon"
p:driverClass="${jdbc.driverClassName}"
p:conUrl="${jdbc.url}"
p:userName="${jdbc.username}"
p:pwd="${jdbc.password}"
/>
屬性文件:
//屬性文件名:db.properties
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql:///test
jdbc.username=root
jdbc.password=123456
上述代碼中,我們使用Spring為我們提供的一個屬性占位配置器,PropertyPlaceholderConfigurer。在實例化容器中的bean實例之前,容器會調(diào)用PropertyPlaceholderConfigurer容器后置處理器讀取指定的Properties文件并保存在Spring配置信息中。
于是,我們可以使用${....}來獲取被加載屬性文件中的內(nèi)容。
二、注解配置Bean實例
一直以來,我們都是使用的XML形式來配置我們的bean實例,但在潮流的推動下,大部分的Java框架也開始傾向于使用簡單的注解來配置我們的bean實例。Spring也已經(jīng)完全支持注解配置了。那么本小節(jié)就將學(xué)習(xí)下使用注解對bean實例的配置。
1、標(biāo)識Bean類
在XML中,一個bean元素代表一個bean實例,它的class屬性指定了它的類型,id指定了它的名稱。而在我們注解中,使用以下幾種注解來標(biāo)識Bean類:
- @Component:標(biāo)識一個普通Bean
- @Controller:標(biāo)識一個控制器組件
- @Service:標(biāo)識一個業(yè)務(wù)組件
- @Repository:標(biāo)識一個DAO組件
我們這里主要使用注解@Component來標(biāo)識Bean類,其他的三種類型的注解在整合第三方框架的時候再做詳細介紹。例如:
@Component("teacher")
public class Teacher {
private String name;
private int age;
//省略setter方法
}
上述代碼等效于以下的XML配置:
<bean id="teacher" class="Test_spring.Teacher" />
當(dāng)然,如果想要Spring的注解生效,還需要在XML中配置掃描器:
<!--配置注解掃描器-->
<context:component-scan base-package="Test_spring" />
base-package屬性告訴Spring容器,應(yīng)該從哪個包開始掃描所有被Spring注解修飾的類。這樣我們就可以在容器外通過getBean獲取到該實例了。
Teacher teacher = (Teacher) context.getBean("teacher");
2、指定Bean實例的作用域
XML中指定bean作用域通常使用scope屬性來指定,例如:
<bean id="coder" class="Test_spring.Coder" scope="singleton" />
在Spring注解中,指定bean實例的作用域相對簡單很多。例如:
@Scope("singleton")
@Component(value = "teacher")
public class Teacher {
........
}
直接使用@Scope注解進行作用域指定即可。
3、配置屬性依賴
Spring中,我們使用注解@Resources來給屬性注入依賴。例如:
//容器中配置coder的bean實例
<bean id="coder" class="Test_spring.Coder" p:name="single" p:age="22"/>
@Component(value = "teacher")
public class Teacher {
private String name;
private int age;
private Coder coder;
@Resource(name = "coder")
public void setCoder(Coder coder) {
this.coder = coder;
}
//省略其他setter方法
}
@Resource的name屬性指向的容器中已經(jīng)配置的bean實例id,它實現(xiàn)了和ref一樣的語義,需要特別注意的是,@Resource注解是修飾在setter方法上的,而非直接修飾實例屬性。
4、管理Bean實例的生命周期
在XML中,我們使用init-method和destory-method來管理bean的兩個特殊時間點。當(dāng)然,使用注解的話會清晰很多。例如:
@Component(value = "teacher")
public class Teacher {
private String name;
private int age;
//省略setter方法
@PostConstruct
public void init(){
System.out.println("initialize all properties...");
}
@PreDestroy
public void destory(){
System.out.println("destory this bean ....");
}
}
我們使注解@PostConstruct標(biāo)識初始化屬性之后回調(diào)的方法,使用注解@PreDestroy指定在bean實例銷毀之前回調(diào)的方法。
常用的注解基本上就是這幾個,還有一些例如@Autowire、@Qualifier用于指定依賴的自動裝配和精準(zhǔn)自動裝配的注解,由于本身使用場景有限,具體用到的時候,可以再次學(xué)習(xí)。
三、Spring AOP
AOP(Aspect Orient Programming),面向切面編程。它作為面向?qū)ο缶幊趟季S的一種延伸,逐漸成為了一種成熟的編程思維。AspectJ是當(dāng)下對AOP思想實現(xiàn)情況中最優(yōu)秀的框架,它提供了強大的AOP功能,有著自己的編譯器和織入器。目前而言,Spring有著自己的AOP實現(xiàn),底層是基于動態(tài)代理,但是也逐漸向AspectJ的規(guī)范靠近,并且在整個Spring AOP中使用的注解全部依賴AspectJ的注解,也就是說一旦哪天Spring 底層修改了AOP的實現(xiàn),采用AspectJ做實現(xiàn)的話,并不影響上層注解的使用。既然是基于動態(tài)代理的,那我們先簡單回顧下動態(tài)代理的內(nèi)容,詳細的內(nèi)容在以前的文章中已經(jīng)做過介紹,讀者可以返回去查看。
//定義一個接口類型
public interface Person {
void programming();
}
//提供一個該接口的實現(xiàn)類
public class Programmer implements Person{
private String name;
public Programmer(String name){
this.name = name;
}
@Override
public void programming(){
System.out.println("my name is :" + this.name);
}
}
//自定義一個處理器
public class MyHander implements InvocationHandler {
private Object target;
public MyHander(Object o){
this.target = o;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("i am programming");
method.invoke(target,args);
System.out.println("programming finished");
return null;
}
}
//生成動態(tài)代理
Programmer programmer = new Programmer("single");
MyHander hander = new MyHander(programmer);
Class proxy = Proxy.getProxyClass(Programmer.class.getClassLoader(),new Class[]{Person.class});
Constructor constructor = proxy.getConstructor(new Class[]{InvocationHandler.class});
Person person = (Person) constructor.newInstance(hander);
person.programming();
程序首先創(chuàng)建一個Programmer 的實例,這也是我們即將代理的實例對象。然后定義了一個處理器并將當(dāng)前代理對象實例傳入,接著通過getProxyClass傳入代理類的類加載器以及該類所有的接口即可,該方法將負責(zé)默認實現(xiàn)所有接口中的方法并返回一個代理類型,最后我們通過反射創(chuàng)建一個代理類的實例并完成方法的調(diào)用。
程序輸出結(jié)果如下:
環(huán)繞著programming方法的前后,我們打印了日志信息。這是一個典型的基于jdk的動態(tài)代理的實現(xiàn)。
而我們的AOP大致也就是做這樣類似的事情,在一個方法調(diào)用之前或者之后增加一些額外的處理,具體的我們先看看幾個有關(guān)AOP的概念:
- 切面:為目標(biāo)對象增加的每一個模塊叫做一個切面
- 連接點:程序執(zhí)行過程中確切的點,方法調(diào)用前,方法調(diào)用后或者異常出現(xiàn)點
- 通知:切面中的每一個方法叫做一個通知或者一次增強處理
- 切入點:可以插入增強處理的連接點叫做切入點
根據(jù)通知的類型不同,我們大致可以分為以下幾類:
- before增強通知
- after增強通知
- around增強通知
- AfterReturning增強通知
- AfterThrowing增強通知
1、before增強通知處理
我們說過,Spring AOP完全依賴的AspectJ的注解,所以在使用AOP之前,我們需要引入相應(yīng)的jar包:
- com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
- com.springsource.org.aopalliance-1.0.0.jar
- spring-aspects-4.0.0.RELEASE.jar
- spring-aop-4.0.0.RELEASE.jar
before增強通知處理是在目標(biāo)方法執(zhí)行之前,進行額外加強。例如:
//定義一個普通bean實例
@Component("runner")
public class Runner {
private String name ="single";
//省略setter方法
public void run(){
System.out.println("my name is:"+ this.name+" and i am running");
}
}
@Aspect
@Component
public class MyAspect {
@Before("execution(* Test_SpringAop.*.*(..))")
public void prepareForRun(){
System.out.println("before running, you need to do some excercises!");
}
}
創(chuàng)建一個普通的Java類,并通過@Aspect將其申明為一個切面,@Component將其注冊到容器中。
//在容器中增加對aspectj注解的支持
<aop:aspectj-autoproxy/>
于是,我們向容器索取runner實例,并調(diào)用它的run方法,結(jié)果如下:
顯然,我們從容器中獲取的runner的bean實例已經(jīng)不再是普通的bean了,而是它的一個代理對象。
再回顧下整個過程,
當(dāng)容器加載的時候,掃描所有配置容器中的bean,一旦發(fā)現(xiàn)有被注解@Aspect修飾的,則注冊它為一個切面,掃描其中的方法或者叫通知,根據(jù)配置在這些方法之前的注解類型判斷當(dāng)前切面需要操作的目標(biāo)對象集。例如,我們上述使用@Before注解指定該切面對Test_SpringAop包下的所有方法進行加強處理,怎么處理呢?Before指定在該調(diào)用之前,調(diào)用當(dāng)前的通知方法(prepareForRun),其實也就是生成一個動態(tài)代理的過程。于是容器中的bean都是被加強后代理實例。
整個過程對目標(biāo)對象中的方法未做一點污染,神不知,鬼不覺的增強了指定實例的指定方法。這就是AOP的核心思想,也是它的魅力所在。
@Before類型的通知類比于我們的動態(tài)代理,InvocationHandler處理器中invoke方法中,method.invoke是調(diào)用目標(biāo)的原方法,而@Before則指明在調(diào)用該目標(biāo)方法之前,插入我們指定的方法。
2、after增強通知處理
和before類似,after是后置處理,它指定在目標(biāo)方法調(diào)用之后插入我們指定的方法,由于與before極其類似,此處不再贅述。
3、afterReturning增強通知處理
afterReturning相較于after來說,它更傾向于處理有返回值的目標(biāo)方法,就是說通過使用afterReturning,我們可以獲取到剛剛執(zhí)行結(jié)束的方法的返回值。例如:
//修改runner的run方法,讓他返回name
public String run(){
System.out.println("my name is:"+ this.name+" and i am running");
return this.name;
}
//為切面類增加兩個通知增強器
@Before("execution(* Test_SpringAop.*.*(..))")
public void prepareForRun(){
System.out.println("before running, you need to do some excercises!");
}
@AfterReturning(returning = "result",value = "execution(* Test_SpringAop.*.*(..))")
public void afterForRun(Object result){
System.out.println("runner : "+ result + " is running");
}
先看結(jié)果:
AfterReturning指定的afterForRun(Object result)方法將在目標(biāo)方法調(diào)用之后被調(diào)用,注解中的returning 屬性的值指向目標(biāo)方法的返回值,于是我們可以在afterForRun方法中獲取原方法的返回值。
4、afterThrowing增強通知處理
afterThrowing主要用于處理目標(biāo)方法中未被處理的異常。例如:
//目標(biāo)方法中有一個未檢查異常
public void doMath(){
int result = 12/0;
}
//切面中定義異常處理通知
@AfterThrowing(throwing = "ex",value = "execution(* Test_SpringAop.*.*(..))")
public void catchEx(Throwable ex){
System.out.println("目標(biāo)方法出現(xiàn)異常:" + ex.getMessage());
}
當(dāng)我們外部調(diào)用doMath方法的時候,該未處理的異常信息將會被捕獲并輸出。
顯然,這種增強器雖然能捕獲到該異常,但是并不能對它做任何處理,程序依然拋出異常信息。它和try catch不同,被catch住的異常如果不手動拋出的話,程序是可以正常結(jié)束的。
5、around增強通知處理
Around類型的增強處理功能比較全,近乎是Before和AfterReturning兩者合起來的功能,但是它也有額外的增強處理,它可以控制目標(biāo)方法是否被執(zhí)行,選擇給目標(biāo)方法傳入什么形參等等。權(quán)限比較大,但是要求線程安全,所以一般建議盡量減少使用around的次數(shù)??磦€例子:
@Around("execution(* Test_SpringAop.*.*(..))")
public void useAround(ProceedingJoinPoint point) throws Throwable {
System.out.println("i meet single");
point.proceed(new Object[]{"single"});
System.out.println("we leave ....");
}
被@Around修飾增強處理方法中,必須傳入一個ProceedingJoinPoint 類型的參數(shù),該參數(shù)代表了目標(biāo)方法,調(diào)用它的proceed方法可以控制執(zhí)行該目標(biāo)方法,并傳入?yún)?shù)。
所以說,Around其實模擬的是我們整個動態(tài)代理的過程,在這里我們可以選擇調(diào)用proceed方法與否來控制是否調(diào)用目標(biāo)方法,也可以選擇傳入?yún)?shù)與否來控制是否覆蓋原方法的參數(shù)值。
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
Runner runner = (Runner)context.getBean("runner");
runner.sayHello("walker");
我們在main函數(shù)中調(diào)用容器為我們生成的代理對象,調(diào)用他的sayhello方法并傳入?yún)?shù)Walker,但是實際輸出結(jié)果如下:
實際上整個目標(biāo)方法的調(diào)用與否,參數(shù)修改與否就完全交到切面中完成了,這樣的程序靈活性很高。
以上簡單介紹了增強處理的幾種不同類型,除此之外,Spring還允許我們在每次增強方法調(diào)用時獲取到連接點的信息。通過向方法傳入JoinPoint類型的形參即可對目標(biāo)方法的相關(guān)信息進行獲取。該類型中有以下幾個方法:
- Object[] getArgs();:獲取目標(biāo)方法的所有參數(shù)
- Signature getSignature();:獲取目標(biāo)方法的簽名
- Object getTarget();:獲取目標(biāo)對象
@Before("execution(* Test_SpringAop.*.*(..))")
public void prepareForRun(JoinPoint point){
System.out.println(point.getSignature());
Object[] args = point.getArgs();
Object obj = point.getTarget();
}
在某些情況下,這些信息還是很有作用的。
最后我們了解下,切入點表達式,也就是上述代碼中一直使用的:
"execution(* Test_SpringAop.*.*(..))"
這個叫做切入點表達式,用于定位具體的切入點。完整的AspectJ中具有大量的切入點指示符,但是Spring AOP只支持其中的部分。我們主要看其中最重要也是最常用的:execution。該指示符使用時最標(biāo)準(zhǔn)的格式為:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?.name-pattern(param-pattern) throws-pattern?)
其中:
- modifiers-pattern:指定方法的修飾符,可以省略
- ret-type-pattern:指定方法的返回值類型,"*"表示匹配所有的返回值類型
- declaring-type-pattern:指定方法所屬的類,可以省略
- name-pattern:方法名稱,"*"表示匹配所有的方法
- param-pattern:該方法的所有形參列表,"*"表示一個任意類型的參數(shù),".."表示零個或者多個任意類型的參數(shù)。
- throws-pattern:指定方法聲明的異常類型
接下來我們解析下我們上述一直在使用的切入點表達式:
"execution(* Test_SpringAop.*.*(..))"
* 匹配任意的返回值類型
Test_SpringAop.* 匹配Test_SpringAop包下的任意類
Test_SpringAop.*.*(..) 匹配了Test_SpringAop包下的任意類的任意方法,并且該方法具有零個或多個任意類型的參數(shù)
當(dāng)然,我們也可以具體到某個方法:
@Before("execution(public void Test_SpringAop.Runner.run(String,int))")
至此,有關(guān)Spring中AOP的基本知識已經(jīng)介紹完了,使用XML配置Spring AOP的方法也是類似的,此處不再贅述??偨Y(jié)不到之處,望指出!