Spring AOP 原理剖析,这一篇就够

Spring AOP 学习记录

AOP(面向切面编程)方面的知识又是看了忘忘了看,今天有空记录下AOP的知识点。主要分为以下几方面:

​ 1、AOP相关术语

​ 2、基础知识及示例

​ 3、增强分类

1、AOP相关术语

连接点(Joinpoint) 一个类拥有一些边界性质的特定点,如一个类的各个方法就可称为连接点。
切点(Pointcut) 切点就是在众多连接点中选择自己感兴趣的连接点,如果将连接点比作一个数据库中的所有记录,那么切点就是一个查询语句的查询条件。
增强(Advice) 增强是置入目标连接点的一段代码,增强本身携带方位信息,如方法调用前、调用后、调用前后、异常抛出使等。
切面(Aspect) 切面由切点和增强组成,既包含增强逻辑和方位信息,也包含连接点信息。
目标对象(Target) 增强逻辑织入的目标类。
引介(Introduction) 引介是一种特殊的增强,它为类提供一些属性和方法。这样,即是一个业务类原本没实现某个接口,通过AOP的引介功能也可以动态的为该类添加接口实现逻辑,让该类成为接口的实现类。
织入(Weaving) 织入就是讲增强添加到目标类的具体方法上的过程。AOP有三种织入方式①编译期织入、②类装载期织入、③动态代理织入(Spring一般采用的方式)
代理(Proxy) 一个类被AOP织入增强后,就产生了一个代理结果类。它融合了增强逻辑,根据代理方式不同,代理类既可能是和原类具有相同接口的类或者是原类的子类。

通过以上概念我们可以想到通过定义切点、给切点织入增强逻辑等步骤,最后就可以完成切面编程。

2、基础知识及示例
2.1 JDK动态代理

​ Java JDK提供的动态代理技术允许开发者在运行期创建接口的代理类,JDK动态代理主要涉及java.lang.reflect包中的两个类:Proxy和InvocationHandler。

​ 其中,InvocationHandler是一个接口,可以通过实现该接口实现增强逻辑,并通过反射机制调用目标类代码,动态的将增强逻辑和业务逻辑编制在一起。Proxy利用InvocationHandler动态创建一个符合某一接口的示例,生成目标类的代理类。下面我使用代码演示。

// 下面假设我们有以下代码
public interface GreetService {
	void serviceTo(String name);
}

public class Waiter implements GreetService{

	@Override
	public void serviceTo(String name) {
         // 假设下面为代码时间性能监测代码
		// Long beginTime = System.currentTimeMillis();
		try {
			// 模拟程序在工作
			System.out.println("service to :"+name);
			Thread.sleep(10000L);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		// System.out.println("consume time:"+(System.currentTimeMillis()-beginTime));
	}
}


// 我们将上面代码的性能监控代码使用JDK动态代理织入
// 首先编写InvocationHandler实现类
public class WaiterHandler implements InvocationHandler{
	// 目标类
	private Object target;
	public WaiterHandler(Object target) {
		this.target = target;
	}
	// 代理调用方法
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		Long beginTime = System.currentTimeMillis();
		// 调用target的相应方法,并传入参数
		method.invoke(target,args);
		
		System.out.println("consume time : "+(System.currentTimeMillis()-beginTime));
		return null;
	}
}
public class MainTest {

	public static void main(String[] args) {
		// 创建GreetService实例,也就是希望被代理的业务类
		GreetService waiter = new Waiter();
		// 创建InvocationHandler实现类,为业务类织入增强逻辑
		WaiterHandler waiterHandler = new WaiterHandler(waiter);
		// 使用Proxy生成GreetService代理实例
		GreetService waiterProxy = (GreetService)Proxy.newProxyInstance(waiter.getClass().getClassLoader(), Waiter.class.getInterfaces(),waiterHandler);
		// 调用代理实例方法
		waiterProxy.serviceTo("fuhang");
	}
}
// output:
//  service to :fuhang
//  consume time : 10002
 

​ 上面为大家演示了一个使用JDK动态代理将性能监控代码从业务逻辑抽出然后动态织入的示例代码。

​ 从上面的示例代码中大家可能发现了几个问题:

​ 1、Waiter必须实现一个接口

​ 2、newProxyInstance(waiter.getClass().getClassLoader(), Waiter.class.getInterfaces(),waiterHandler);第二个参数必须传入Waiter实现的接口列表

由以上可以知道JDK创建代理有一个限制,它只能为接口创建代理实例,难道我们写一个业务类必须为其定义一个接口吗?对于没有通过接口定义业务方法的类,如何实现为其动态创建代理实例呢?下面我们看看下一个动态代理技术CGLib。

2.2CGLib动态代理

​ CGLib底层使用了动态字节码技术,可以为一个类创建子类,在子类中采用方法拦截技术拦截所有父类方法调用,并顺势织入增强逻辑。下面我们使用CGLib实现上面性能监控织入的逻辑。

// 首先我们创建类CglibProxy实现MethodInterceptor
public class CglibProxy implements MethodInterceptor{

	@Override
	public Object intercept(Object obj, Method arg1, Object[] arg2, MethodProxy methodProxy) throws Throwable {
		Long beginTime = System.currentTimeMillis();
		// 实际方法调用的地方
		methodProxy.invokeSuper(obj, arg2);
		
		System.out.println("consume time : "+(System.currentTimeMillis()-beginTime));
		return null;
	}
}

// 测试类
public class MainTest {

	public static void main(String[] args) {
		// 创建EnHancer对象
		Enhancer enhancer = new Enhancer();
        // 创建我们的CglibProxy代理对象
		CglibProxy cglibProxy = new CglibProxy();
		// 设置父类为Waiter
		enhancer.setSuperclass(Waiter.class);
        // 设置回调函数为CglibProxy
		enhancer.setCallback(cglibProxy);
		// 使用enhancer动态字节码技术创建Waiter的代理类
		Waiter waiter = (Waiter) enhancer.create();
		waiter.serviceTo("fuhang");
	}
}
// output:
//    service to :fuhang
//    consume time : 10044

​ 上面就是通过Cglib动态字节码技术为业务类生成子类的方式生成代理类,由此牵扯到Java知识点回顾,final和private修饰符修饰的类不能使用Cglib动态代理技术。

小结

​ SpringAOP技术底层就是使用动态代理技术为目标bean织入增强逻辑的,但是从以上两类型代理技术的示例代码中可以发现以下几个问题:

​ (1)、目标类被代理后,所有方法都加入了增强逻辑,有些时候这并不是我们想要的。

​ (2)、 通过手动编码方式指定了增强逻辑

​ (3)、 手工编写代理实例创建过程,为不同类创建代理时候需要手工编写相应的代理实例创建过程代码。

​ 所以Spring AOP的主要工作就是解决以上三个问题:Spring AOP 通过切点(Pointcut)指定在哪些类的哪些方法上织入增强逻辑,通过增强(Advice)描述具体的织入点(方法调用前、调用后、调用前后),Spring通过Advisor(切面)将切面和增强组织起来,下面Spring就可以利用动态代理技术采用统一的方式为目标bean织入增强了。

3、增强类型

​ Spring使用增强(Advice)定义增强逻辑,增强不仅包括增强逻辑,还包括方位信息(即方法调用前、调用后、调用前后)

​ Spring支持五种增强类型,解释如下:

在这里插入图片描述

  • 前置增强org.springframework.aop.BeforeAdvice 代表前置增强,Spring只支持方法级的增强,所以目前MethodBeforeAdvice是Spring中可用的前置增强,BeforeAdvice接口是为了将来扩展定义的。
  • 后置增强 :org.springframework.aop.AfterReturningAdvice,表示在目标方法执行后进行增强。
  • 环绕增强 :org.aopalliance.intercept.MethodInterceptor代表环绕增强,表示在目标方法执行前后实施增强。
  • 异常抛出增强 : org.springframework.aop.ThrowsAdvice代表抛出异常增强,表示在目标方法抛出异常后实施增强。
  • 引介增强 : org.springframework.aop.IntroductionInterceptor代表引介增强,表示在目标类中添加一些新的方法和属性。
3.1前置增强

​ 我们还用我们之前的GreetService和Waiter做示例代码,之前我们的代码中Waiter.serviceTo(name)方法只是输出"service to xxx",现在我们想实现Waiter.serviceTo(name)在输出之前想客户打招呼,那么按照前面的理解,我们需要为其定义一个前置增强,下面我们着手改造吧。

// 我们定义一个打招呼前置类,实现MethodBeforeAdvice
public class GreetBeforeAdvice implements MethodBeforeAdvice{
	@Override
	public void before(Method arg0, Object[] arg1, Object arg2) throws Throwable {
         // 输出招呼用语
		System.out.print("hello "+ arg1[0]);
	}
}
public class MainTest {
	public static void main(String[] args) {
		// 创建Waiter和前置增强类
		Waiter waiter = new Waiter();
		GreetBeforeAdvice greetBeforeAdvice = new GreetBeforeAdvice();
		// 创建代理工厂,随后解释这个工厂
		ProxyFactory proxyFactory = new ProxyFactory();
		// 设置被代理的目标和要织入的增强
		proxyFactory.setTarget(waiter);
		proxyFactory.addAdvice(greetBeforeAdvice);
		// 通过代理工厂创建代理类
		Waiter myWaiter = (Waiter) proxyFactory.getProxy();
		// 调用方法
		myWaiter.serviceTo("fuhang");	
	}
}
// output:
// 		hello fuhang service to :fuhang

​ 上面就是前置增强的示例,下面讲解下ProxyFactory的细节,先来回一下前面的基础知识章节的内容,说了两个动态代理技术,ProxyFactory实际就是用到了JDK或者Cglib动态代理技术。

​ Spring定义了org.springframework.aop.framework.AopProxy接口,最终提供了两个final实现类,如下图所示。

在这里插入图片描述

​ 上图中Cglib2Proxy使用的是Cglib动态代理技术,JdkDynamicAopProxy使用的是JDK动态代理技术,如果通过ProxyFactory的setInterfaces(Class [] interfaces)方法指定目标类的接口信息,那么ProxyFactory底层将使用Jdk动态代理技术,否则不设置就使用Cglib动态代理技术。由此可知我们的示例代码中用到了Cglib动态代理技术。

3.2后置增强

​ 比如每次服务过后服务员需要询问顾客"还有什么需要吗?"这句话,那么可以通过一个后置增强来实施这一个要求。示例如下。

// 声明一个后置增强类并实现AfterReturningAdvice
public class GreetAfterAdvice implements AfterReturningAdvice{

	@Override
	public void afterReturning(Object arg0, Method arg1, Object[] arg2, Object arg3) throws Throwable {
		System.out.println("还有什么需要吗?");
	}
}

// 测试类
public class MainTest {

	public static void main(String[] args) {
		// 创建Waiter及前置增强、后置增强
		Waiter waiter = new Waiter();
		GreetBeforeAdvice greetBeforeAdvice = new GreetBeforeAdvice();
		GreetAfterAdvice greetAfterAdvice = new GreetAfterAdvice();
		// 代理工厂
		ProxyFactory proxyFactory = new ProxyFactory();
		// 设置目标代理类及前置增强、后置增强
		proxyFactory.setTarget(waiter);
		proxyFactory.addAdvice(greetAfterAdvice);
		proxyFactory.addAdvice(greetBeforeAdvice);
		// 创建代理Waiter
		Waiter myWaiter = (Waiter) proxyFactory.getProxy();
		// 调用方法
		myWaiter.serviceTo("fuhang");
	}
}
// output:
//     hello fuhang service to :fuhang
//     还有什么需要吗?
3.3 环绕增强

​ 使用环绕增强实现上面前置后置增强综合体,示例代码如下。

// 声明环绕增强类并实现MethodInterceptor(aopalliance的包)
public class SurrondAdvice implements MethodInterceptor{

	@Override
	public Object invoke(MethodInvocation arg0) throws Throwable {
         // 获取目标类参数
		Object [] args = arg0.getArguments();
		System.out.println("hello :"+args[0]);
         // 执行目标类方法
		arg0.proceed();
		System.out.println(" 请问还有什么需要吗?");
		return null;
	}
}

// 下面代码不在啰嗦,直接看输出
public class MainTest {

	public static void main(String[] args) {
		
		Waiter waiter = new Waiter();
		SurrondAdvice surrondAdvice = new SurrondAdvice();
		
		ProxyFactory proxyFactory = new ProxyFactory();
		
		proxyFactory.setTarget(waiter);
		proxyFactory.addAdvice(surrondAdvice);

		
		Waiter myWaiter = (Waiter) proxyFactory.getProxy();
		
		myWaiter.serviceTo("fuhang");
		
	}
}
// output:
//  	hello :fuhang
//   	service to :fuhang
//		请问还有什么需要吗?
3.4 异常抛出增强

​ 异常抛出增强适合的应用场景是事务管理,当参与事务管理的某个Dao抛出异常时候回滚事务。示例如下。

public class Waiter{

	@Override
	public void serviceTo(String name) {
		throw new RuntimeException("Dao异常");
	}
}

// 异常抛出增强类,在异常抛出之前会调用
public class ExceptionAdvice implements ThrowsAdvice{
	
	public void afterThrowing(Method method,Object [] args,Object target,Exception exception){
		System.out.println("事务回滚");
	}
}

// 测试类
public class MainTest {

	public static void main(String[] args) {
		
		Waiter waiter = new Waiter();
		ExceptionAdvice exceptionAdvice = new ExceptionAdvice();
		
		ProxyFactory proxyFactory = new ProxyFactory();
		
		proxyFactory.setTarget(waiter);
		proxyFactory.addAdvice(exceptionAdvice);

		
		Waiter myWaiter = (Waiter) proxyFactory.getProxy();
		
		myWaiter.serviceTo("fuhang");
		
	}
}
// output:
// 		事务回滚
//		Exception in thread "main" java.lang.RuntimeException: Dao异常

​ ThrowingAdvice是一个标签接口,其中没有声明任何方法,在运行期Spring通过反射机制自动判断增强方法,增强方法签名必须是以下格式:

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

方法前三个参数可选,要么全部传入,要么全部不传。最后一个必须传入。最后一个参数是Throwable的子类就行。

3.5 引介增强

​ 引介增强比较特殊,他不是在方法周围增加增强逻辑,而是为目标类动态添加新的方法和属性。所以引介增强的连接点定位是类而不是方法上。下面我们通过引介增强为目标类添加一个接口实现,完成监控开关功能。

// 声明一个附加接口
public interface Monitor {
	void setMonitorActive(boolean active);
}

// 目标类
public class Waiter{
	public void serviceTo(String name) {
		System.out.println("hello :"+name);
	}
}

// 创建一个类继承DelegatingIntroductionInterceptor并实现将来要附加到目标类的接口
public class PerformanceMonitor extends DelegatingIntroductionInterceptor implements Monitor{
	// 相当于最终给类添加的属性
	ThreadLocal<Boolean> isOpen = new ThreadLocal<>();

	@Override
	public void setMonitorActive(boolean active) {
		isOpen.set(active);
	}

	@Override
	public Object invoke(MethodInvocation arg0) throws Throwable {
		if(isOpen.get()!=null && isOpen.get() ){
			Long beginTime = System.currentTimeMillis();
			super.invoke(arg0);
			System.out.println("consume time :"+(System.currentTimeMillis()-beginTime));
		}else{
			super.invoke(arg0);
		}
		return null;
	}

}

// 测试类
public class MainTest {

	public static void main(String[] args) {
		
		Waiter waiter = new Waiter();
		PerformanceMonitor performanceMonitor = new PerformanceMonitor();
		
		ProxyFactory proxyFactory = new ProxyFactory();
		// 下面必须设置setOptimize(true),开启Cglib代理
		proxyFactory.setInterfaces(Monitor.class);
		proxyFactory.setTarget(waiter);
		proxyFactory.addAdvice(performanceMonitor);
		proxyFactory.setOptimize(true);

		// 第一次调用方法
		Waiter myWaiter = (Waiter) proxyFactory.getProxy();
		myWaiter.serviceTo("fuhang");
		
        // 转为Monitor后设置监控开关后再次调用方法
		Monitor monitor = (Monitor)myWaiter;
		monitor.setMonitorActive(true);
		
		myWaiter.serviceTo("zhangsan");
		
	}
}
// output:
// 		hello :fuhang
//		hello :zhangsan
//		consume time :0

总结

​ 以上就是本次学习的主要内容,后面如何将上面手动创建的方法转换为Spring xml配置在本篇不做记录(因为不难,也没啥技术含量)。希望通过本次记录能学到Spring Aop核心知识。

参考资料
《精通Spring4.x企业应用开发实战》

  • 5
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值