Spring 深入淺出核心技術(二)



Spring基於註解方式裝配Bean

基於註解方式裝配Bean

Spring從2.0開始引入基於註解的配置方式,並且不斷的進行完善。通過註解的方式可以直接在類上定義Bean的信息,非常方便。

@Component註解來對類進行標註,它可以被Spring容器識別,Spring容器將自動將類轉換爲容器管理的Bean。

        //使用註解之前,我們要先導入aop的jar包
        //使用@Component註解定義Bean ,和<bean id="user" class="com.cad.domain.User">是等效的。
        @Component("user")
        public class User {
            private String name;
            private int age;


            public void setName(String name) {
                this.name = name;
            }


            public void setAge(int age) {
                this.age = age;
            }

            public void say(){
                System.out.println(name+":"+age);
            }

        } 

僅僅在類上定義了註解是不夠的,Spring提供了組件掃描,來進行對指定包進行掃描,對擁有註解的類進行實例化等操作。


    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        <!--聲明context命名空間-->
        xmlns:context="http://www.springframework.org/schema/context"  
        xsi:schemaLocation="
            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd 
            <!--指定xsd約束位置-->
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">


    <!--組件掃描:Spring容器會掃描這個包裏所有類,從類的註解信息中獲取Bean的信息-->
    <context:component-scan base-package="com.cad.domain"></context:component-scan>  
    </beans>  

我們進行測試一下,看Bean是否被放入容器中

    public class Test { 
        @org.junit.Test
        public void test(){
            ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
            User user=(User) ac.getBean("user"); 
            user.setName("Tizzy");
            user.setAge(18);
            user.say(); 
        }
    }

這裏寫圖片描述


除了@Component外,Spring提供了三個功能和@Component等效的註解。
它們一般用於web項目,對DAO,service,web層進行註解,所以也稱爲Bean的衍生註解。

  • @Repository:對DAO實現類進行註解

  • @Service:對service實現類進行註解

  • @Controller:對web層Controller實現類進行註解

之所以提供這三個特殊的註解,是爲了讓註解類本身的用途清晰化,此外,Spring還賦予了一些特殊的功能。我們在項目開發中應該儘量使用這種形式

基於註解方式注入屬性

Spring通過@Autowired註解實現Bean的自動依賴注入,會默認根據Bean的類型進行注入。

我們使用小例子來演示一下。

    WEB層 

        @Controller("useraction")
        public class UserAction {
            @Autowired                     //會根據類型自動注入
            private UserService userservice;

            public void say(){
                userservice.say();
            }
        }  
    service層,實現UserService接口

        @Service("userservice1")
        public class UserServiceImpl implements UserService {

            public void say() {
                System.out.println("service");

            }

        } 
    配置文件,配置組件掃描 
        <context:component-scan base-package="com.cad.example"></context:component-scan> 
    測試一下
            public class Test { 
                @org.junit.Test
                public void test(){
                    ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
                    UserAction user=(UserAction) ac.getBean("useraction"); 
                    user.say();
                }   
            }  


    輸出:service。

問題:@Autowired默認按類型匹配的方式,在容器中查找匹配的Bean,當有且只有一個匹配的Bean時,Spring將其注入到@Autowired註解的變量中。但是如果容器中有超過一個以上的匹配Bean時,例如有兩個UserService類型的Bean,這時就不知道將哪個Bean注入到變量中,就會出現異常。

例如上面例子,我們再創建一個UserService接口的實現類

        @Service("userservice2")
        public class otherUserService implements UserService {

            public void say() {
                System.out.println("service222");
            }
        }

這時我們再運行就會出現異常,爲了解決這個問題,Spring可以通過@Qualifier註解來注入指定Bean的名稱


        @Controller("useraction")
        public class UserAction {
            @Autowired 
            //指定指定Bean的名稱
            @Qualifier("userservice2")
            private UserService userservice;

            public void say(){
                userservice.say();
            }

        } 

    這時候otherUserService就被注入
    輸出結果service222 

@Autowired可以對類成員變量的set方進行註解。

            @Service("userservice1")
            public class UserServiceImpl implements UserService {
                private UserDao userdao; 
                //對set方法使用註解,UserDao的實例就會被注入進來
                @Autowired
                public void  setUserdao(UserDao userdao){
                    this.userdao=userdao;
                }
                public void say() {
                    userdao.add();
                }
            } 
    dao層
            @Repository
            public class UserDao {
                public void add(){
                    System.out.println("dao add.....");
                }
            }

註解方式配置Bean的作用範圍和生命過程方法

通過註解配置的Bean和通過< bean >配置的Bean一樣,默認的作用範圍都是singleton,Spring爲註解配置提供了一個@Scope的註解,顯式指定Bean的作用範圍。

            @Controller("user") 
            //指定作用範圍爲多例prototype
            @Scope("prototype")
            public class User {

                public void say(){
                    System.out.println("hello.word");
                }

            }    

Spring定義的@PostConstruct和@PreDestroy兩個註解相當於bean的init-method和destory-method屬性的功能

            @Controller("user")
            @Scope("prototype")
            public class User {
                public void say(){
                    System.out.println("hello.word");
                }

                @PostConstruct
                public void myinit(){
                    System.out.println("初始化....");
                }

                @PreDestroy
                public void mydestory(){
                    System.out.println("銷燬中....");
                }

            }

整合多個Spring配置文件

對於一個大型項目而言,可能有多個XML配置文件,在啓動Spring容器時,可以通過一個String數組指定這些配置文件。Spring還允許我們通過< import >標籤將多個配置文件引入到一個文件中,進行配置文件的集成,這樣啓動Spring容器時,就僅需指定這個合併好的配置文件即可。

  • 第一種方式,使用String數組指定所有配置文件
ApplicationContext ac=new ClassPathXmlApplicationContext(new String[]{"bean1.xml","bean2.xml"});
  • 第二種方式,使用import標籤
 //resource屬性指定配置文件位置,支持Spring標準的資源路徑
 <import resource="classthpath:com/cad/domain/bean1.xml"/>
 <import resource="classthpath:com/cad/domain/bean2.xml"/>

第一種方式並不容易維護,我們在開發中推薦使用第二種方式。

Spring AOP

AOP爲Aspect Oriented Programming的縮寫,意爲:面向切面編程,通過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。面向切面編程這個概念一直被很多人詬病,因爲它和IoC一樣晦澀,不太容易理解。

AOP到底是什麼

我們先來看看以前的OOP(面向對象編程)解決的問題。按照軟件重構思想的理念,如果多個類中出現了相同的代碼,應該考慮定義一個共同的抽象類,將這些代碼提取到抽象類中。
例如dog,cat這些動物對象都應該有eat()、run()等功能,所以我們通過引入包含這兩個方法抽象的Animal父類,然後dog、cat這些類通過繼承複用eat()、run()等功能。我們大多數情況通過引入父類來消除多個類中重複的代碼,但有的時候並沒這麼簡單,我們看下面的例子。

    public class Dog implements Animal {

        public void run() {
            System.out.println("主人發出命令");
            System.out.println("跑");
            System.out.println("主人給予獎勵");
        }

        public void eat() {
            System.out.println("主人發出命令");
            System.out.println("吃");
            System.out.println("主人給予獎勵");

        }

    }

仔細看上面的代碼,我們會發現run()和eat()方法中有很大一部分重複代碼,這些代碼無法完全提取出來,這些情況會發生在很多地方,如性能監測、訪問控制、事務管理及日誌記錄等。

假設我們將Dog類看成一顆圓木,eat()和run()方法堪稱圓木上的一截,我們會發現那些重複代碼(事務開啓和關閉、主人發出命令和給予獎勵)就像一圈年輪,真正的業務代碼是樹心,這就是這些重複代碼被稱爲橫切代碼概念的由來。

我們無法通過抽象父類的方式消除以上所述的重複性橫切代碼,因爲這些橫切代碼依附在具體的業務方法中。AOP獨闢蹊徑通過橫向抽取機制爲這類無法通過縱向繼承的重複性代碼提供瞭解決方案,AOP希望將這些分散在業務邏輯中的相同代碼,通過橫向切割的方式抽取到一個獨立模塊中,我們知道將這些重複性的橫切代碼提取出來是很容易的,但如何將這些獨立的模塊融合到業務邏輯中完成和以前一樣的操作,這纔是關鍵,也是AOP解決的主要問題。

AOP術語

  • 連接點(Joinpoint)程序執行的某個特定位置:如類初始化前,類初始化後,某個方法調用前,方法調用後,方法拋出異常後。一個類或一段代碼擁有一些具有邊界性質的特定點,就被稱爲 “連接點”。Spring僅支持方法的連接點,僅能在方法調用前後、方法拋出異常時這些連接點織入增強。

  • 切點(Pointcut)每個程序都擁有很多連接點,如有一個兩個方法的類,這兩個方法都是連接點。如何定位到某個連接點上呢?AOP通過“切點”定位特定的連接點。例如:數據庫記錄是連接點,切點就是查詢條件,用來定位特定的連接點。在Spring中,切點通過Pointcut接口描述,使用類和方法作爲連接點的查詢條件,Spring AOP的解析引擎負責解析切點設定的條件,找到對應的連接點。連接點是方法執行前後等具體程序執行點,而切點只定位到某個方法上,所以如果定位到具體的連接點,還需要提供方位信息,即方法執行前還是執行後。

  • 增強/通知(Advice)增強是織入到目標類連接點上的一段程序代碼。在Spring中,增強除了描述一段程序代碼外,還擁有另一個和連接點相關的信息,就是執行點的方位。結合方位信息和切點信息,就可以找到特定的連接點。Spring提供的增強接口都是帶方位名的:BeforeAdvice,AfterRetuningAdvice,ThrowsAdvice等。所以只有結合切點和增強,才能確定特定的連接點並織入增強代碼。

  • 目標對象(Target)需要織入增強代碼的目標類。

  • 引介(Introduction)引介是一種特殊的增強,它爲類添加一些屬性和方法。這樣,即使某個類沒有實現某個接口,通過AOP的引介功能,我們可以動態的爲該類添加接口的實現邏輯,讓該類成爲這個接口的實現類。

  • 織入(Weaving)織入是將增強添加到目標類上具體連接點的過程。

  • 代理(Proxy)一個類被AOP織入增強後,就產生了一個結果類,它是融合了原類和增強邏輯的代理類。

  • 切面(Aspect)切面由切點和增強組成,既包括了橫切邏輯的定義,也包括了連接點的定義,Spring AOP就是負責實施切面,將定義的橫切邏輯織入到切面指定的連接點中。

AOP基礎知識

AOP的工作重心在於如何將增強應用於目標對象的連接點上,包括兩個工作。

  • 第一:如何通過切點和增強定位到連接點

  • 第二,如何在增強中編寫切面的代碼。

Spring AOP使用動態代理技術在運行期織入增強的代碼。Spring AOP使用了兩種代理機制,一種是基於JDK的動態代理;另一種是基於CGLib的動態代理,之所以使用兩種代理機制,很大程度上是因爲JDK本身只提供接口的代理,不支持類的代理。

JDK動態代理

我們以前學習過JDK動態代理,這裏我們來演示一個前面的例子。

    //動物接口
    public interface Animal {
        public void run();
        public void eat();

    }
    //狗的實現類
    public class Dog implements Animal {

        public void run() {
            System.out.println("跑");
        }

        public void eat() {
            System.out.println("吃");
        }

    }
    //現在我們需要在run()和eat()方法前後加一些動作,這些動作是相同的,所以我們提取出來 
    public class MyAspect {
        public void before(){
            System.out.println("主人發出命令");
        }

        public void after(){
            System.out.println("主人給予獎勵");
        }
    }
    //現在我們需要使用動態代理將增強邏輯和方法編織在一起 
    public class MyProxyFactory {
        public  static Animal createDog(){
            final Animal dog=new Dog();
            final MyAspect aspect=new MyAspect(); 
            //生成代理對象
            Animal dogproxy=(Animal)  Proxy.newProxyInstance(MyProxyFactory.class.getClassLoader(), dog.getClass().getInterfaces(), new InvocationHandler() {

                @Override
                public Object invoke(Object proxy, Method method, Object[] arg2) throws Throwable {
                    aspect.before();
                    Object obj=method.invoke(dog, arg2); 
                    aspect.after();
                    return obj;
                }
            });
            return dogproxy;
        }

    }
    //我們來進行測試
    public class TestJDK {
        @Test
        public void test(){
            Animal animal=MyProxyFactory.createDog();
            animal.eat();
        }

    }

這裏寫圖片描述

我們發現程序的運行結果和直接在方法裏面編寫重複代碼的效果是一樣的,但是重複代碼已經被我們提取到了某個類中。

CGLib動態代理

我們以前講過,使用JDK動態代理有一個很大的限制,就是隻能爲接口創建代理實例。
對於沒有通過接口定義的類,如何動態創建代理實例呢?CGLib填補了這麼空缺。

CGLib採用非常底層的字節碼技術,可以爲一個類創建子類,並在子類中採用方法攔截的技術攔截所有父類方法的調用,並織入橫切邏輯。但是由於CGLib採用動態創建子類的方式生成代理對象,所以不能對目標類中的private、final等方法進行代理

CGLib使用需要導入cglib.jar核心包和asm.jar依賴包,但是Spring 的Core.jar核心包中已經包含了這兩個jar包,所以我們直接使用即可。

我們還使用上面的小例子,不過要稍作一些修改。

    //我們還實現一個Dog類,但是不需要實現任何接口
    public class Dog {

        public void run() {
            System.out.println("跑");
        }

        public void eat() {
            System.out.println("吃");
        }

    }
    //把橫切邏輯代碼提取出來的類
    public class MyAspect {
        public void before(){
            System.out.println("主人發出命令");
        }

        public void after(){
            System.out.println("主人給予獎勵");
        }
    }
//我們使用CGLib生成代理對象
public class CglibProxy {

    public static Dog createDog(){  
        //CGLib核心類
        Enhancer enhancer=new Enhancer();
        Dog dog=new Dog();
        MyAspect aspect=new MyAspect();

        //設置父類,因爲CGLib底層是生成需要被代理類的子類
        enhancer.setSuperclass(dog.getClass()); 
        //設置回調方法,和InvocationHandler起到的效果一樣
        enhancer.setCallback(new org.springframework.cglib.proxy.MethodInterceptor() {

            //intercept方法會攔截所有目標類方法的調用
            //proxy表示目標類的實例,method爲目標類方法的反射對象,arg2是方法的參數,arg3是代理類實例
            @Override
            public Object intercept(Object proxy, Method method, Object[] arg2, MethodProxy arg3) throws Throwable {
                aspect.before();
                Object obj=method.invoke(dog, arg2);
                aspect.after();
                return obj;
            }
        });
        //創建代理對象
        Dog dogproxy=(Dog) enhancer.create();
        return dogproxy;
    }

}
    //我們進行測試一下 

    public class TestCGLib {
        @Test
        public void test(){
            Dog dog=CglibProxy.createDog();
            dog.run();
        }

    }

這裏寫圖片描述

代理出現的一些問題

Spring AOP的底層就是通過使用JDK動態代理或者CGLib動態代理技術爲目標Bean織入橫切邏輯。

我們雖然通過JDK動態代理和CGLib完成了橫切邏輯的動態織入,但我們的方式存在很多需要改進的地方:

  1. 目標類的所有方法都添加了橫切邏輯,而有時,我們只想對某些方法添加橫切邏輯。
  2. 我們通過硬編碼的方式指定了織入橫切邏輯的連接點,顯然不利於修改和維護。
  3. 我們純手工的編寫創建代理的過程,過程繁瑣,爲不同類創建代理時,需要分別編寫不同的代碼,無法做到通用。

Spring AOP爲我們完全解決了上述的問題,Spring AOP通過切點指定在哪些類的哪些方法上織入橫切邏輯,通過增強描述橫切邏輯代碼和方法的具體織入點。Spring通過切面將切點和增強組裝起來,通過切面的信息,就可以利用JDK或者CGLib的動態代理技術採用統一通用的方式爲Bean創建代理對象。

Spring中增強類型

Spring使用增強類定義橫切邏輯,同時由於Spring只支持方法連接點,增強還包括了在方法的哪一點織入橫切代碼的方位信息,所以增強既包括橫切邏輯,還包括連接點的部分信息。

AOP聯盟爲增強定義了Advice接口,Spring支持五種類型的增強。

  • 前置增強:org.springframework.aop.BeforeAdvice 代表前置增強,因爲Spring目前只支持方法級的增強,所以MethodBeforeAdvice是目前可用的前置增強,表示在目標方法執行前實施置增強,而BeforeAdvice是爲了將來擴展而定義的。

  • 後置增強:org.springframework.aop.AfterReturningAdvice 代表後置增強,表示在目標方法執行後實施增強。

  • 環繞增強:org.aopalliance.intercept.MethodInterceptor 代表環繞增強,表示在目標方法的執行前後實施增強。

  • 異常拋出增強:org.springframework.aop.ThrowsAdvice 代表拋出異常增強,表示在目標方法拋出異常後實施增強。

  • 引介增強:org.springframework.aop.IntroductionInterceptor 代表引介增強,表示在目標類中添加一些新的方法和屬性。

這些增強接口都有一些方法,通過實現這些接口的方法,在接口方法中定義橫切邏輯,就可以將他們織入到目標類方法的特定連接點

Spring中前置增強

我們還使用我們的狗的例子,即簡單又生動。我們的狗只有很單一的功能,我們需要在功能執行前增加一些功能。

    //定義動物接口 
    public interface Animal {
        public void run();
        public void eat();

    }
    //狗的實現類 
    public class Dog implements Animal{

        public void run() {
            System.out.println("跑");
        }

        public void eat() {
            System.out.println("吃");
        }

    }
    //我們使實現方法前置增強接口,並寫入我們的橫切邏輯代碼
    public class MyBeforeAdvice implements MethodBeforeAdvice {

        //method爲目標類的方法,args爲目標類方法的所需參數,obj爲目標類實例
        public void before(Method method, Object[] args, Object obj) throws Throwable {
            System.out.println("主人發出命令");

        }

    }
    //我們測試一下
    public class Test {
        @Test
        public void test(){
            Animal dog=new Dog(); 
            BeforeAdvice advice=new MyBeforeAdvice();  
            //Spring提供的代理工廠
            ProxyFactory pf=new ProxyFactory(); 
            //設置代理目標 
            pf.setTarget(dog);
            //爲代理目標添加增強
            pf.addAdvice(advice);

            //生成代理對象
            Animal proxy=(Animal)pf.getProxy();
            proxy.run();
        }

    }

這裏寫圖片描述

解析ProxyFactory

在上面的測試中,我們使用ProxyFactory代理工廠將增強Advice織入到目標類Dog中,ProxyFactory內部就是使用JDK動態代理或者CGLib動態代理技術。

Spring中定義了AopProxy接口,並且提供了兩個final類型的實現類Cglib2AopProxy和JdkDynamicAopProxy。

Cglib2AopProxy使用CGLib技術創建代理,JdkDynamicAopProxy通過JDK動態代理技術創建代理。

如果通過ProxyFactory的setInterfaces(Classs[] interfaces)方法指定針對接口進行代理,ProxyFactory就使用JdkDynamicAopProxy,如果是針對類的代理,則使用Cglib2AopProxy。此外還可以通過ProxyFactory的setOptimize(true)方法,讓ProxyFactory啓動優化處理方式,這樣,針對接口的代理也會使用Cglib2AopProxy。

使用配置文件聲明代理

雖然通過ProxyFactory已經簡化了很多操作,但是Spring給我們提供了通過配置文件來聲明一個代理來讓免去硬編碼

<!--實例化目標對象-->
<bean id="target" class="com.cad.demo.Dog"></bean>  
<!--實例化增強對象-->
<bean id="advice" class="com.cad.demo.MyBeforeAdvice"></bean> 
<!--實例化代理對象-->
<bean id="proxydog" class="org.springframework.aop.framework.ProxyFactoryBean"> 
    <!--指定代理的接口,有多個的話可以使用數組、list等標籤-->
    <property name="proxyInterfaces" value="com.cad.demo.Animal"></property> 
    <!--指定使用的增強-->
    <property name="interceptorNames" value="advice"></property> 
    <!--指定代理的目標類-->
    <property name="target" ref="target"></property>
</bean>

ProxyFactoryBean是FactoryBean接口的實現類,我們前一節中介紹了FactoryBean的功能,它負責實例化一個Bean。ProxyFactoryBean負責爲其他Bean創建代理對象,內部使用ProxyFactory來完成。我們來了解一下可配置的屬性

  • target:指定要代理的目標對象。

  • proxyInterfaces:代理要實現的接口,可以是多個接口,使用數組、list標籤來指定。

  • interceptorNames:需要織入的增強類。 是String[]類型,接受增強Bean的id而不是實例。

  • singleton:返回的代理是否是單實例,默認是單實例。

  • optimize:當設置爲true時,強制使用CGLib代理。

  • proxyTargetClass:是否對類進行代理,設置爲true,使用CGLib代理。

    //我們測試一下
    public class Test {
        @Test
        public void test(){
            ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml"); 
            Animal dogproxy=(Animal) ac.getBean("proxydog");
            dogproxy.eat();
        }

    }

這裏寫圖片描述

後置增強和前置增強一模一樣,不需要過多說明。

Spring中環繞增強

環繞增強允許在目標方法調用前後織入增強,綜合實現了前置、後置增強兩者的功能。

我們還是使用上面小狗的例子。

    //創建環繞增強實現類
    public class MyInterceptor implements MethodInterceptor {

        public Object invoke(MethodInvocation invocation) throws Throwable {
            System.out.println("主人發出命令");  
            //使用proceed()方法反射調用目標類相應的方法
            Object obj=invocation.proceed();
            System.out.println("主人給予獎勵"); 

            return obj;
        }

    }

        <!--修改配置文件 -->
<bean id="target" class="com.cad.demo.Dog"></bean> 
<bean id="advice" class="com.cad.demo.MyInterceptor"></bean> 

<bean id="proxydog" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="proxyInterfaces" value="com.cad.demo.Animal"></property>
    <property name="interceptorNames" value="advice"></property> 
    <property name="target" ref="target"></property>
</bean>
//我們測試一下
public class Test {
    @Test
    public void test(){
        ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml"); 
        Animal dogproxy=(Animal) ac.getBean("proxydog");
        dogproxy.eat();
    }

}

這裏寫圖片描述

Spring中異常拋出增強

異常拋出增強最適合的應用場景是事務管理,當執行事務發生異常時,就必須回滾事務。

我們給出一個模擬的例子,來說明異常拋出增強的使用。

    //模擬業務類中的轉賬操作,拋出異常
    public class UserService {
        public void addMoney(){
            throw new RuntimeException("轉賬異常");
        }
    }
    //我們實現異常拋出增強接口,對轉賬業務方法進行增強處理
    public class TransactionManager implements ThrowsAdvice { 
        public void afterThrowing(Method method,Object[] args,Object target,Exception e)throws Throwable{
            System.out.println(method.getName());
            System.out.println(e.getMessage());
            System.out.println("回滾事務");
        }

    }

ThrowsAdvice異常拋出增強接口沒有定義任何方法,它是一個標識接口,在運行期Spring使用反射機制自行判斷,我們必須使用以下簽名形式定義異常拋出的增強方法。

  • void afterThrowing(Method method,Object[] args,Object target,Throwable)

方法名必須爲afterThrowing,前三個參數是可選的,最後一個參數是Throwable或者其子類。可以在同一個增強中定義多個afterThrowing()方法,當目標類方法拋出異常時,Spring會自動選用最匹配的增強方法,主要是根據異常類的匹配程度。

<!--編寫配置文件-->
<bean id="target" class="com.cad.demo.UserService"></bean> 
<bean id="advice" class="com.cad.demo.TransactionManager"></bean> 

<bean id="proxyservice" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="interceptorNames" value="advice"></property> 
    <property name="target" ref="target"></property> 
    <!--因爲我們的業務類沒有實現接口,因此使用CGLib代理-->
    <property name="proxyTargetClass" value="true"></property>
</bean>
//我們進行測試一下 
public class Test {
    @Test
    public void test(){
        ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml"); 
        UserService service=(UserService) ac.getBean("proxyservice");
        service.addMoney();
    }

}

這裏寫圖片描述

Spring中切點類型

在學習增強時,我們可能注意到一個問題:增強被織入到目標類的所有方法中。我們希望有選擇的織入到某些特定的方法中,就需要使用切點來進行目標連接點的定位。

Spring通過Pointcut接口描述切點,Pointcut由ClassFilter和MethodMatcher構成,通過ClassFilter定位到某些特定類上,通過MethodMatcher定位到某些特定方法上,這樣Pointcut就擁有了描述某些類的特定方法的能力。

Spring支持兩種方法匹配器:靜態方法匹配器和動態方法匹配器。靜態方法匹配器僅對方法名簽名(包括方法名和入參類型和順序)進行匹配。動態方法匹配器會在運行期檢查方法入參的值。靜態匹配僅匹配一次,而動態匹配因爲每次調用方法的參數都可能不一樣,所以每次調用都需要判斷。所以動態匹配影響性能,不常使用。

切點類型

  • 靜態方法切點::org.springframework.aop.support.StaticMethodMatcherPointcut是靜態方法切點的抽象基類,默認情況下它匹配所有的類。StaticMethodMatcherPointcut包括兩個主要的子類,分別是NameMatchMethodPointcut和AbstractRegexpMethodPointcut,前者提供簡單字符串匹配方法簽名,而後者使用正則表達式匹配方法簽名。

  • 動態方法切點:org.springframework.aop.support.DynamicMethodMatcherPointcut 是動態方法切點的抽象基類,默認情況下它匹配所有的類。DynamicMethodMatcherPointcut類已經過時,可以使用DefaultPointcutAdvisor 和DynamicMethodMatcherPointcut動態方法匹配器替代之。

  • 註解切點:org.springframework.aop.support.AnnotationMatchingPointcut實現類表示註解切點。使用AnnotationMatchingPointcut支持在Bean中直接通過JDK5.0註解標籤定義的切點。

  • 表達式切點:org.springframework.aop.support.ExpressionPointcut接口主要是爲了支持AspectJ切點表達式語法而定義的接口。

  • 流程切點:org.springframework.aop.support.ControlFlowPointcut實現類表示控制流程切點。ControlFlowPointcut是一種特殊的切點,它根據程序執行堆棧的信息查看目標方法是否由某一個方法直接或間接發起調用,以此判斷是否爲匹配的連接點。

  • 複合切點:org.springframework.aop.suppot.ComposablePointcut實現類是爲創建多個切點而提供的方便操作類。它所有的方法都返回ComposablePointcut類,這樣,我們就可以使用鏈接表達式對切點進行操作

Spring中切面類型

由於增強既包含橫切代碼,又包含部分的連接點信息,所以我們可以僅通過增強類生成一個切面。 Spring使用org.springframework.aop.Advisor接口表示切面的概念,一個切面同時包含了增強和切點的信息。切面可以分爲三類:一般切面、切點切面、引介切面。

  • Advisor:代表一般切面,它僅包含一個Advice。我們說過,因爲Advice包含了橫切代碼和連接點的部分信息,所以Advice本身就是一個簡單的切面,但是它代表的連接點是目標類的所有方法,橫切面太寬泛,不會直接使用。

  • PointcutAdvisor:代表具有切點的切面,它包含Pointcut和Advice兩個可u,這樣,我們就可以通過類名、方法名、方法方位等信息靈活的定義切面的連接點。

  • IntroductionAdvisor:代表引介切面

我們主要學習PointcutAdvisor,PointAdvisor主要有六個具體的實現類

  • DefaultPointcutAdvisor:最常用的切面類型,它可以通過任意Pointcut和Advice定義一個切面,不支持引介切面類型,可以通過擴展該類實現自定義的切面。

  • NameMatchMethodPointcutAdvisor:通過該類可以定義按方法名定義切點的切面。

  • RegexpMethodPointcutAdvisor:對於按照正則表達式匹配方法名定義切點的切面,通過該類進行操作。

  • StaticMethodMatcherPointcutAdvisor:靜態方法匹配器切面定義的切面,默認情況匹配所有目標類。

  • AspectJExpressionPointcutAdvisor:用於AspectJ切點表達式定義切點的切面。

  • AspectJPointcutAdvisor:用於AspectJ語法定義切點的切面。

演示靜態方法匹配切面

StaticMethodMatcherPointcutAdvisor代表一個靜態方法匹配切面,通過類過濾和方法名匹配定義切點

    我們創建兩個類 ,Dog和Cat 

    public class Dog {
        public void eat(){
            System.out.println("狗在吃飯");
        }

        public void run(){
            System.out.println("狗在跑步");
        }
    } 

    public class Cat {
        public void eat(){
            System.out.println("貓在吃飯");
        }

        public void run(){
            System.out.println("貓在跑步");
        }
    }

    //現在我們定義一個切面,在Dog的eat()方法調用前織入一個增強 
    //StaticMethodMatcherPointcutAdvisor抽象類唯一需要實現的是matches()方法
    //但是默認情況,該切面匹配所有類,我們通過覆蓋getClassFilter方法,僅匹配Dog及其子類
    public class MyAdvisor extends StaticMethodMatcherPointcutAdvisor {

        //匹配方法名稱爲eat的方法
        public boolean matches(Method method, Class<?> clazz) {
            return method.getName().equals("eat");
        }

        //我們通過覆蓋getClassFilter方法,僅匹配Dog及其子類
        public ClassFilter getClassFilter() {
            return new ClassFilter(){
                public boolean matches(Class<?> clazz) {        
                    return Dog.class.isAssignableFrom(clazz);
                }

            };
        }
    }


    //我們還需要定義一個增強,我們使用前置增強
    public class MyBeforeAdvice implements MethodBeforeAdvice {

        public void before(Method arg0, Object[] arg1, Object arg2) throws Throwable {
            System.out.println("主人發出命令");
        }

    }
    <!--配置文件-->
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:context="http://www.springframework.org/schema/context" 
        xsi:schemaLocation="
            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <!--Dog目標類和Cat目標類-->
    <bean id="targetDog" class="com.cad.spring.Dog"></bean>
    <bean id="targetCat" class="com.cad.spring.Cat"></bean> 
    <!--增強類--> 
    <bean id="advice" class="com.cad.spring.MyBeforeAdvice"></bean>  
    <!--切面,裏面包含增強,還有一個classFilter屬性,可以指定類匹配過濾器,但我們在類裏面已經通過覆蓋來實現類匹配過濾器,所以不用配置-->
    <bean id="MyAdvisor" class="com.cad.spring.MyAdvisor">
        <property  name="advice" ref="advice"></property>
    </bean>

    <!--小狗代理-->
    <bean id="dogproxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="interceptorNames" value="MyAdvisor"></property>
        <property name="proxyTargetClass" value="true"></property> 
        <property name="target" ref="targetDog"></property>
    </bean>

    <!--小貓代理-->
    <bean id="catproxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="interceptorNames" value="MyAdvisor"></property>
        <property name="proxyTargetClass" value="true"></property> 
        <property name="target" ref="targetCat"></property>
    </bean>
    </beans>       
    //我們進行測試一下
    public class TestDemo {
        @Test
        public void test(){
            ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
            Dog dog=(Dog)ac.getBean("dogproxy");
            Cat cat=(Cat)ac.getBean("catproxy");
            dog.eat();
            System.out.println("-------------------------");
            cat.eat();
        }
    }

這裏寫圖片描述

可見切面只織入到Dog的eat方法前的連接點上,並且Dog的run()方法沒有織入切面。

我們只演示這些,還有動態切面,複合切點切面等都可以自行找相關資料,大概流程都差不多。用的也不多

Spring自動創建代理

在前面所有的例子中,對於每個代理對象,我們都需要進行配置,需要代理的對象很多的時候就會很麻煩。
Spring爲我們提供了自動代理機制,讓容器爲我們自動生成代理,免去我們繁瑣的配置功能,內部原理是使用BeanPostProcessor後處理Bean自動地完成這項工作,當我們獲取對象時,後處理Bean會獲取我們對象的代理,然後最後返回的是我們獲取Bean的代理。

這些基於BeanPostProcessor的自動代理創建器的實現類,根據一些規則自動在容器實例化Bean時爲匹配的Bean生成代理,這些代理創建器可以分爲以下三類。

  • 基於Bean配置名規則的自動代理創建器:允許爲一組特定配置名的Bean自動創建代理。實現類爲BeanNameAutoProxyCreator

  • 基於Advisor匹配機制的自動代理創建器:它會對容器中所有的Advisor進行掃描,自動將這些切面應用到匹配的Bean中,實現類爲DefaultAdvisorAutoProxyCreator

  • 基於Bean中AspectJ註解標籤的自動創建代理器:爲包含AspectJ註解的Bean自動創建代理。實現類是AnnotationAwareAspectJAutoProxyCreator

演示BeanNameAutoProxyCreator

    我們還使用前面貓和狗的例子,不過我們需要修改配置文件
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:context="http://www.springframework.org/schema/context" 
        xsi:schemaLocation="
            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    //貓和狗的目標類
    <bean id="targetDog" class="com.cad.spring.Dog"></bean>
    <bean id="targetCat" class="com.cad.spring.Cat"></bean>  
    //增強類
    <bean id="advice" class="com.cad.spring.MyBeforeAdvice"></bean>  

    //配置BeanNameAutoProxyCreator實現類
    <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
        //beanNames屬性允許用戶指定需要自動代理的Bean名稱,可以使用*通配符,例如*t,就會匹配cat
        <property name="beanNames" value="targetDog,targetCat"></property>
        <property name="interceptorNames" value="advice"></property>
        //指定使用CGLib
        <property name="optimize" value="true"></property>
    </bean> 

    </beans>
    //我們進行測試
    public class TestDemo {
        @Test
        public void test(){
            ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
            Dog dog=(Dog)ac.getBean("targetDog");
            Cat cat=(Cat)ac.getBean("targetCat");
            dog.eat();
            System.out.println("-------------------------");
            cat.eat();
        }
    }

這裏寫圖片描述

AspectJ學習

AspectJ是一個面向切面的框架,它擴展了Java語言。AspectJ定義了AOP語法,所以它有一個專門的編譯器用來生成遵守Java字節編碼規範的Class文件。

Spring2.0之後增加了對AspectJ切點表達式的支持。@AspectJ是AspectJ1.5新增的功能,通過JDK的註解技術,允許開發者在Bean上直接通過註解定義切面。Spring使用和@AspectJ相同風格的註解,並通過AspectJ提供的註解庫和解析庫來處理切點。

AspectJ切點表達式

@AspectJ支持三種通配符

  • * 匹配任意字符,只匹配一個元素

  • .. 匹配任意字符,可以匹配多個元素 ,在表示類時,必須和*聯合使用

  • + 表示按照類型匹配指定類的所有類,必須跟在類名後面,如com.cad.Car+,表示繼承該類的所有子類包括本身

邏輯運算符

切點表達式由切點函數組成,切點函數之間還可以進行邏輯運算,組成複合切點。

 - &&:與操作符。相當於切點的交集運算。xml配置文件中使用切點表達式,&是特殊字符,所以需要轉義字符&amp;來表示。 

 - ||:或操作符。相當於切點的並集運算。  

 - !:非操作符,相當於切點的反集運算。   

Spring支持9個@AspectJ切點表達式函數,它們用不同的方式描述目標類的連接點。我們來了解幾個常用的

  • execution()
    execution()是最常用的切點函數,用來匹配方法,語法如下
    execution(<修飾符><返回類型><包.類.方法(參數)><異常>)
    修飾符和異常可以省略。

        使用例子 
        -execution(public * *(..)):匹配目標類的所有public方法,第一個*代表返回類型,第二個*代表方法名,..代表方法的參數。  
    
        -execution(**User(..)):匹配目標類所有以User爲後綴的方法。第一個*代表返回類型,*User代表以User爲後綴的方法 
    
        -execution(* com.cad.demo.User.*(..)):匹配User類裏的所有方法 
    
        -execution(* com.cad.demo.User+.*(..)):匹配該類的子類包括該類的所有方法 
    
        -execution(* com.cad.*.*(..)):匹配com.cad包下的所有類的所有方法   
    
        -execution(* com.cad..*.*(..)):匹配com.cad包下、子孫包下所有類的所有方法  
    
        -execution(* addUser(Spring,int)):匹配addUser方法,且第一個參數類型是String,第二個是int
    
  • args()
    該函數接受一個類名,表示目標類方法參數是指定類時(包含子類),則匹配切點。
    args(com.cad.User):匹配addUser(User user)方法等

  • within()
    匹配類,語法
    within(<類>)

        within(com.cad.User):匹配User類下的所有方法  
    
  • target()

    target()函數通過判斷目標類是否按類型匹配指定類決定連接點是否匹配。

    例如 target(com.cad.User):如果目標類類型是User沒那麼目標類所有方法都匹配切點。

  • this()
    this()函數判斷代理對象的類是否按類型匹配指定類。

AspectJ增強類型

  • Before:前置增強。相當於BeforeAdvice的功能,方法執行前執行。

  • AfterReturning:後置增強。相當於AfterReturningAdvice,方法執行後執行。

  • Around:環繞增強。

  • AfterThrowing:異常拋出增強。

  • After:不管是拋出異常還是正常退出,該增強都會執行,類似於finally塊。

  • DeclareParents:引介增強。

AspectJ基於XML配置切面

使用AspectJ我們需要的jar包有aopalliance-1.0.jar,aspectjweaver-1.8.10.jar,spring-aop-4.3.8.RELEASE.jar,spring-aspects-4.3.8.RELEASE.jar。

我們先來一個簡單的配置,看看怎麼使用

    //我們先創建Dog類
    public class Dog {
        public void eat(){
            System.out.println("狗在吃飯");
        }

        public void run(){
            System.out.println("狗在跑步");
        }
    }
    //我們創建一個增強類 
    public class AdviceMethod {
        public void before(){
            System.out.println("主人發出命令");
        }
    }
    <!--配置文件-->
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:context="http://www.springframework.org/schema/context"   
        xmlns:aop="http://www.springframework.org/schema/aop"     //聲明aop命名空間
        xsi:schemaLocation="
            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/aop            //引入aop xsd文件http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-實例Dog類和增強類-->
    <bean id="dog" class="com.cad.aspectj.Dog"></bean>
    <bean id="advices" class="com.cad.aspectj.AdviceMethod"></bean>

    <!-配置aop,proxy-target-class屬性設定爲true時,使用CGLib,爲false時,使用JDK動態代理-->
    <aop:config proxy-target-class="true"> 
        <!--使用<aop:aspect>標籤定義切面,ref引入增強-->
        <aop:aspect ref="advices">
            <!--通過<aop:before>聲明一個前置增強,pointcut屬性使用切點表達式,method指定使用增強類中方法-->
            <aop:before pointcut="execution(* *(..))"   method="before" />
        </aop:aspect>
    </aop:config>

    </beans>     
    //我們測試一下
    public class TestDemo {
        @Test
        public void test(){
            ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
            Dog dog=(Dog)ac.getBean("dog");
            dog.eat();
            dog.run();
        }
    }

這裏寫圖片描述

配置切點

我們上面的代碼中,直接在< aop: before>前置增強標籤裏使用了表達式來聲明切點。
我們還可以在外面配置一個切點,使用的時候直接引用即可。

<aop:config proxy-target-class="true">
    <aop:aspect ref="advices"> 
        //定義一個切點
        <aop:pointcut expression="execution(* *(..))" id="mypointcut"/>
        //直接通過id引用即可
        <aop:before pointcut-ref="mypointcut"   method="before" />
    </aop:aspect>
</aop:config> 

    <aop:pointcut>元素如果位於 <aop:aspect>元素之中,則只能被當前<aop:aspect>中的元素訪問到。爲了能被整個 <aop:config>元素中定義的所有切面訪問到,必須在<aop:config>下定義。

後置增強

    //我們在我們的增強類裏添加一個後置增強方法after
    public class AdviceMethod {
        public void before(){
            System.out.println("主人發出命令");
        }

        public void after(){
            System.out.println("主任給予獎勵");

        }
    }
<bean id="dog" class="com.cad.aspectj.Dog"></bean>
<bean id="advices" class="com.cad.aspectj.AdviceMethod"></bean> 

<aop:config proxy-target-class="true">
    <aop:aspect ref="advices">
        <aop:pointcut expression="execution(* *(..))" id="mypointcut"/>
        <aop:before pointcut-ref="mypointcut"   method="before" /> 
        //配置後置增強
        <aop:after-returning pointcut-ref="mypointcut" method="after"/>
    </aop:aspect>
</aop:config>

這裏寫圖片描述

< aop:after-returning >後置增強有一個returning屬性,該屬性對應後置方法裏的參數值,並且必須與方法裏的參數值名稱相同,該參數用來接收目標方法執行後的返回值。

//我們run()方法返回一個String字符串 
public class Dog {
    public void eat(){
        System.out.println("狗在吃飯");
    }

    public String run(){
        System.out.println("狗在跑步");
        return "跑完了";
    }
}
//增強類的後置方法接收目標方法返回的參數
public class AdviceMethod {
    public void before(){
        System.out.println("主人發出命令");
    }

    public void after(String arg){
        System.out.println("主任給予獎勵");
        System.out.println(arg);

    }


}
配置文件中配置,別的和前面的沒區別
<aop:after-returning pointcut-ref="mypointcut" method="after" returning="arg"/>

這裏寫圖片描述

環繞增強

    //定義環繞增強方法,參數爲ProceedingJoinPoint,返回值爲Object,這是連接點信息,後面會詳解
    public class AdviceMethod {

        public Object around(ProceedingJoinPoint pjp) throws Throwable{
            System.out.println("環繞增強前");
            Object obj=pjp.proceed();
            System.out.println("環繞增強後");
            return obj;
        }


    }
    <!--配置環繞增強-->

    <aop:config proxy-target-class="true">
        <aop:aspect ref="advices">
            <aop:pointcut expression="execution(* *(..))" id="mypointcut"/>
            <aop:around method="around" pointcut-ref="mypointcut"/>
        </aop:aspect>
    </aop:config>
    //我們測試一下
    public class TestDemo {
        @Test
        public void test(){
            ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
            Dog dog=(Dog)ac.getBean("dog");
            dog.eat();
        }
    }

這裏寫圖片描述

其他三個配置起來都大同小異,這裏就不再一一演示。

增強方法訪問連接點信息

AspectJ使用JoinPoint接口表示目標類的連接點對象,如果是環繞增強訪問的話,則必須使用ProceedingJoinPoint表示連接點對象,該類是JoinPoint子接口。

JoinPoint接口主要方法

  • Object [] getArgs():獲取連接點方法的參數

  • Signature getSignatrue():獲取連接點的方法簽名對象,方法簽名由方法名稱和形參列表組成。

  • Object getTarget():獲取連接點所在的目標對象

  • Object getThis():獲取代理對象本身

ProceedingJoinPoint主要方法

  • Object proceed()throws Throwable;通過反射執行目標對象的連接點方法

  • Object proceed(Object[] args)throws Throwable:通過反射執行目標對象的連接點方法,使用我們提供的參數。

這樣,我們就可以在我們的增強方法中使用JoinPoint或者ProceedingJoinPoint參數,來獲得連接點方法的一些信息。

AspectJ基於註解配置切面

    //先使用註解來實例我們的Bean
    @Component("dog")
    public class Dog {
        public void eat(){
            System.out.println("狗在吃飯");
        }

        public void run(){
            System.out.println("狗在跑步");
        }
    }
    //實例增強類
    @Component("advices") 
    //使用Aspect
    @Aspect
    public class AdviceMethod { 
        //使用環繞增強,裏面參數是切點表達式
        @Around("execution(* com.cad.anno.*.*(..))")
        public Object around(ProceedingJoinPoint pjp) throws Throwable{
            System.out.println("環繞增強前");
            Object obj=pjp.proceed();
            System.out.println("環繞增強後");
            return obj;
        }
    }
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:context="http://www.springframework.org/schema/context"  
        xmlns:aop="http://www.springframework.org/schema/aop"
        xsi:schemaLocation="
            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

     //使用組件掃描
    <context:component-scan base-package="com.cad.anno"></context:component-scan> 
    //使用aspectj自動代理,自動爲匹配@AspectJ切面的Bean創建代理
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

    </beans>    
    //我們測試一下
    public class TestDemo {
        @Test
        public void test(){
            ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
            Dog dog=(Dog)ac.getBean("dog");

            dog.eat();
        }
    }

這裏寫圖片描述

增強類中的方法可以使用不同的增強類型來定義。

  • @Before:前置增強
  • @AfterReturning:後置增強
  • @Around:環繞增強
  • @AfterThrowing:拋出異常增強
  • @After:Final增強

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章