4.Spring揭秘-AOP概念

4.Spring揭秘-AOP概念

萤火虫 767 2021-10-22

AOP框架概念

aop的是什么?

在oop编程,也就是对象时代,我们可以根据模块或业务进行拆分抽象,进而可以很好的实现业务。但如果说需要在系统内增加一层通用逻辑,比如所有业务接口都需要在返回时打印出所耗费的时间,此时,打印时间逻辑就会散落在各处。如果一个两个还好那如果是千万个呢?此时我们有了aop。

aop是什么?

全称AspectOrientedProgramming,中文翻译为面向方面编程。使用aop我没可以类似于日志统计权限管理等系统需求进行模块化的组织,简化系统需求与实现之间的对比关系,进而使得系统的实现更具模块化

任何系统都可能会有横切的业务与需求,这些需求就是系统中的横切关注点。使用传统方法,我们无法更好的以模块的形式对这些关注点进行组织和实现,因为你的代码会散落在各处,每次修改都需要全部修改。aop引入了aspect(切面/方面)的概念,用来以模块化的形式对系统中的横切关注点进行封装。在面向对象中class(类)就是根本,在aop中aspect就是根本。但是aop仅仅是对oop中的缺点进行补足方式的一种,当我们把class形式模块化的业务需求和以aspect形式模块化的系统需求拼装在一起,就组成了一个完成系统。

image-20201123221730314

aop的不足

aop无法向oop那样可以独立的完成一个系统,oop不借助aop可以完成系统,只不过是对于横切类的需求处理的不够优雅而已。所以说aop是建立在oop之上的,用来弥补oop的缺点。

aop的实现

Aop是一种理念,要实现理念通常需要一种实现方式。与oop需要相应的语言支持一样(例如:java/C++/Go),aop也需要某种语言以帮助实现相应的概念实体,我们称这些实现aop的语言为aol,即aspect-oriented language。

aol与系统实现语言相同,因为你要构筑在oop之上,所以oop的实现语言是什么你就要跟着是什么,例如开发用java,那么aol就是java,当然你可以用别的语言来实现。对应java的就是AspectJ,除了java语言还有很多语言

例如:AspectC、AspectC++、Aspect.Net、AspectPHP等等,只要你想就会有各种个样的aol

受限于aop技术实现上的困境,aol实现的aop中的概念实体,最终都需要某种方式集成到系统中去,毕竟oop是不会主动集成aop的组件的。而将aop组件集成到oop组件的过程,在aop中称为织入(Weave)过程。

将aop的Aspect织入到oop系统的实现方式可谓千差万别。但不管如何实现,织入过程是处于aop和oop的开发过程之外的,也就是说不需要手动代码的方式将oop与aop结合起来,并且对于整个系统的实现是透明的,开发者只需要关心相应的业务需求实现,或切面需求实现即可。在所有业务和切面以模块化开发完后,通过织入就可以将系统完成了。

静态AOP

第一代aop,静态aop,以AspectJ为代码,特点是相应横切关注点以Aspect形式实现后,会通过特定的编译器,将实现后的Aspect编译并织入到系统的静态类中。比如AspectJ会使用ajc编译器将各个Aspect以Java字节码的形式编译到系统的各模块中去,达到Aspect和Class融合的目的。

优点:以字节码形式编译到java的class中,jvm可以像加载普通的class一样,不会对系统运行造成任何性能损失。

缺点:不够灵活,如果横切关注点需要改变织入位置,就需要修改Aspect定义文件,并重新编译。

动态AOP

第二代aop,动态aop,通过java提供的各种动态特性来实现Aspect织入到当前系统的过程SpringAop就属于此类。使用java实现aop后Aspect就是普通class,易于开发和维护。

缺点:动态大都在类加载或者系统运行期间,采用对系统字节码进行操作的方式来完成织入,难免会有性能损失。

JAVA平台AOP实现机制

动态代理

JDK1.3引入,动态代理机制,在运行期间为相应的接口动态生成对应的代理对象。所以我们可以将横切点关注点逻辑封装到动态代理的InvocationHandler中,然后在系统运行期间,根据横切关注点需要植入的模块位置,将横切逻辑织入到相应的代理类中。

优点与缺点就是织入关注点的逻辑模块类需要实现相应的接口,并且运行期间使用反射,性能有损豪。SpringAop默认使用此种方式。

动态字节码增强

jvm加载的class文件是符合一定规范的,所以只要交给jvm运行的文件符合规范就可以。可以ASM或CDLIB等java工具库,在程序运行期间动态构建字节码的class文件。原理就是为代理的类生成一个子类,将横切逻辑加到子类中,让程序使用动态生产的子类,从而达到织入的目的。

缺点:受限于需要给类实现子类,如果类声明final的话则无法对其进行子类化的扩展。Spring在无法使用动态代理时才会使用字节码增强。

Java代码生成

在需要织入的类自动生成需要织入的代码

自定义类加载器

所有的java程序的class都要通过相应的类加载器,进行加载到jvm中才可以运行,如果在此时进行对加载进来的class先进行修改在给jvm就可以完成织入过程。

AOP涉及概念

Joinpoint

在系统运行之前aop的功能模块需要织入到oop的功能模块中,所以,要进行这种织入过程我们需要知道系统在哪些执行点上进行织入操作,这些将要在其上进行织入操作的系统执行点就是joinpoint。说白了就是,哪里可以加回调,或者可以说是在哪里加入扩展代码。而这个哪里是有说法的,并不是说你想加载哪里就在哪里,这需要当前的aop支持才可以。而这个哪里就是joinpoint,就是切点。例如,我想在方法调用前打印方法参数,想再方法异常后调用异常统计,在初始化类前,在初始化类后等等,这些位置就是joinpoint。

较为常见的Joinpoint:

方法调用、方法执行、构造方法调用、构造方法执行、字段设置、字段获取、异常处理执行、类初始化。

基本上,程序执行过程中你觉得可以的地方都可以作为joinpoint,不过具体的aop实现产品支持不支持就是另一回事了。

Pointcut

pointcut概念代表的是joinpoint的表述方法。例如你需要在A类的a方法处执行,joinpoint就是方法,但是你知道并不代码计算机知道,因此需要一种方式去告知aop框架我要在这里去进行织入,把我写的日志逻辑在这里调用。

Pointcut表达式
  • 直接制定joinpoint所在的方法名称
  • 正则表达式
  • 使用特定的pointcut表述语言。例如Spring提供的@Pointcut
Pointcut运算
即pointcut与pointcut进行逻辑运算,比如&、|、 !

Advice

在aop知道在那个方法织入了以后,我们只告诉了aop框架在这里执行并没有告诉执行的时机,是在joinpoint前后什么时候执行。而advice就是告诉aop框架你在什么执行并执行什么功能。
  • before advice
  • after davice
  • after returning advic
  • after throwing advice
  • after advice
  • around advice
  • introduction

Aspect

就相当于oop中的class,用于把pointcut和advice进行组合的类。

织入和织入器

织入只是一个过程,织入器是一个工具。比如你打补丁缝补丁是一个过程,针和线就是工具。Aspectj使用ajc来完成织入操作,Spring则使用一组类来完成织入操作,ProxyFactory类则是代表。

目标对象

通过pointcut的描述找到符合条件的对象,则这个对象就是目标对象。例如打补丁过程中,破洞的衣服就是目标对象。

image-20201123233812270

SpringAop实现机制

动态代理

public interface ServiceA {
	void speck1();
	void speck2();
}
public class ServiceAImpl implements ServiceA {
	
	@Override
	public void speck1() {
		System.out.println("Service A 1111");
	}

	@Override
	public void speck2() {
		System.out.println("Service A 22222");
	}
}
public interface ServiceB {
	void noSpeck();
}
public class ServiceBImpl implements ServiceB{
	@Override
	public void noSpeck() {
		System.out.println("Service B");
	}
}
public class ServiceABImpl implements ServiceA,ServiceB {
	@Override
	public void noSpeck() {
		System.out.println("Service AB no speck");
	}

	@Override
	public void speck1() {
		System.out.println("Service AB speck 1111");
	}

	@Override
	public void speck2() {
		System.out.println("Service AB speck 22222");
	}
}
public class AroundInvocationHandler implements InvocationHandler {

	private final Object object;

	public AroundInvocationHandler(Object object) {
		this.object = object;
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		System.out.println("开始计时。。");
		Object invoke = method.invoke(object, args);
		System.out.println("计时结束!!");
		return invoke;
	}
}
import java.lang.reflect.Proxy;

public class ProxyMain {

	public static void main(String[] args) {
		ServiceA proxyA = (ServiceA) Proxy.newProxyInstance(ProxyMain.class.getClassLoader(),
				new Class[]{ServiceA.class},
				new AroundInvocationHandler(new ServiceAImpl()));

		ServiceB proxyB= (ServiceB) Proxy.newProxyInstance(ProxyMain.class.getClassLoader(),
				new Class[]{ServiceB.class},
				new AroundInvocationHandler(new ServiceBImpl()));

		ServiceB proxyAB= (ServiceB) Proxy.newProxyInstance(ProxyMain.class.getClassLoader(),
				new Class[]{ServiceA.class,ServiceB.class},
				new AroundInvocationHandler(new ServiceABImpl()));

		proxyA.speck1();
		proxyA.speck2();
		proxyB.noSpeck();
		proxyAB.noSpeck();
		//此处说明可以对代理类强转到
		ServiceA serviceA = (ServiceA) proxyAB;
		serviceA.speck1();
		serviceA.speck2();

	}
}
//------
开始计时。。
Service A 1111
计时结束!!
开始计时。。
Service A 22222
计时结束!!
开始计时。。
Service B
计时结束!!
开始计时。。
Service AB no speck
计时结束!!
开始计时。。
Service AB speck 1111
计时结束!!
开始计时。。
Service AB speck 22222
计时结束!!

生成ServiceA和ServiceB的代理,并且代理不通方法使用相同的逻辑。

可以看到ServiceA中两个方法都被代理了,并且ServiceABImpl可以强转到对应接口调用方法。

动态字节码生成-CGLIB

compile group: 'cglib', name: 'cglib', version: '3.3.0'
public class ServiceA {
	public String getInfo(String msg) {
		return "Service A->" + msg;
	}
}
public class ServiceAMethodInterceptor implements MethodInterceptor {
	@Override
	public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
		System.out.println("这里是对目标类进行增强!!!");
		//注意这里的方法调用,不是用反射
		Object object = proxy.invokeSuper(obj, args);
		System.out.println("这里是对返回结果进行处理!!!");
		return object;
	}
}
public class CglibMain {
	public static void main(String[] args) {
		//在指定目录下生成动态代理类,我们可以反编译看一下里面到底是一些什么东西
		//最好设置绝对路径
		System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "spring-mm-demo/src/main/java/com/mm/demo/aop/cglib");

		//创建Enhancer对象,类似于JDK动态代理的Proxy类,下一步就是设置几个参数
		Enhancer enhancer = new Enhancer();
		//设置目标类的字节码文件
		enhancer.setSuperclass(ServiceA.class);
		//设置回调函数
		enhancer.setCallback(new ServiceAMethodInterceptor());

		//这里的creat方法就是正式创建代理类
		ServiceA proxy = (ServiceA) enhancer.create();
		//调用代理类的getInfo方法
		String main = proxy.getInfo("Main");
		System.out.println(main);
	}
}
//----
这里是对目标类进行增强!!!
这里是对返回结果进行处理!!!
Service A->Main