简介
Java代理与AOP都有那么多文章了,为什么我还要写呢?
- 因为我是写给自己看的,不是写给你们看的,不过对读者来说应该是又有作者拿AOP鞭尸了,烂熟于心的建议继续出去逛街吧,发博只是顺手而为罢了
言归正转,Java中的代理分为静态代理与动态代理两种:
- 动态代理:程序运行时动态调用和扩展目标对象方法,代理类在运行时确定,如生成的代理类带有随机值的后缀。
- 静态代理:程序编译时生成相应的扩展字节码。
目前最常见的Java动态代理框架为Spring AOP,其又根据代理目标分为JDK动态代理与Cglib动态代理两种方式,这两种方式都是通过生成代理类进行增强。
常见的静态代理框架为ApsectJ,是通过在编译时添加扩展功能的字节码从而进行增强。AspectJ 扩展了 Java 语言,提供了一个专门的编译器在编译时提供横向代码的植入。从Spring 2.0开始,Spring AOP引入了对AspectJ的支持,使AspectJ的编写更方便了。
动态代理
Java中的动态代理常见的两种实现方式如下:
- JDK动态代理:根据目标类实现的接口生成实现了这些接口的代理类
- Cglib动态代理:生成目标类的子类从而实现代理
Spring AOP框架集成了以上两种代理方式,若代理目标实现了接口,则使用JDK动态代理的方式实现代理。由于JDK动态代理性能比Cglib性能要高不少,所以Spring AOP默认会优先使用JDK代理。
由于JDK Proxy无法代理没有实现接口的类,所以如果代理目标没有实现接口,Spring AOP会使用cglib生成一个代理目标类的子类来实现代理。
JDK动态代理
JDK动态代理通过对实现了接口的目标类实例生成一个代理类java.lang.reflect.Proxy
动态子类com.sun.proxy.$ProxyX
实例,该代理子类实例与目标类实例具有相同的方法、接口,通过调用实现了InvocationHandler
接口的类进行方法的增强。
JDK动态代理的实现流程
JDK动态代理的实现流程如下:
- 确认代理目标类实例,目标类必须实现了接口
JDK动态生成的代理类同样实现了目标类的接口,但并没有继承目标类,其实现一般包含了目标类的行为,且可根据需要自定义额外操作。若目标类没有实现接口,类转换时将抛出ClassCastException
异常。 - 创建调用处理器
InvocationHandler
实例,该实例通过invoke()
方法代理调用目标类操作 - 通过
Proxy.newProxyInstance()
方法生成动态代理类$ProxyX
实例,该实例包含了代理目标类的方法名,区别在于这些方法的执行都是交给InvocationHandler
- 将
$ProxyX
实例转为相应的目标类接口,执行所需要的方法
代理过程中涉及到的类的关系图如下所示:
JDK动态代理例子
- 目标类实现的接口
public interface Animal {
String run();
}
- 目标类
public class Fish implements Animal{
@Override
public String run() {
return"用鳍游动";
}
}
- 代理目标的代理类调用处理器
public class AnimalInvocationHandler implements InvocationHandler {
private Animal animal;
public AnimalInvocationHandler(Animal animal) {
this.animal = animal;
}
/**
* 代理对象方法调用
*
* @param proxy 根据代理目标类创建的代理Proxy实例ProxyX
* @param method 与在代理实例上调用的接口方法相对应的Method实例。
* @param args
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = method.invoke(animal, args);
System.out.println("Animal代理增加:" + animal.getClass().getName() + result + ", 该代理由Proxy动态子类" + proxy.getClass().getName() + "负责");
return result;
}
}
- 测试程序
public class ProxyTest {
public static void main(String[] args) {
// 保存代理生成的文件
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
Fish fish = new Fish();
InvocationHandler animalHandler = new AnimalInvocationHandler(fish);
Animal proxyFish = (Animal) Proxy.newProxyInstance(fish.getClass().getClassLoader(), Fish.class.getInterfaces(), animalHandler);
System.out.println("Proxy0 is a son class of Animal:" + (proxyFish instanceof Animal));
System.out.println("Proxy0 is a son class of Proxy:" + (proxyFish instanceof Proxy));
System.out.println("Proxy0 is a son class of Fish:" + (proxyFish instanceof Fish));
}
}
测试输出结果如下:
Proxy0 is a son class of Animal:true
Proxy0 is a son class of Proxy:true
Proxy0 is a son class of Fish:false
Animal代理增加:io.wilson.basic.proxy.Fish用鳍游动, 该代理由Proxy动态子类com.sun.proxy.$Proxy0负责
- 生成的动态代理类如下:
package com.sun.proxy;
.....
public final class $Proxy0 extends Proxy implements Animal {
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final String run() throws {
try {
return (String)super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m3 = Class.forName("io.wilson.basic.proxy.jdk.Animal").getMethod("run");
m2 = Class.forName("java.lang.Object").getMethod("toString");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
cglib动态代理
cglib动态代理是通过生成一个继承了代理目标类的子类从而达到目标对象的增强,实现流程与JDK动态代理十分相似,方法的增强实现接口为MethodIntercetpor
(相当于JDK中的InvocationHandler
),而代理类由Enhancer
创建。其实现流程如下:
- 创建
Enhancer
实例,设置代理目标类,设置方法回调(方法拦截器)实例 - 通过
Enhancer
的create()
方法创建代理类实例,代理类实例转为目标类实例 - 调用实例方法
例子
代理目标类Cat
public class Cat {
public String jump(String name) {
return "貓貓" + name + "跳得很高";
}
}
目标类方法拦截器CatInterceptor
public class CatInterceptor implements MethodInterceptor {
/**
* 所有生成的代理方法都调用此方法,而不是原始方法。
* 原始方法既可以通过使用Method对象的常规反射来调用,也可以通过使用MethodProxy(更快)来调用。
*
* @param obj 被增强的对象
* @param method 拦截的方法
* @param args 方法参数
* @param proxy 用于方法调用的代理
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// 调用代理实例的父类方法,即代理目标类Cat的方法
Object result = proxy.invokeSuper(obj, args);
// 返回增强处理
return result + "! Wow, 居然跳了" + RandomUtils.nextInt(1, 6) + "米高!";
}
}
测试CglibTest
import net.sf.cglib.proxy.Enhancer;
import org.springframework.cglib.core.DebuggingClassWriter;
public class CglibTest {
public static void main(String[] args) throws InterruptedException {
// 设置cglib的调试路径,用于输出代理生成的文件
String targetPath = CglibTest.class.getResource("/").getPath();
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, targetPath);
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Cat.class);
// 设置代理目标类的拦截器
enhancer.setCallback(new CatInterceptor());
Cat proxyCat = (Cat) enhancer.create();
System.out.println(enhancer.create().getClass());
System.out.println(proxyCat.jump("Ketty"));
}
}
控制台输出如下:
class io.wilson.basic.proxy.cglib.Cat$$EnhancerByCGLIB$$38389e96
貓貓Ketty跳得很高! Wow, 居然跳了1米高!
代理类Cat$$EnhancerByCGLIB$$xxx
package io.wilson.basic.proxy.cglib;
......
public class Cat$$EnhancerByCGLIB$$38389e96 extends Cat implements Factory {
......
public final String jump(String var1) {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
return var10000 != null ? (String)var10000.intercept(this, CGLIB$jump$0$Method, new Object[]{var1}, CGLIB$jump$0$Proxy) : super.jump(var1);
}
......
}
静态代理AOP框架:AspectJ
由于Spring AOP支持了AspectJ,所以就以个人平时写得最多的Spring AOP代理方式来写个AspectJ代理例子。此前先对相关的AOP名词作个简单的介绍:
- 切面(Aspect):一般为独立于业务的特定通用功能,每个切面都专注于特定的领域功能,如日志打印、鉴权等,在Spring AOP中以
@Aspect
注解声明切面 - 切点(pointcut):一般表示为正则表达式,用于匹配连接点,即筛选哪些连接点是生存在切面上的。在Spring AOP中以
@Pointcut
注解到方法上来声明切点 - 连接点(joinpoint):程序执行过程中需编织Advice的特定点,如方法执行、构造函数调用、字段分配等,比如在方法
A()
上添加了切点值中包含的注解@LogAspect
,则A()
即为@LogAspect
所属切面中的连接点。Spring AOP动态代理只支持方法执行 - 编织(Weaving):将切面与目标对象链接以创建Advice对象的过程,如将日志打印操作添加到方法执行前/后
- 建议(Advice):切面在特定链接点中采取的操作,比如日志切面在方法执行前的参数打印,返回时的结果打印,Spring AOP中常见的Advice注解有
Before
、@Around
、@After
、@AfterReturning
、@AfterThrowing
一般情况下个人在开发中的Spring AOP实现步骤如下:
- 定义切点所需的注解如
@LogAspect
- 定义切面
@Aspect
、切点@Pointcut
、编织操作@Before
、@After
、@Around
等 - 功能测试
之后便是个人的例子搭建了
添加AspectJ maven依赖与相应插件
由于是在Spring Boot环境下的例子,所以可以不用设置AspectJ版本,spring-boot-dependencies.pom中已统一了版本。
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
</dependency>
.....
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.11</version>
<configuration>
<complianceLevel>1.8</complianceLevel>
<source>1.8</source>
<target>1.8</target>
<showWeaveInfo>true</showWeaveInfo>
<verbose>true</verbose>
<Xlint>ignore</Xlint>
<encoding>UTF-8 </encoding>
</configuration>
<executions>
<execution>
<goals>
<!-- 对main目录下的类编译时进行编织增强 -->
<goal>compile</goal>
<!-- 对test目录下的类编译时进行编织增强 -->
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
定义连接点注解LogPoint
@Target({ElementType.METHOD,ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.RUNTIME)
public @interface LogPoint {
}
切面LogAspect
public class LogAspect {
@Pointcut("@annotation(io.wilson.basic.proxy.aspectJ.LogPoint) && execution(* io.wilson.basic.proxy.aspectJ..*(..))")
public void pointcut() {
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
String animal = joinPoint.getSourceLocation().getWithinType().getSimpleName();
System.out.println("What does the " + animal + " want to do ?");
Object result = joinPoint.proceed();
System.out.println("It's barking at you");
return result;
}
}
编织目标类Dog
public class Dog {
@LogAspect
public void cry(){
System.out.println("狗狗汪汪叫");
}
}
看到这里可能有人疑惑了,这是常见的Spring AOP日志编写吧,只是多了个Mavan插件,编织器上少了@Component
注解,Service/Controller日志写成了Dog
日志且少了单例注解而已。唔,的确仅此而已。
测试一波
public class DogTest {
public static void main(String[] args) {
Dog dog = new Dog();
dog.cry();
}
}
测试就这么简单?是的,结果输出如下:
What does the Dog want to do ?
狗狗汪汪叫
It's barking at you
该静态代理的实现原理其实很简单,前文说过AspectJ会有一个编织器对目标类进行字节码编织,该例子中编织器以maven插件的形式引入了,项目编译时会对目标类进行字节码织入,而目标类Dog
编译编织后的结果如下:
AspectJ静态代理与Spring AOP动态代理的切换
- 如果保留AspectJ的maven编织器插件,切面
LogAspect
补充@Component
注解,Dog
添加@Component
改为单例(切点注解一般用在单例的service或controller方法上,如controller请求与响应日志), Spring会怎么进行代理?
依旧会以AspectJ静态代理,不会生成任何的动态类,Dog
字节码中依旧保留了编织器的织入,在IDEA中编译后的测试例子如下(输出的单例Dog
依旧是原生类):
-
去掉AspectJ的maven编织器插件后,Spring会用回动态代理,以下为去掉后的输出图(由于
Dog
没有实现接口,所以Spring AOP用的是cglib动态代理):
由上可知,在Spring环境中AspectJ与Spring AOP动态代理的切换其实十分简单,只需添加一个AspectJ编织器maven插件即可,切面类(如
LogAspect
)与连接点所在类(如Dog
)的单例注解虽然对AspectJ来说是冗余的,但加了也不会有负面作用。
AOP框架对比:Spring AOP与AspectJ
特点 | Spring AOP | AspectJ |
---|---|---|
实现方式 | 纯Java实现 | 使用Java编程语言的扩展实现 |
编译 | 无需单独的编译过程 | 需设置LTW (load-time weaving)或AspectJ编译器 |
编织 | 运行时编织可用,通过动态代理实现 | 支持编译时、后编译、加载时编织,运行时编织已完成 |
编织范围 | 只支持方法级别的编织 | 支持字段、方法、构造函数、静态块等各范围编织 |
切点范围 | 仅支持方法执行切点 | 支持所有切入点 |
织入有效性 | 同一类中的其它方法调用连接点方法将导致连接到的织入操作失效, 如常见的类内部方法调用类中事务方法导致事务失效问题 | 织入操作有效性不受任何调用方式影响 |
支持对象 | 只支持Spring容器管理的bean | 任何对象,因为是直接往类中添加操作字节码 |
性能 | 比AspectJ慢很多,运行时才根据代理生成实际操作类 | 性能更好,编译时便可完成操作的编织,运行时无额外开销,基准测试表明AspectJ几乎比Spring AOP快8到35倍。 |
难度 | 易学 | 如果以前文例子则编写难度一样,但要学习aj则较复杂 |
看起来AspectJ在很多方面都比Spring AOP好很多诶,但个人认为选择哪个框架最终决定点在于需求,比如:
- 灵活性:Spring AOP无需添加额外的编织器、依赖等,虽然只支持方法级别的切点,但已满足大部分需求
- 性能:AspectJ性能比Spring AOP好很多,如果切面的连接点请求较多,可以选用AspectJ;但如果性能要求不高,比如一些后台管理的鉴权,就那点管理员就没必要瞎忙了,直接Spring AOP即可,反正我经常就是这样干的
总结
Java代理
|-静态代理:程序运行前已完成行为扩展
|- AspectJ:对目标类进行字节码编织,可理解成字节码代理
|-动态代理:程序运行时才进行行为扩展
|- JDK动态代理:根据接口生成子类进行代理 --
|-- Spring AOP
|- Cglib动态代理:根据基类生成子类进行代理 --
至于源码层面的东西就不多逼逼了,记好Spring AOP动态代理的创建入口DefaultAopProxyFactory
即可。最后声明,我跟测试真的没仇。
若全文copy,请备注好原创信息,谢谢