Spring AOP(动态代理\动态字节码)精华一页纸

时间:2021-11-24 17:09:55

1、AOP


AOP作为一种设计理念, 拦截方法执行前后, 提供一些辅助功能。实际上, 在AOP火爆起来之前, 已经存在了很多AOP实现的理念

比如一些设计模式就体现了AOP的思想
Decorator(装饰者)
Observer(观察者)
Chain of Responsibility(责任链)
...

一些现有的使用场景, 比如 Servlet 拦截器;比如 Java 集合中 Collections 提供了 并发 | 只读 等等拦截的功能。
除了常说的 鉴权、日志等用途, 实际使用过程中, 事务和缓存的应用意义非常大, 在框架设计上, 把缓存和事务的功能从应用功能中独立出来。从而实现 职责单一的思想。还能对调用堆栈、性能分析。比如, 在不破坏现有代码的情况下,统计方法的执行时间。

2、动态代理


针对每一种类型,都可以采用 Collections的做法, 实现多个静态代理的类. 这么做的优点是性能较好, 而且代码结构相对清晰;缺点是代码层次多, 冗余代码多。java提供了另一种机制 - 动态代理。

Spring AOP(动态代理\动态字节码)精华一页纸

接口
public interface AOP {
public String aop();
}

目标类
public class AOPObject implements AOP{
@Override
public String aop() {
return this.getClass().getName();
}
}

代理类
public class AOPProxy implements InvocationHandler{
private AOP target;

public AOPProxy(AOP target){
this.target = target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("aop---");
return method.invoke(target, args);
}
}
使用

AOP aop = (AOP)Proxy.newProxyInstance(AOP.class.getClassLoader(), new Class[]{AOP.class}, new AOPProxy(new AOPObject()));
aop.aop();

核心在于Proxy类通过反射把所有的方法调用都转换成 InvocationHandler 的invoke调用。
使用动态代理的注意点:
只能拦截有接口的对象
和接口要在同一个类加载器下

如果没有的对象, 如何拦截?

3、动态字节码


因为Class的设计特点, 可以动态加载Class;所以产生了很多, 动态生成字节码-动态加载的技术,实现拦截。

I、Instrumentation 指令改写
Instrumentation(指令改写), Java提供的这个功能开口, 可以向java类文件添加字节码信息。可以用来辅助日志信息,记录方法执行的次数,或者建立代码覆盖测试的工具。通过该方式,不会对现有应用功能产生更改。
a、静态方式 PreMain
manifest文件 指定 Premain-Class 实现类
实现 ClassFileTransformer 接口的transform 方法

b、动态方式 AgentMain
manifest文件 指定Agent-Class 实现类
Attach Tools API 把代码绑定到具体JVM上 (需要指定绑定的进程号)

-- 这个功能正常的用途是JVM管理的一个功能,可以监控系统运行的;原理上也可以用在系统增强上面,不过premain和agentmain是针对整个虚拟机的钩子(hook),每个类装载进来都会执行,所以很少使用在功能增强上

II、asm 
原理是 读入 现有Class 的字节码, 生成结构树, 通过二次开发接口, 提供增强功能, 然后再把字节码写入
a、继承 ClassAdapter 获取Class内域和方法的访问器 ClassVisitor
b、继承 MethodAdapter 实现方法的覆盖
c、通过 ClassReader 和 ClassWriter 修改原先的实现类源码

III、CGLib (Code Generation Library)
因为 asm可读性较差, 产生了更易用的框架 cglib,其底层使用的是 asm 接口;这个技术被很多AOP框架使用,例如
Spring AOP 和dynaop ,为她们提供方法的 interception(拦截)
hibernate使用 cglib 来代理单端 single-ended(多对一和一对一)关联
EasyMoc和jMock通过使用moke 对象来测试java代码包

a、Enhancer 类似于 动态代理的 Proxy 提供代理的对象
b、MethodInterceptor 类似于 动态代理的 InvocationHandler 提供拦截逻辑

对应动态管理的例子, 就是类似如下代码
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(AOPObject.class);
enhancer.setCallback(new AOPProxy());
AOPObject aop = (AOPObject)enhancer.create();
aop.aop();

真实的接口和架构要比者复杂很多,最基本调用就是如此

IV、javasist
这是另外一种字节码生成技术, 通过java 文本方式, 来实现字节码的替换;不像 asm 是字节层面的替换, javasist 封装了底层细节, 直接把java 代码文本翻译替换。

-- 一个经典的打印执行时间的例子
CtClass clas = ClassPool.getDefault().get("StringBuilder");

CtMethod method = cls.getDeclaredMethod(methodName);
String replaceMethodName = methodName + "$impl";
method.setName(replaceMethodName);
CtMethod oldMethod = CtNewMethod.copy(method, methodName, cls, null);

String type = method.getReturnType().getName();
StringBuffer body = new StringBuffer();
body.append("{\nlong start = System.currentTimeMillis();\n");
if (!"void".equals(type)) {
body.append(type + " result = ");
}
body.append(replaceMethodName + "($$);\n"); // 第一个$符号是关键字,第二个是参数

body.append("System.out.println(\"Call to method " + methodName
+ " took \" +\n (System.currentTimeMillis()-start) + "
+ "\" ms.\");\n");
if (!"void".equals(type)) {
body.append("return result;\n");
}
body.append("}");

oldMethod.setBody(body.toString());
cls.addMethod(oldMethod);

System.out.println("Interceptor method body:");
System.out.println(body.toString());

V、Bcel/Serl
还有很多动态字节码技术, 不做介绍, 有需要的可以去了解一下

4、Spring AOP


有了动态代理 + 动态字节码增强 技术, AOP 拦截的实现技术已经完备。
是时候介绍一下 AOP的一些概念了。

I、aopalliance -- AOP联盟的API接口规范
通知(Advice): 何时(Before,After,Around,After还有几个变种) 做什么
连接点(JoinPoint): 应用对象提供可以切入的所有功能(一般是方法,有时也是参数)
切点(PointCut): 通过指定,比如指定名称,正则表达式过滤, 指定某个/些连接点, 切点描绘了 在何地 做
切面(Aspect): 通知 + 切点 何时何地做什么
引入(Introduction):向现有类添加新的属性或方法
织入(Weaving): 就是将切面应用到目标对象的过程

-- aopalliance 包含了一些常用的接口, 实现 AOP 功能的框架, 一般都会实现这些接口

II、Spring AOP

需要有依赖的, 继承自 Spring AOP 体系的用法

扩展以下接口
org.aopalliance.intercept.MethodInterceptor -- 标准AOP接口,可以对方法做任意增强
org.springframework.aop.MethodBeforeAdvice -- 继承自标准接口Advice,对方法做前增强
org.springframework.aop.AfterReturningAdvice -- 继承自标准接口Advice,对方法做后增强
org.springframework.aop.ThrowsAdvice -- 继承自标准接口Advice,对方法做异常增强,该接口是一个空方法接口,约定方法是 afterThrowing ,参数是任意定义的异常。

可以自己编写代码, 编码式来实现 AOP 的调用过程。

a、手工代理 - FactoryBean 适配 代理 方式
org.springframework.aop.framework.ProxyFactoryBean
<bean id="businessObject" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target">
<ref local="xxx"/>
</property>
<property name="proxyInterfaces">
<value>xxx</value>
</property>
<property name="interceptorNames">
<list>
<value>beforeAdive</value>
<value>afterAdive</value>
</list>
</property>
</bean>

b、自动代理
<bean id="autoProxy" class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
<bean id="pointcutAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="advice">
<ref local="pointcutBeforeAdive"/>
</property>
<property name="patterns">
<list>
<value>.*add.*</value> -- 只匹配名称中含有add方法的
</list>
</property>
</bean>
Spring自动去搜索拦截

利用Aspect , 不需要继承 Spring AOP 体系, 普通类POJO 能实现的拦截, 非注解方式下, 实现完全解耦的用法(AspectJ + Spring AOP)

c、基于Aspect 注解
配置 <aop:aspectj-autoproxy />,目的是启用Spring的 AnnotationAwareAspectJAutoProxyCreator 类,由该类来扫描注解,把代理的通知和切入点等等载入,(代替上下文加载载入),实现代理的过程
每个通知的 实现类,通过 Aspecj 的标注,实现 切入点定义 和 方法
@Aspect
public class POJOAdvisor {
// 定义切入点,此处 doActionPoint 即切点的名字,是一个空方法 主要目的给Pointcut注解使用,不然Poingcut注解无法使用
@Pointcut("execution(* com.aop.annotation.AopInterface.doAction(..)) ")
public void doActionPoint(){

}

@Before("doActionPoint()")
public void beforePointCut(){
System.out.println(getClass().getName() + ":beforePointCut() by annotation");
}

@After("doActionPoint()")
public void afterPointCut(){
System.out.println(getClass().getName() + ":afterPointCut() by annotation");
}

d、基于Aspect 配置 - 此方式为最常用方式
<bean id="pointcutBeforeAdive" class="com.aop.POJOAdvisor"/>
<aop:config>
<aop:aspect ref="pointcutBeforeAdive">
<!-- within 可以指定包下的类,bean 可以指定具体类 -->
<aop:before pointcut="execution(* com.aop.AOP.aop(..))" method="beforePointCut"/>
</aop:aspect>
</aop:config >

III、ApsectJ 增强功能
Aspect 不仅能拦截方法, 还能扩充方法和功能

<!-- 依赖 asectj.jar aspectjwear.jar -->
<aop:config>
<aop:aspect ref="pojoadvisor">
<!-- within 可以指定包下的类,bean 可以指定具体类 -->
<aop:before pointcut="execution(* com.aop.AopInterface.doAction(..)) and within(com.aop.*)" method="beforePointCut"/>
<!-- after-returning/after-throwing -->
<aop:after pointcut="execution(* com.aop.AopInterface.doAction(..)) and bean(targetaop)" method="afterPointCut"/>
</aop:aspect>
<!-- 如果pointcut 是一样的,可以抽取出来单独定义 -->

<aop:aspect ref="pojoadvisor">
<aop:pointcut id="doActionPoint" expression="execution(* com.aop.AopInterface.doAction(..))"/>
<aop:before pointcut-ref="doActionPoint" method="beforePointCut"/>
<aop:after pointcut-ref="doActionPoint" method="afterPointCut"/>
</aop:aspect>
<!-- 环绕相对于普通 JAVA对象而言,多了一个 ProceedingJoinPoint 的传入参数,依赖了AspectJ 功能 -->

<aop:aspect ref="aroundAdvisor">
<aop:around pointcut-ref="doActionPoint" method="around"/>
</aop:aspect>

<!-- 给通知 传递参数, 主要用在 可以检查传入的参数,被拦截的方法调用中,传入的参数是否合法 -->
<aop:aspect ref="intercept">
<aop:pointcut id="action" expression="execution(* com.aop.AopParaInterface.doAction(..)) and args(msg)"/>
<aop:before pointcut-ref="action" method="intercept" arg-names="msg"/>
</aop:aspect>

<!-- 利用拦截功能,为对象增加新的功能,其实就是动态代理,在invoke时,发现是注册的新方法,改调用新方法 -->
<aop:aspect>
<aop:declare-parents
types-matching="旧的对象+"
implement-interface="新增接口"
default-impl="委托对象"
/>

</aop:aspect>

</aop:config>

Aspect 拦截 语法
execution(* 方法路径(..))
within( 指定方法)
args 传入参数

AOP的概念和知识点很多, 只要能理解其用途, 无非就是两个部分
需要拦截哪些模块 -- 定位 (从包-类-方法, Spring最小粒度就到方法)
增加哪些功能 -- 增加 (普通POJO类、或者继承扩展Spring AOP拦截器)