1. 程式人生 > >spring引介增強定時器例項:無侵入式動態增強類功能

spring引介增強定時器例項:無侵入式動態增強類功能

引介增強例項需求

在前面我們已經提到了前置、後置、環繞、最終、異常等增強形式,它們的增強物件都是針對方法級別的,而引介增強,則是對類級別的增強,我們可以通過引介增強為目標類新增新的屬性和方法,更為誘人的是,這些新屬性或方法是可以根據我們業務邏輯需求動態變化的。怎麼來理解這一點?我們先展示一個用引介增強解決的現實需求問題,現在先來看看我們的一個需求:

我們要設計一個定時任務,在每天特定流量高峰時間裡,判斷人們某個網路請求在服務端程式業務邏輯處理上的耗時,一般地,我們web後端處理請求處理按如下時序圖執行:

Created with Raphaël 2.1.0模擬服務端接受web請求並返回客戶端客戶端controllercontrollerserviceserviceDAO/資料庫DAO/資料庫傳送請求進行業務邏輯處理呼叫DAO層API訪問資料庫進行資料處理返回資料封裝返回相應業務邏輯處理結果傳送響應。

現在,即需要我們統計從接收請求後到發出相應前這個過程的耗時。

最直接的解決方法就是,在Controller層的每一個方法起始到結束,分別記錄一次時間,用結束減起始來獲得結果,但這存在兩個問題:
1. 根據springMVC一個方法對應一種請求,如果我們的controller中有很多方法,而我們需要為每一個方法都進行統計,這意味者我們需要在controller中嵌入大量重複冗雜的程式碼,並且這些程式碼和我們controller層邏輯沒有任何關係,如果日後我們根據需求要新增其他任務,就又要修改我們的controller,這不符合開閉原則和職責分明原則。
2. 因為我們的需求是動態的,我們還可能需要在controller中每次呼叫都去判斷當前時刻是否處於需要統計耗時的時刻。

基於介面實現和代理工廠

下面看看我們如何用引介增強來解決同樣的問題:

1. 定義UserController

public class UserController {
    public void login(String name){
        System.out.println("I'm "+name+" ,I'm logining");

        try {
            Thread.sleep(100);//模擬登陸過程中進行了資料庫查詢。各種業務邏輯處理的複雜工作
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2. 配置引介增強需要實現的代理介面

public interface TaskActiver {//通過此介面來監控是是否啟用定時器任務
    void startTask();//滿足特定條件,開啟任務
    void stopTask();//不滿足特定條件,停止任務
}

3. 配置我們的增強實現類

    public class MyTaskController extends DelegatingIntroductionInterceptor implements TaskActiver{
    /*
    1. DelegatingIntroductionInterceptor是引介增強介面IntroductionInterceptor的實現類,我們通過擴充套件此介面自定義引介增強類
    2. 引介增強類要實現我們的代理介面TaskActiver,這是和其他增強不同的地方。
    */
        private ThreadLocal<Boolean> isTime = new ThreadLocal<Boolean>();//這是一個執行緒安全的變數。

        public MyTaskController(){//定時任務控制器建構函式
            isTime.set(false);//任務預設處於關閉狀態
        }
        @Override
        public void startTask() {//通過定時器監控達到特定時間,啟動任務。
            isTime.set(true);
        }

        @Override
        public void stopTask() {//在任務完成後關閉任務,等待下次定時器到時啟動
            isTime.set(false);
        }

        /**
         * //覆蓋父類的invoke方法,當程式執行到特定方法時,我們先攔截下來,
         * 然後啟動我們的任務,再呼叫相應的方法,在方法呼叫完後,完成並關閉我們的任務

         */
        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable{
            Object obj = null;
            if(isTime.get()){//到了特定時刻,即我們的定時器處於任務啟用狀態。
                MyTask.getTarget().set(invocation.getMethod().getName());
                //通過反射獲取執行期當前任務的名稱,並初始化我們的任務配置,在這裡我們的任務是記錄時間
                MyTask.beginRecord();//任務開始,記錄開始時間
                obj = super.invoke(invocation);//呼叫原目標物件方法
                MyTask.endRecord();//結束任務,結合開始時間計算耗時,並將統計結果儲存到日誌記錄文字中
            }else{
                obj = super.invoke(invocation);
            }
            return obj;
        }
    }
    /********************下面是我們的任務操作類***********************/
//這裡是記時操作,如果以後我們有其他類似業務需求,可以修改此類,而不是像我們開始提到的那樣直接寫在controller中
        /*
        MyTask是單例類,為確保每個執行緒有自己獨立的時間記錄和方法名,將下面兩個靜態變數定義為ThreadLocal型別。
        */
        private static ThreadLocal<Long>beginTime = new ThreadLocal<Long>() ;//記錄開始時間
        private static ThreadLocal<String> target = new ThreadLocal<String>();//記錄呼叫方法

        public static void beginRecord(){
            System.out.println("記時開始");
            beginTime .set(System.currentTimeMillis());
        }

        public static void endRecord(){
            System.out.println("記時結束");
            System.out.println("呼叫"+target+"方法耗時:" +( System.currentTimeMillis()  - beginTime.get())+ "毫秒");
        }

    }
  1. 首先談談ThreadLocal是什麼東東,當我們在多執行緒中共享一個物件時,物件裡的成員屬性是有狀態的,只要一個執行緒對成員屬性進行了修改,在其他執行緒也會生效,如果我們想每個執行緒都共享一個物件,但不共享裡面的成員屬性,就可以使用ThreadLocal, 它會為我們的每一個執行緒建立一個當前成員屬性的拷貝,即使我們任一執行緒對其進行了修改,也不會影響到其他執行緒。這就好像幾個朋友(執行緒)一起住在(共享)一間大房子(物件)但每個人都有自己獨立的(一模一樣的)房間(成員屬性)即使我們把自己的房間拆了,也不會影響到其他人。
  2. 對於以上三個ThreadLocal變數中的beginTime和target我們可能較好理解,但為什麼我們要把isTime也設為ThreadLocal型?這是考慮我們可能要利用MyTaskController是單例的,如果我們要排程其他定時任務(而不是這裡簡單地記錄前後耗時),為確保不同引介增強的“定時狀態“不互相共享,我們必須確保它是執行緒安全的。
  3. 那麼可能又問:為什麼不乾脆直接將MyTaskControoler設成prototype?這首先要明確,MyTaskController是一個代理類,由CGLib建立,而CGLib建立代理的效能是很低的,如果每次注入(如getBean())獲取代理例項,都返回一個新的例項,將會嚴重影響效能。

4. 配置我們的IOC容器

<bean id="myTaskController" class="test.aop4.MyTaskController"/>
<bean id="proxyFactory" class="org.springframework.aop.framework.ProxyFactoryBean" >
    <!-- 使用CHLib代理,配合引介增強通過建立子類來生成代理,預設值為false會報錯 -->
    <property name="proxyTargetClass" value="true" />
    <property name="interfaces" value="test.aop4.TaskActiver" /><!--註冊代理介面,我們的代理增強類必須實現此介面-->
    <property name="targetClass" value="test.aop4.UserController"/><!--這是我們的目標物件,通過指定目標物件,我們可以在注入時,直接將當前代理工廠Bean強制轉換為我們的UserController。從而生成我們的代理子類-->
    <property name="interceptorNames" value="myTaskController" /><!--字面意思是攔截器名稱,實現效果類似於攔截器,把我們目標類的所有行為(方法)都攔截下來,織入我們的增強(體現在invoke函式的重寫中)-->
</bean>

5. 執行測試函式

    public static void main(String args[]) throws InterruptedException{
        ApplicationContext ac = new ClassPathXmlApplicationContext("classpath:test/aop4/aop.xml");
        UserController userController = (UserController) ac.getBean("proxyFactory");//通過代理工廠來生成我們的目標類
        SimpleDateFormat sdf = new SimpleDateFormat("HHmm");
        TaskActiver taskActiver = (TaskActiver) userController;
        userController.login("zenghao");//這裡方便測試,沒有開啟任務,先在正常情況下模擬使用者登陸請求
        while(true){//這裡方便測試,一般情況,我們應另開一個執行緒單獨來執行我們的定時任務部分
            //定時任務部分開始
            Integer now = Integer.valueOf(sdf.format(new Date()));//以時分格式獲取當前時間
            if(now >  0000&& now < 0030 ){
            //在特定時間內開啟任務,咱們不妨假設這個時間就是雙十一晚的12點到12點半。淘寶要實施監控統計資料
                taskActiver.startTask();
            }else if(now <=  0000 && now >= 0030){//在特定時間外關閉任務
                taskActiver.stopTask();
            }
            //定時任務部分結束
            //下面模擬正常的web請求,假設每隔5秒中,伺服器接受一個web請求。
            Thread.sleep(5 * 1000);
            userController.login("zenghao");
        }

    }

6. 測試結果

I’m zenghao ,I’m logining ——————-這是任務沒開始前的web請求
記時開始
I’m zenghao ,I’m logining —————-這是滿足特定時間開啟任務後的web請求
記時結束
呼叫[email protected]方法耗時:101毫秒
記時開始
記時結束
呼叫[email protected]方法耗時:0毫秒 ——-代理增強類中使用ThreadLocal變數的時間也被記錄下來,
記時開始
I’m zenghao ,I’m logining —————-這是滿足特定時間開啟任務後的web請求
記時結束
呼叫[email protected]方法耗時:101毫秒

7. 例項小結

  1. 很多核心概念與實現細節都體現在例項程式碼註釋中,如果有不理解的地方,可以再多看看上面的程式碼例項及相關的註釋。
  2. 觀察我們的測試函式,有一個語句為TaskActiver taskActiver = (TaskActiver) userController;即將我們的代理介面引用指向了我們的目標物件,這個時候我們操縱我們的代理介面,實際上由多型特性,我們操縱的是實現了此介面的增強類MyTaskController,於是,核心部分來了:我們的增強類”涵蓋“了我們的目標物件(注意到我們的taskActiver是由userController強轉而來的),但他又有了許多新的特性,比如我們在MyTaskController中定義的isTime變數,或者簡介呼叫了MyTask中的方法(實際上可以完全不要MyTask類,直接將裡面的變數方法定義在MyTaskController中)。總之,我想說的是,我們通過引介增強,可以:
    1. 新的方法、成員屬性添進我們的目標物件中,這些新的方法和成員屬性通過介面代理操控
    2. 卻不用對目標對像做任何修改,而者在現實中是很有意義的,對於大型專案或新接手專案,我們的類可能已經集成了很多的功能,面對新的需求,我們只想新增新的功能,而不是直接對已有的完整的健壯的類進行修改。這也是各類增強的重大實現意義所在。
    3. 不用改變目標物件的被呼叫方式(比如要登陸,還是呼叫login方法),即不影響我們的上一層方法呼叫,比如,我修改了service層的類,不用隨之修改呼叫了service層該類的對應程式碼段。
    4. 同時最為巧妙的是,我們可以通過我們的業務邏輯來控制這種增強什麼時候起作用,什麼時候不起作用!比如這裡的例子,我要的只是在特定時間內增強。這也是區別其他增強如環繞增強的核心所在。其他的增強一旦配置好了,就必然生效,而不像引介那樣是可控的。

除了上述方法外,我們還可以通過@AspectJ和基於Schema的配置來實現引介增強。不過通過測試,通過以上兩種方法,由於不能實現重寫invoke方法的橫切邏輯,增強效果大大減弱,它僅能為目標類動態新增新的實現介面,卻不能無侵入式修改原目標類方法的呼叫效果,比如我再呼叫login方法,不能通過引介增強來實現耗時統計的功能了,除非使用環繞增強加上一定的業務邏輯處理。下面我們先看用@AspectJ配置來分析說明

使用@AspectJ

使用註解@DeclareParents,相當於IntroductionInterceptor,該註解有兩個屬性:
- value:定義切點
- defaultImpl:指定預設的介面實現類。

下面是程式碼示例:

1. 目標物件類

相對之前例子沒有變化

public class UserController {
    public void login(String name){
        System.out.println("I'm "+name+" ,I'm logining");

        try {
            Thread.sleep(100);//模擬登陸過程中進行了資料庫查詢。各種業務邏輯處理的複雜工作
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2. 代理介面和代理介面實現類

相對之前例子,代理介面沒有變化,而代理介面實現(增強)類則不再繼承DelegatingIntroductionInterceptor類。

/*********************代理介面*********************/
public interface TaskActiver {//通過此介面來監控是是否啟用定時器任務
    void startTask();
    void stopTask();
}

/*********************代理介面實現類*********************/
public class MyTaskController  implements TaskActiver{
    private ThreadLocal<Boolean> isTime = new ThreadLocal<Boolean>();

    public MyTaskController(){
        isTime.set(false);
    }
    @Override
    public void startTask() {//通過定時器監控達到特定時間,啟動任務。
        System.out.println("任務開啟");
        isTime.set(true);
    }

    @Override
    public void stopTask() {//在任務完成後關閉任務,等待下次定時器到時啟動
        System.out.println("任務關閉");
        isTime.set(false);
    }
}

3. 配置切面(新增類)

@Aspect
public class MyTaskAspect {
    //value指向我們的目標物件,defaultImpl指向我們的代理介面實現類
    @DeclareParents(value = "test.aop5.UserController",defaultImpl = MyTaskController.class)//註解在我們的代理介面上
    public TaskActiver taskActiver;//宣告代理介面
}

4. IOC容器配置

<aop:aspectj-autoproxy proxy-target-class="true"/><!-- 使切面註解@AspectJ生效 -->
<bean id="userController" class="test.aop5.UserController" /><!-- 測試需要注入的Bean-->
<bean class="test.aop5.MyTaskAspect" /><!--註冊切面,使IOC容器自動配置AOP-->

5. 執行測試方法

public static void main(String args[]) throws InterruptedException{
    ApplicationContext ac = new ClassPathXmlApplicationContext("classpath:test/aop5/aop.xml");
    UserController userController = (UserController) ac.getBean("userController");
    TaskActiver taskActiver = (TaskActiver) userController;//①強制轉換成功,這是java多型中的向上轉型。
    System.out.println(taskActiver instanceof UserController);//②
    System.out.println(userController instanceof TaskActiver);//③
    taskActiver.startTask();//④
    userController.login("zenghao");//⑤
}

6. 結果分析

執行測試程式碼後,控制檯列印資訊:

true
true
任務開啟
I’m zenghao ,I’m logining

接下來我們分析測試方法中註釋編號相應的程式碼行:
1. ①中使用了向上轉型,向上轉型會使父類(taskTactiver)丟失了子類(userController)的方法,但這裡的關鍵是,taskActiver成為了userController的父類,這是通過引介增強實現的,即我們為UserController添加了新的實現介面taskActiver
2. ②結果為true是顯然的,因為我們的taskActiver就是由userController轉型而來
3. ③恰恰驗證了我們①中的結論:taskActiver成為了userController的父類
4. ④我們通過父類介面呼叫其增強實現類中的startTask方法,這點可以通過列印資訊“任務開啟”來說明。在這裡,我們也得到一個結論,我們為目標類引介新的代理介面,實際上是讓目標類新增了新的代理介面的增強實現類中的功能
5. ⑤模擬我們的web請求,這裡不再像我們開始例子那樣,能夠實現耗時統計,儘管我們在④中開啟了任務(事實上,從增強實現類的定義來看,這是顯然的結果),也就是說,我們只是為目標物件添加了新的介面,但要使用相應介面的功能還要通過向上轉型來完成,而向上轉型後父介面也丟失了目標物件的屬性方法,因而兩者是相對獨立的。從這個角度講,我個人覺得接引增強的這種配置實現實際意義並不大。

基於Schema配置

使用這種方法我們主要需要配置<aop:declare-parents>標籤屬性。同上例的實現效果,使用基於Schema的配置,可去除切面類MyTaskAspect,同時重新配置xml檔案:

<bean id="userController" class="test.aop6.UserController" />
<aop:config proxy-target-class="true">
    <aop:aspect>
        <!-- 
1. types-matching:指向我們的目標物件類,這裡可以使用我們的AspctJ統配符如+、\*、和..等萬用字元
2. implement-interface:指向我們的代理介面
3. default-impl指向我們實現了代理介面的增強類。這裡使用全限定名的方式
4. <aop:declare-parents>裡的還有另一個屬性delegate-ref,它指向我們註冊好的另外一個Bean名稱
-->
        <aop:declare-parents types-matching="test.aop6.UserController"
            implement-interface="test.aop6.TaskActiver" default-impl="test.aop6.MyTaskController" />
    </aop:aspect>
</aop:config>

運行同樣的測試檔案,我們會得到相同的結果。

在這裡,我們要注意:
1. 一定要將proxy-target-class配置成true,否則引介增強配置失敗,會報異常:java.lang.ClassCastException: com.sun.proxy.\$Proxy0 cannot be cast to test.aop6.UserController
2. 因為我們引介增強是類級別的,不用專門配置切面,即也無需在<aop:aspect>中宣告ref=”切面Bean名稱”

原始碼下載

相關推薦

spring引介增強定時例項侵入動態增強功能

引介增強例項需求在前面我們已經提到了前置、後置、環繞、最終、異常等增強形式,它們的增強物件都是針對方法級別的,而引介增強,則是對類級別的增強,我們可以通過引介增強為目標類新增新的屬性和方法,更為誘人的是,這些新屬性或方法是可以根據我們業務邏輯需求而動態變化的。怎

spring整合quartz2定時啟動報batch acquisition of 0 triggers

這是條日誌輸出,輸出這樣的內容表示目前沒有要執行的trigger,這不是錯,而且此時任務不執行。 如果要執行,看配置的cron時間,讓它配置成在當前一會可執行的時間就行了。 DEBUG or

spring自帶定時

author google scheduled xmlns tasks ogl 位置 .org 執行 http://www.cnblogs.com/pengmengnan/p/6714203.html 註解模式的spring定時器1 , 首先要配置我們的spring

spring整合Quartz定時

() void java類 info throws tex execute new protect 第一種:為普通java類中的某個方法配置跑批任務 1.定義要跑批的類和方法: package com.xy.utils.quartz; import org.joda.t

Spring自帶定時實現定時任務

str esc exec count nis 開始 針對 exe 結束 在Spring框架中實現定時任務的辦法至少有2種(不包括Java原生的Timer及Executor實現方式),一種是集成第三方定時任務框架,如無處不在的Quartz;另一種便是Spring自帶的定時器(

spring框架中定時的配置及應用

首先我們來簡單瞭解下定時器:  1. 定時器的作用             在實際的開發中,如果專案中需要定時執行或者需要重複執行一定的工作,定時器

spring boot 中定時的使用

有時候,我們需要我們的專案定時的去執行一些方法 要想在spring boot 中使用定時器,其實非常簡單 第一步: 在spring boot的入口處新增@EnableScheduling這個註解,如下 @SpringBootApplication @EnableScheduling

Spring中使用定時步驟

1.我們建立定時器的配置檔案:     spring-quartz.xml 2.引入頭部 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.or

關於Spring中用quartz定時定時到達時同時執行兩次的問題

我在使用spring的quartz配置定時任務時,發現每次定時時間到達後,指定的定時方法同時執行兩次,而且此方法還是使用的synchronized關鍵字,每次定時一到,會發現此方法內的System.out輸出資訊輸出兩次,說明方法在這時執行了兩次,解決方法沒有找到更好的,不過

Keras多層感知例項印第安人糖尿病診斷

本例項使用Keras在python中建立一個神經網路模型,這是一個簡單的貫序模型,也是神經網路最常見的模型。本例項按照以下步驟建立: 1. 匯入資料。 2.定義模型 3.編譯模型 4.訓練模型 5.評估模型 6.彙總程式碼 Pima Indians糖尿病發病情況

微控制器--按鍵定時檢測短按、長按

 通過使用定時器計數的方法來分辨按鍵的:短按、長按 檢測到引腳被拉低:按鍵按下為低電平,沒有按下為高電平 延時10毫秒:濾波 引腳還是被拉低:確定按鍵被按下 設定按鍵按下標誌 開啟定時器,開始計數:定時器中有一個全域性變數用於記錄計數值 直到引腳

java spring-mvc spring-boot quartz定時

1.java中定時任務類。java.util.Timer  java.util.TimerTask package com.jiayun.demo; import java.util.Timer; import java.util.TimerTask; public c

關於Spring容器中定時到時執行會出現兩個執行緒同時執行的問題

最近公司有一個小需求,是需要定時去從某一個視訊供應商下載視訊檔案,問題很簡單,直接使用quartz,編寫相應的定時器程式碼,同時配置相應的定時器時間,但是在定時執行之後會出現兩個執行緒同時執行定時任務的問題,並且這兩個執行緒併發執行,從而一直影響到視訊檔案下載。

spring中crontab定時 的表示式

0 0 10,14,16 * * ? 每天上午10點,下午2點,4點 0 0/30 9-17 * * ?   朝九晚五工作時間內每半小時 0 0 12 ? * WED 表示每個星期三中午12點 "0 0 12 * * ?" 每天中午12點觸發 "0 15 10 ? * *" 每天上午10:15觸發 "0 15

關於Spring中的定時配置

講這個之前,我們先講講定時器。 從實現技術上來看,定時器分為三種: 1.Timer比較單一,這個類允許你排程一個java.util.TimerTask任務。使用這種方式可以讓你的程式按照某一個頻度執行,但不能在指定時間執行,一般用的較少。 2.Quartz

spring整合quartz定時的專案中,如何關閉不斷輸出的batch acquisition of 0 triggers ?

不斷輸出的batch acquisition of 0 triggers太鬧心了,嚴重影響了除錯效率,不能忍,經過查閱資料得出關閉方法。希望幫助更多的小夥伴。解決方法:在pom.xml中看看使用的是哪個

Spring的quartz定時同一時刻重複執行二次的問題解決

最近用Spring的quartz定時器的時候,發現到時間後,任務總是重複執行兩次,在tomcat或jboss下都如此。 打印出他們的hashcode,發現是不一樣的,也就是說,在web容器啟動的時候,重複啟了兩個quartz執行緒。 研究下來發現quartz確實會載入兩次: 第一次:web容器啟動的時候,

spring多個定時的寫法

<?xml version="1.0" encoding="utf-8"?> <beans xmlns="http://www.springframework.org/schema/beans"     xmlns:xsi="http://www.w3.org/2001/XMLSchema-

spring配置quartz定時任務

解決方案: 將CronTriggerBean修改為CronTriggerFactoryBean。如下所示: <!-- 配置觸發器 --> <bean id="te

quartz-2.2.2.jar + spring 3.2 定時配置

 <!-- 任務類 -->     <bean id="TaskSendMsmToDriver" class="com.halis.souhuo.uc.model.job.TaskSendMsmOfAllDriverService">      &l