詳解Spring AOP 底層原理

AOP

AOP的實現一般都是基於 代理模式 ,在JAVA中採用JDK動態代理模式,但是我們都知道,JDK動態代理模式只能代理接口而不能代理類。因此,Spring AOP 同時支持了 CGLIB、ASPECTJ、JDK動態代理。在不同的場景選擇不同的代理方法來實現AOP,開發者也無需關心其選擇過程.

  • 如果目標對象實現了接口,Spring AOP 將會默認採用 JDK 動態代理來生成 AOP 代理類;
  • 如果目標對象沒有實現接口,Spring AOP 將會選擇採用 CGLIB 來生成 AOP 代理類;

 

代理模式

我們知道AOP思想的實現一般都是基於 代理模式 ,所以非常有必要先了解一下靜態代理以及JDK動態代理CGLIB動態代理的實現方式。

代理模式這種設計模式是一種使用代理對象來執行目標對象的方法並在代理對象中增強目標對象方法的一種設計模式。

可以看到還是很簡單的,代理類實現了被代理類的接口,同時與被代理類是組合關係。下面看一下代理模式的幾種實現.

靜態代理

package aop.proxy.staticProxy;
//接口
public interface UserDao {

	void add();
	void delete();
	void update();
	void query();

}
package aop.proxy.staticProxy;
//目標對象(被代理的對象)
public class UserDaoImp implements UserDao {

	@Override
	public void add() {
		System.out.println("目標對象正在執行add方法");
	}

	@Override
	public void delete() {
		System.out.println("目標對象正在執行delete方法");
	}

	@Override
	public void update() {
		System.out.println("目標對象正在執行update方法");
	}

	@Override
	public void query() {
		System.out.println("目標對象正在執行query方法");
	}
}
package aop.proxy.staticProxy;


/**
 * @Author younus
 * @Description // 目標的代理對象
 ** @Param
 **/
public class UserDaoImpProxy  implements UserDao{
	UserDaoImp userDaoImp =null;

	public UserDaoImpProxy(UserDaoImp userDaoImp){
		this.userDaoImp=userDaoImp;
	}

	@Override
	public void add() {
		System.out.println("add執行前記錄日誌--");
		userDaoImp.add();
	}

	@Override
	public void delete() {
		System.out.println("delete執行前記錄日誌--");
		userDaoImp.delete();
	}

	@Override
	public void update() {
		System.out.println("update執行前記錄日誌--");
		userDaoImp.update();
	}

	@Override
	public void query() {
		System.out.println("query執行前記錄日誌--");
		userDaoImp.query();
	}
}

不難看出,靜態代理其實就是將增強功能寫死了在代理對象中執行的形式,每次要在接口中添加一個新方法,則需要在目標對象中實現這個方法,並且在代理對象中實現相應的代理方法,然而我們可以使用Java的反射技術,實現動態代理。

JDK動態代理

在講JDK的動態代理方法之前,不妨先想想如果讓你來實現一個可以代理任意類的任意方法的代理類,該怎麼實現?大概是用反射來獲取被代理對象及其方法,再執行該方法,並在執行前後添加一些增強方法,這樣似乎有點naive,讓我們一起看看JDK的動態代理吧.

首先介紹一下最核心的一個接口和一個方法:

1.java.lang.reflect包裏的InvocationHandler接口:

public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

 

我們對於被代理的類的操作都會由該接口中的invoke方法幫我們實現,其中的參數的含義分別是:

  • proxy:被代理類的實例
  • method:需要被代理的方法
  • args:方法需要的參數(即參數method的入參)

我們可以在invoke方法中調用被代理類的方法並獲得返回值,自然也可以在調用該方法的前後去做一些額外的事情,從而實現動態代理.

 

2.另外一個很重要的靜態方法是java.lang.reflect包中的Proxy類的newProxyInstance方法:

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
    throws IllegalArgumentException

該方法會返回一個被修改過(增強)的被代理對象的實例,從而可以自由的調用該實例的方法.其參數如下

  • loader:被代理的類的類加載器
  • interfaces:被代理類的接口數組(實現的接口)
  • h:攔截方法的句柄,即在上文中提到的實現增強功能的接口

直接上實例:

package aop.proxy.jdkProxy;

import aop.proxy.staticProxy.UserDao;
import aop.proxy.staticProxy.UserDaoImp;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class JdkProxy {

	public static void main(String[] args) {
		UserDao userDao = new UserDaoImp();//被代理對象
		//獲得代理對象
		UserDao proxyObject = (UserDao) Proxy.newProxyInstance(userDao.getClass().getClassLoader(),
				userDao.getClass().getInterfaces(), new InvocationHandler() {
			@Override
			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
				System.out.println("在代理對象中攔截方法:"+method.getName());
				System.out.println("執行前--------");
				Object o=method.invoke(userDao,args); //調用攔截方法
				System.out.println("執行完成---------");
				return o;
			}
		});

		proxyObject.add(); //通過代理對象執行方法add

	}
}

 

由於jdk動態代理只能代理實現了某些接口的對象,所以在獲取的代理對象的聲明類型一定是 接口類型,而不是UserDaoImp實現類.

可以看到對於不同的實現類來說,可以用同一個動態代理類來進行代理,實現了“一次編寫到處代理”的效果。但是這種方法有個缺點,就是被代理的類一定要是實現了某個接口的,這很大程度限制了本方法的使用場景。

Cglib動態代理

CGlib是一個字節碼增強庫,爲AOP等提供了底層支持。下面看看它是怎麼實現動態代理的。

首先需要導入兩個包支持:

package aop.proxy.cglibProxy;

import aop.proxy.staticProxy.UserDao;
import aop.proxy.staticProxy.UserDaoImp;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CglibProxy {

	public static void main(String[] args) {
		UserDao userDao =new UserDaoImp();
		Enhancer enhancer=new Enhancer();
		//設置代理對象的父類
		enhancer.setSuperclass(userDao.getClass());
		//設置回掉方法
		enhancer.setCallback(new MethodInterceptor() {
			@Override
			public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
				System.out.println("在代理對象中攔截到:"+method.getName());
				System.out.println("執行前+++++++");
				Object object=method.invoke(userDao,objects);
				System.out.println("執行後+++++++");
				return object;
			}
		});
		UserDao proxy=(UserDao) enhancer.create();
		proxy.delete();



	}
}

可以看到,Cglib實現代理的方式是和目標對象使用同一個父類,無論是繼承還是實現接口,都是爲了代理對象能直接調用目標對象的方法。這種方法可以做到代理任何一個對象的任何方法,不再有jdk動態代理的接口限制.

 

 AOP基本概念

面向切面AOP——Spring提供了面向切面編程的豐富支持,允許通過分離應用的業務邏輯與系統級服務進行內聚性的開發。應用對象只實現它們應該做的——完成業務邏輯。它們並不負責其它的系統級關注點,例如日誌或事務支持,但我們可以利用AOP來實現這些系統級關注點。

當你開發一個登陸功能,你可能需要在用戶登陸前後進行權限校驗並將校驗信息(用戶名,密碼,請求登陸時間,ip地址等)記錄在日誌文件中,當用戶登錄進來之後,當他訪問某個其他功能時,也需要進行合法性校驗.當系統非常地龐大,系統中專門進行權限驗證的代碼是非常多的,而且非常地散亂,我們就想能不能將這些權限校驗、日誌記錄等非業務邏輯功能的部分獨立拆分開,並且在系統運行時需要的地方(連接點)進行動態插入運行,不需要的時候就不使用.下圖可以直觀的看到業務邏輯與系統級功能的關係,我們可以使用aop來實現它.

 

 

基本概念: 

  1. 通知(Advice),有5種通知類型
  • before 在目標方法執行之前被調用
  • after 在目標方法完成後調用通知,不管方法是否執行成功
  • after-running 在方法成功執行後調用通知
  • after-throwing 在方法拋出異常之後調用通知
  • around 包圍目標方法,在方法調用前後都調用通知 

通知並不是某個類或者某個方法, 通知是用來告訴我們(告訴系統何)何時執行切入,規定一個時間,在系統運行中的某個時間點(比如拋異常啦!方法執行前啦!).

    2.切點(pointcut)

切點在Spring AOP中可以對應到系統中的某個(某些)方法,指定要切入哪一個方法中.

   3.連接點(jointpoint)

  在程序執行過程中的任何時間點都可以作爲織入點(連接點),如方法調用,方法執行,字段設置,異常處理,類初始化,循環中的某個點等.Spring AOP 目前僅支持方法執行(method execution) .可以理解爲, 連接點就是準備在系統中執行切點和切入通知的地方(一般是一個方法或一個字段).

   4.切面(Aspect)

Advice 和 Pointcut定義了一個切面Aspect.Advice定義了Aspect的任務和什麼時候執行它,而切入點Pointcut定義在哪裏具體地方切入,也就是說,Aspect定義了它是什麼東西 什麼時候切入和在哪裏切入。

  5.引入(introduction)

 允許我們向現有的類添加新的方法或者屬性

  6.織入(weaving)

 Weaving是一個混合橫向方面到目標業務對象的過程,織入可以是在編譯時間,也可以在運行時間使用classload,Spring AOP缺省是在運行時間。

 

 

Spring AOP

Spring的AOP實現內部機制:

  • jdk動態代理

使用到的類和接口:Proxy,InvocationHandler (只能對接口進行代理)

  •  CGLIB代理(三方庫) 可以直接對任意class進行代理
  • AspectJ需要用到特定的編譯器,在編譯時期混合代碼

引入一個實例:

spring-AOP.xml

<?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:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">

    <bean id="userAction1" class="springAOP.UserActionImpl"/>
    <bean id="userAction2" class="springAOP.UserActionImpl" />
    <bean id="aspect" class="springAOP.Aspect"/>

    <aop:config>
        <aop:aspect ref="aspect" >
            <!--定義切點-->
            <aop:pointcut id="pointcut" expression="execution(* springAOP.*.*(..))"/>
            <!--定義通知-->
            <aop:before method="permissionCheck" pointcut-ref="pointcut"/>
            <aop:after method="logger" pointcut-ref="pointcut"/>
            <aop:after-returning method="saveInfo" pointcut-ref="pointcut"/>
        </aop:aspect>
    </aop:config>
</beans>

接口類:

package springAOP;

public interface UserAction {

	void  login(String username);

	void  download();

}

實現類:

package springAOP;

import aop.proxy.staticProxy.UserDao;

public class UserActionImpl implements UserAction {
	@Override
	public void login(String username) {
		System.out.println(username+"正在登陸中");
	}

	@Override
	public void download() {
		System.out.println("正在下載文件中");
	}
}

 

切面:

package springAOP;

/**
 * @Author younus
 * @Description // 切面類
 * @Param
 **/
public class Aspect {


	public void  logger(){
		System.out.println("正在記錄日誌----------");
	}

	public void  permissionCheck(){
		System.out.println("正在校驗權限------------");
	}

	public void  saveInfo(){
		System.out.println("正在保存用戶訪問記錄=============");
	}
}

 

測試代碼:

package springAOP;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class AOPTest {

	public static void main(String[] args) {
		ApplicationContext context= new ClassPathXmlApplicationContext("spring-AOP.xml");
		UserAction action1 =(UserAction)context.getBean("userAction1");
		UserAction action2 =(UserAction) context.getBean("userAction2");
		action1.login("用戶1");
		action1.download();

		System.out.println("----------------------------------------------------------------");
		
		action2.login("用戶2");
		action2.download();
	}
}

除了spring的包之外記得還要引入額外兩個包:

執行結果:

 

我本人在測試時,由於xml中切入點表達式寫錯導致了異常,如果對於expression寫法不瞭解可以去這裏查看:

expression 表達式的具體寫法:AOP expression語法

 

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

 

基於註解的AOP實現

首先,不要忘記導入jar包

spring-AOP.xml文件 

<?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:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop-4.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">


   <!--掃描包-->
    <context:component-scan base-package="annotationAOP"/>
    <!--啓用自動代理,自動爲切面方法中匹配的方法所在的類生成代理對象-->
    <aop:aspectj-autoproxy>
    </aop:aspectj-autoproxy>
</beans>

接口文件:

package annotationAOP;

public interface Calculator <T extends Number>{

	T  add(T t1,T t2);


	T  subtract(T t1, T t2);


	T  divide(T t1,T t2) ;

	T  multiply(T ti,T t2);
}

實現類:(不要忘記加@Component註解 ,將其交由spring管理)

package annotationAOP;

import org.springframework.stereotype.Component;

@Component("calculatorImp")
public class CalculatorImp  implements  Calculator{
	@Override
	public Number add(Number t1, Number t2) {
		return t1.doubleValue()+t2.doubleValue();
	}

	@Override
	public Number subtract(Number t1, Number t2) {
		return t1.doubleValue()-t2.doubleValue();
	}

	@Override
	public Number divide(Number t1, Number t2) {
		if (t2.doubleValue()==0)
			throw new  ArithmeticException("/ by 0");
		return t1.doubleValue()/t2.doubleValue();
	}

	@Override
	public Number multiply(Number t1, Number t2) {
		return t1.doubleValue()*t2.doubleValue() ;
	}
}

 

切面類:(這裏將annotationAOP包下的所有方法都切入),注意@Component與@Aspect(將其標註爲切面類)

package annotationAOP;


import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Arrays;

@Component
@Aspect
public class LogAspect {

	@Before(value = "execution(* annotationAOP.*.*(..))")
	public void  before(JoinPoint joinPoint){
		System.out.println("[前置通知]方法"+joinPoint.getSignature().getName()+"運行前,"+"參數"+ Arrays.asList(joinPoint.getArgs()));
	}

	@AfterReturning(value = "execution(* annotationAOP.*.*(..))",returning = "result")
	public void afterReturning(JoinPoint joinPoint,Object result ){
		System.out.println("[返回通知]方法"+joinPoint.getSignature().getName()+"正常返回,返回值"+result);

	}

	@AfterThrowing(value = "execution(* annotationAOP.*.*(..))",throwing = "e")
	public void  afterThrowing(JoinPoint joinPoint,Exception e){
		System.out.println("[異常通知]方法"+joinPoint.getSignature().getName()+"拋出異常"+e.getMessage());
	}

	@After(value = "execution(* annotationAOP.*.*(..))")
	public  void after(JoinPoint joinPoint){
		System.out.println("[後置通知]方法"+joinPoint.getSignature().getName()+"運行結束");
	}

}

測試類:

package annotationAOP;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test {
	public static void main(String[] args) {

		ApplicationContext context =new ClassPathXmlApplicationContext("spring-AOP.xml");
		Calculator calculator=(Calculator) context.getBean("calculatorImp");

		Number result1 =calculator.add(1,2);
		System.out.println("結果:"+result1);

		System.out.println("--------------------------------------------------");
        //這個方法會報拋出異常
		Number result2=calculator.divide(3,0);
		System.out.println("結果:"+result2);
		System.out.println("----------------------------------------------------");

	}
}

測試結果:可以看到我們的切面方法都織入到了目標方法中

 

環繞通知:

切面類:(環繞通知)

package annotationAOP;


import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Arrays;

@Component
@Aspect
public class LogAspect {

	@Around(value = "execution(* annotationAOP.*.*(..))")
	public Object  around(ProceedingJoinPoint proceedingJoinPoint){
		Object result = null;
		try {
			System.out.println("[環繞通知-->前置通知]方法" + proceedingJoinPoint.getSignature().getName() + "運行前," + "參數" + Arrays.asList(proceedingJoinPoint.getArgs()));
			result = proceedingJoinPoint.proceed();
			System.out.println("[環繞通知-->返回通知]方法" + proceedingJoinPoint.getSignature().getName() + "運行結束,返回" + result);
		} catch (Throwable e) {
			System.out.println("[環繞通知-->異常通知]方法" + proceedingJoinPoint.getSignature().getName() + "拋出異常" + e.getLocalizedMessage());
		}
		System.out.println("[環繞通知-->後置通知]方法" + proceedingJoinPoint.getSignature().getName() + "運行結束");
		return result;
	}

}

測試結果:

可以觀察到,前置通知和後置通知始終都在執行,不管程序返回還是異常;而返回通知與異常通知中只有其中一個會運行;

 

 

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