深入理解spring中的AOP原理 —— 實現MethodInterceptor接口,自已動手寫一個AOP


 

1.前言

       AOP是面向切面編程,即“Aspect Oriented Programming”的縮寫。面對切面,就是面向我們的關注面,不能讓非關注面影響到我們的關注面。而現實中非關切面又必不可少,例如獲取資源、釋放資源、處理異常、記錄日志等,太多的非關切面會讓關切面的代碼變得雜糅,難以維護。此時面向切面編程便是解決此問題的方案,減少非關切面的東西,讓我們只專注於核心業務代碼。而要理解AOP,就必須要懂得代理模式是啥,能干啥,特別要清楚Cglib動態代理是咋回事(不懂的可以看這篇文章)。

2.實現過程

    為了直觀看到各個類,我將示例所需的類作為靜態內部類放在外層類中。

package demo;
 
import java.util.Arrays;
 
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.ProxyFactory;
 
public class SpringAop {
 
    public static class UserDaoImpl {
        public int addUser(String user) {
            System.out.println("保存了一個用戶 " + user);
            return 1;
 
        }
 
        public int deleteUser(int id) {
 
            if (id <= 0 || id > 99999999) {
                throw new IllegalArgumentException("參數id不能大於99999999或小於0");
            }
 
            System.out.println("刪除了id為" + id + "用戶");
            return 1;
        }
    }
 
    /**
     * 自定義的方法攔截器
     *
     */
    public static class UserMethodInterceptor implements MethodInterceptor {
 
        @Override
        public Object invoke(MethodInvocation mi) throws Throwable {
            String methodName = mi.getMethod().getName();// 方法名
            Object returnVal = null;
 
            /*
             * 根據方法名的不同設置不同的攔截策類 似於配置文件中的<aop:pointcut
             * expression="execution( xxxmethodName)"/>,以此定義切入點。
             *
             * spring框架中可以根據方法對應的包、對應的參數、返回值、方法名等多個條件,並結合正則表達式來定義切入點
             * 為了簡單化,此處我只用方法名的前綴來定義切入點。
             *
             */
 
            if (methodName.startsWith("add")) {
 
                returnVal = beforeEnhance(mi);
 
            } else if (methodName.startsWith("delete")) {
                returnVal = afterThrowingEnhance(mi);
            } else {
                returnVal = mi.proceed();
            }
 
            return returnVal;
        }
 
        /*
         * 前置增強策略
         */
        private Object beforeEnhance(MethodInvocation mi) throws Throwable {
            /*
             * spring框架通過配置文件或注解來獲得用戶自定義的增強方法名及對其JavaBean的類名, 然后通過反射機制動態地調用用戶的增強方法。
             * 為了簡單化的測試,我這里將增強方法及對應的類給定死了、不能改動, 增強JavaBean就是ConsoloEnhancer,
             * 增強方法就是"public void before(MethodInvocation mi)"
             *
             */
            new ConsoloEnhancer().before(mi); // 調用用戶定義的前置處理方式
            Object returnVal = mi.proceed(); // 執行目標方法
            return returnVal;
        }
 
        /*
         * 異常處理策略
         */
        private Object afterThrowingEnhance(MethodInvocation mi) throws Throwable {
            try {
                return mi.proceed();// 執行目標方法
            } catch (Throwable e) {
                new ConsoloEnhancer().afterThrowing(mi, e); // 調用用戶定義的異常處理方式
                throw e;
 
            }
 
        }
    }
 
    /**
     * 定義包含增強方法的JavaBean
     *
     */
    public static class ConsoloEnhancer {
        /**
         * 前置增強方法
         *
         * @param mi
         */
        public void before(MethodInvocation mi) {
            /*
             * 這里的MethodInvocation類型參數mi和開發中常用的切點工具類JoinPoint的作用類似,它可以獲取
             * 目標對象("Object getThis()"方法)、目標方法("Method getMethod()"方法)、
             * 方法參數("Object[] getArguments()"方法)等信息
             */
            System.out.print("調用了" + mi.getThis() + "對象的" + mi.getMethod().getName() + "方法。方法入參原始值是"
                    + Arrays.toString(mi.getArguments()) );
            if (mi.getThis().getClass() == UserDaoImpl.class) {         //對方法入參進行修改
                Object[] args = mi.getArguments();
                String enhancedAgr ="user-" +(String) args[0]  ;
                args[0] = enhancedAgr;
            }
            System.out.println(".而增強后的方法入參是"+Arrays.toString(mi.getArguments()) );
            System.out.println("*********************前置增強加星號 *********************");
        }
 
        /**
         * 處理異常的增強方法
         *
         * @param mi
         * @param e
         */
        public void afterThrowing(MethodInvocation mi, Throwable e) {
            System.out.println("調用了" + mi.getThis() + "對象的" + mi.getMethod().getName() + "方法,當方法入參是"
                    + Arrays.toString(mi.getArguments()) + "時,發生了異常。");
            System.out.println("異常的堆棧信息是" + Arrays.toString(e.getStackTrace()));
            System.out.println("!!!!!!!!!!!!!!!!!!!!!異常加感嘆號!!!!!!!!!!!!!!!!!!!!!!");
        }
 
    }
 
    public static void main(String[] args) {
        UserDaoImpl userDaoImpl = new UserDaoImpl();
        ProxyFactory proxyFactory = new ProxyFactory(); // 初始化一個代理工廠
        // 設置目標類,以便於Cglib工具包動態生成目標類的子類,即我們所需的代理類
        proxyFactory.setTarget(userDaoImpl);
        /*
         * 設置攔截器,而攔截器的"public Object invoke(MethodInvocation mi)"定義了
         * 代理類(實際是UserDaoImpl的子類)的方法生成策略。
         *
         */
        proxyFactory.addAdvice(new UserMethodInterceptor());
 
        // 向上轉型,轉型為父類類型
        UserDaoImpl proxyUserDaoImpl = (UserDaoImpl) proxyFactory.getProxy();
 
        /**
         * 調用代理方法
         */
        proxyUserDaoImpl.deleteUser(2);  //應該正常執行,沒有什何增強效果
        System.out.println("");// 換行
        proxyUserDaoImpl.addUser("李華");  //入參值會被修改
        System.out.println("");// 換行
        proxyUserDaoImpl.deleteUser(-1);  // 將異常處理效果
 
    }
}
各個內部類

 

 實現細節

1)先定義一個業務類。

這個簡單的業務類有保存用戶、刪除用戶的功能。

	public static class UserDaoImpl {
		public int addUser(String user) {
			System.out.println("保存了一個用戶 " + user);
			return 1;

		}

		public int deleteUser(int id) {

			if (id <= 0 || id > 99999999) {
				throw new IllegalArgumentException("參數id不能大於99999999或小於0");
			}

			System.out.println("刪除了id為" + id + "用戶");
			return 1;
		}
	}

 

2)自定義一個通知(實際上是一個攔截器)

         這個Advice通知實現了 MethodInterceptor接口,而MethodInterceptor接口繼承了Interceptor接口,Intercepto接口又繼承了Advice接口,因此我個將這攔截器稱為一個通知。

為了偷懶,此處的Advice通知一次性實現了兩個功能,前置增強和后異常處理增強,明顯不滿足“單一職責”的原則,此處同時要處理兩種增強。而spring框架中,當讀到配置文件的"<aop:before>"標簽,就安排一個AspectJMethodBeforeAdvice類(繼承於AbstractAspectAdive抽象類,此抽象類實現了Advice接口)去處理前置增強;當讀到配置文件的"<aop:after-throwing>"標簽,則會安排一AspectJAfterThrowingAdvice類(實現了MethodInterceptor接口)去做異常增強處理。spring框架切實做到了遵循”單一職責“原則,專門組織一個類去處理一種類型(前置、后置或最終等)的增強。

         另外需要注意的是:這里的MethodInterceptor接口是位於"org.aopalliance.intercept"包,除此之外Cglib工具包中的"org.springframework.cglib.proxy"包也有MethodInterceptor接口,但這個接口不是我們所需要的。

 

 Advice接口結構體系圖(配圖引用自"spring適配器模式-aop中的MethodInterceptor攔截器")

	public static class UserMethodInterceptor implements MethodInterceptor {

		@Override
		public Object invoke(MethodInvocation mi) throws Throwable {
			String methodName = mi.getMethod().getName();// 方法名
			Object returnVal = null;

			/*
			 * 根據方法名的不同設置不同的攔截策類 似於配置文件中的<aop:pointcut
			 * expression="execution( xxxmethodName)"/>,以此定義切入點。
			 *
			 * spring框架中可以根據方法對應的包、對應的參數、返回值、方法名等多個條件,並結合正則表達式來定義切入點
			 * 為了簡單化,此處我只用方法名的前綴來定義切入點。
			 *
			 */
			if (methodName.startsWith("add")) {

				returnVal = beforeEnhance(mi);

			} else if (methodName.startsWith("delete")) {
				returnVal = afterThrowingEnhance(mi);
			} else {
				returnVal = mi.proceed();
			}

			return returnVal;
		}

		/*
		 * 前置增強策略
		 */
		private Object beforeEnhance(MethodInvocation mi) throws Throwable {
			/*
			 * spring框架通過配置文件或注解來獲得用戶自定義的增強方法名及對其JavaBean的類名, 然后通過反射機制動態地調用用戶的增強方法。
			 * 為了簡單化的測試,我這里將增強方法及對應的類給定死了、不能改動, 增強JavaBean就是ConsoloEnhancer,
			 * 增強方法就是"public void before(MethodInvocation mi)"
			 * 
			 */
			new ConsoloEnhancer().before(mi); // 調用用戶定義的前置處理方式
			Object returnVal = mi.proceed(); // 執行目標方法
			return returnVal;
		}

		/*
		 * 異常處理策略
		 */
		private Object afterThrowingEnhance(MethodInvocation mi) throws Throwable {
			try {
				return mi.proceed();// 執行目標方法
			} catch (Throwable e) {
				new ConsoloEnhancer().afterThrowing(mi, e); // 調用用戶定義的異常處理方式
				throw e;

			}

		}
	}

 

3) 用戶定義包含增強方法的JavaBean

MthodInvocation接口比較有意思,它的祖先級接口是Joinpoint ,注意不是開發中的JoinPoint,但MthodInvocation封裝的信息和JoinPoint差不多,都包含目標對象、方法的參數、目標方法等。

	public static class ConsoloEnhancer {
		/**
		 * 前置增強方法
		 * 
		 * @param mi
		 */
		public void before(MethodInvocation mi) {
			/*
			 * 這里的MethodInvocation類型參數mi和開發中常用的切點工具類JoinPoint的作用類似,它可以獲取
			 * 目標對象("Object getThis()"方法)、目標方法("Method getMethod()"方法)、
			 * 方法參數("Object[] getArguments()"方法)等信息
			 */
			System.out.print("調用了" + mi.getThis() + "對象的" + mi.getMethod().getName() + "方法。方法入參原始值是"
					+ Arrays.toString(mi.getArguments()) );
			if (mi.getThis().getClass() == UserDaoImpl.class) {			//對方法入參進行修改
				Object[] args = mi.getArguments();
				String enhancedAgr ="user-" +(String) args[0]  ;
				args[0] = enhancedAgr;
			}
			System.out.println(".而增強后的方法入參是"+Arrays.toString(mi.getArguments()) );
			System.out.println("*********************前置增強加星號 *********************");
		}

		/**
		 * 處理異常的增強方法
		 * 
		 * @param mi
		 * @param e
		 */
		public void afterThrowing(MethodInvocation mi, Throwable e) {
			System.out.println("調用了" + mi.getThis() + "對象的" + mi.getMethod().getName() + "方法,當方法入參是"
					+ Arrays.toString(mi.getArguments()) + "時,發生了異常。");
			System.out.println("異常的堆棧信息是" + Arrays.toString(e.getStackTrace()));
			System.out.println("!!!!!!!!!!!!!!!!!!!!!異常加感嘆號!!!!!!!!!!!!!!!!!!!!!!");
		}

	}

  

 

4)方法測試

	public static void main(String[] args) {
		UserDaoImpl userDaoImpl = new UserDaoImpl();
		ProxyFactory proxyFactory = new ProxyFactory(); // 初始化一個代理工廠
		// 設置目標類,以便於Cglib工具包動態生成目標類的子類,即我們所需的代理類
		proxyFactory.setTarget(userDaoImpl);
		/*
		 * 設置攔截器,而攔截器的"public Object invoke(MethodInvocation mi)"定義了
		 * 代理類(實際是UserDaoImpl的子類)的方法生成策略。
		 * 
		 */
		proxyFactory.addAdvice(new UserMethodInterceptor());

		// 向上轉型,轉型為父類類型
		UserDaoImpl proxyUserDaoImpl = (UserDaoImpl) proxyFactory.getProxy();

		/**
		 * 調用代理方法
		 */
		proxyUserDaoImpl.deleteUser(2);  //應該正常執行,沒有什何增強效果
		System.out.println("");// 換行
		proxyUserDaoImpl.addUser("李華");  //入參值會被修改
		System.out.println("");// 換行
		proxyUserDaoImpl.deleteUser(-1);  // 將異常處理效果

	}

  控制台輸出

3.總結

    spring框架的AOP非常強大,可以實現前置增強、后置增強、最終增強、環繞增強等處理,另外還可以對方法入參進行控制過濾,而影響目標方法的執行中的狀態。AOP都是基於(Cglib)代理模式實現的,其中的關鍵點在於實現MethodInterceptor接口,在其“public Object invoke(MethodInvocation mi)”方法中制定代理方法的生成策略,而從此方法的MethodInvocation類型參數mi中可以獲得目標對象、方法的參數、目標方法等信息,根據這些信息可以精確地控制增強效果。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM