CGLIB代理的门门道道
之前一篇文章介绍了JDK的代理的用法和应用示例,这次我们来研究一下CGLIB代理。
在开始之前,我要先贴一段CGLIB在github上的官方声明:
[!WARNING]
IMPORTANT NOTE: cglib is unmaintained and does not work well (or possibly at all?) in newer JDKs, particularly JDK17+. If you need to support newer JDKs, we will accept well-tested well-thought-out patches… but you’ll probably have better luck migrating to something like ByteBuddy.
这段声明的意思是,这个项目目前没有mt(主理人),并且在新版本JDK中有严重BUG几乎无法正常工作。顺便它还推荐了另一个新兴的基于字节码的代理库ByteBuddy
。
因此,本文所有代码都使用JDK8编译和运行。
基础用法
CGLIB提供的API非常简单易用,核心组件有两个:
Enhancer
类。生成子类并代理方法执行过程的核心。MethodInterceptor
接口。最常用的方法拦截接口,它可以实现常规语义下的Around
增强,可以在目标方法执行前、后执行自定义代码,可以对目标方法的入参做改动,也可以完全不执行目标方法。
除了MethodInterceptor
之外,我们还可以使用Callback
接口下的其它子接口,如FixedValue
接口、LazyLoader
接口等。
更多细节,可以查阅Enhancer
类的注释,里面有详细的说明,本文聚焦于MethodInterceptor
这个最强大的接口,探究其用法和具体应用场景。
这个接口的用法很简单,直接上代码:
// PlayVideo.java
public class PlayVideo {
public String play(String video) {
return "播放视频:" + video;
}
public String pause(String video) {
return "暂停视频:" + video;
}
}
// CglibDemo.java
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import top.tonybee.business.PlayVideo;
public class CglibProxyDemo {
public static void main(String[] args) {
String video = "华尔街之狼";
PlayVideo playVideo = new PlayVideo();
System.out.println(playVideo.play(video));
// 利用cglib生成代理类,增强播放视频的功能
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(PlayVideo.class);
enhancer.setCallback((MethodInterceptor) (o, method, argArr, methodProxy) -> {
String methodName = method.getName();
if (!methodName.equals("play")) { // 不是play方法,就直接调用原方法并返回
return methodProxy.invokeSuper(o, args);
}
String originalPlayAction = (String) methodProxy.invokeSuper(o, args);
return "安装potplayer之后," + originalPlayAction;
});
PlayVideo proxy = (PlayVideo) enhancer.create();
System.out.println(proxy.play(video));
}
}
// 输出为:
// 播放视频:华尔街之狼
// 安装potplayer之后,播放视频:华尔街之狼
这段简单的代码可以实现对PlayVideo
中的play()
方法的代理增强,其核心流程在:
- 创建
Enhancer
实例,设置需要被代理的类 - 在
Enhancer
实例中,设置好Callback的实现实例。本例中使用了lambda写法,如果有更复杂的状态需要管理,可以单独创建MethodInterceptor
接口的实现类,实例化后传入即可。 - 最后,通过
create()
这个API就可以创建出一个代理实例
总结一下,其实就是利用Enhancer
提供的各种API,设置好代理目标类、代理逻辑,然后就可以生成代理实例了。
理解了这个简单用法后,我们下面再来尝试一下使用cglib模拟实现Spring AOP的环绕增强。
简单模拟实现Spring AOP的环绕增强
既然我们可以利用cglib的能力简便的生成一个代理实例,那么对于任何业务类来说,创建其代理实例并增强其指定方法就是我们的核心目标。我们首先创建一个代理工厂,它可以加载切面配置、生成目标类的代理实例;同时我们需要一个切面配置注解,以标识某个类属于切面配置类。
综合上述信息,这个工厂的定义和使用方式大致如下:
// ProxyFactory.java
public class ProxyFactory {
public ProxyFactory() {}
public ProxyFactory(Object aspectInstance) {
registerAspect(aspectInstance);
}
public <T> T createInstance(Class<T> clz) {
// 待实现
}
public void registerAspect(Object aspectInstance) {
// 待实现
}
}
// Aspect.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Aspect {
}
// AopDemo.java
import top.tonybee.aspect.PlayAroundAspect;
import top.tonybee.business.PlayVideo;
public class AopDemo {
public static void main(String[] args) {
ProxyFactory proxyFactory = new ProxyFactory(new PlayAroundAspect());
PlayVideo instance = proxyFactory.createInstance(PlayVideo.class);
System.out.println(instance.play("黑雪公主"));
System.out.println(instance.pause("黑雪公主"));
}
}
框架已经搭好,接下来我们一步步填充它。
首先,我们针对PlayVideo
这个业务类,创建一个切面配置。这个配置需要哪些要素呢:
- 配置中包含切点表达式。因为有切点表达式,这意味着我们还需要创建一个切点注解,来标识表达式所在的方法
- 切入后的具体逻辑方法。同样的,我们需要一个注解来标识某个方法是一个切入的逻辑方法
- 切点需要一个
JoinPoint
类作为参数传入,因此我们也要创建它。因为我们在模拟环绕增强,所以我们就不定义太复杂的JoinPoint
类家族了,只定义一个ProceedingJoinPoint
来代表
综合以上信息,我们的代码如下:
// Pointcut.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Pointcut {
String value() default "";
}
// Around.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Around {
String value() default "";
}
// ProceedingJoinPoint.java
import lombok.RequiredArgsConstructor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
@RequiredArgsConstructor
public class ProceedingJoinPoint {
// 根据MethodInterceptor的intercept()方法的参数,我们可以预见我们需要在JoinPoint中保存这些内容
// 以便实现获取参数、执行方法等Spring AOP中的具体API功能
private final Object obj;
private final Method method;
private final Object[] args;
private final MethodProxy proxy;
public Object proceed() {
// 执行原方法
Object returnObj = null;
try {
returnObj = proxy.invokeSuper(obj, args);
} catch (Throwable e) {
throw new RuntimeException(e);
}
return returnObj;
}
}
// PlayAroundAspect
import top.tonybee.annotation.Around;
import top.tonybee.annotation.Aspect;
import top.tonybee.annotation.Pointcut;
@Aspect
public class PlayAroundAspect {
@Pointcut("execution(* top.tonybee.business.PlayVideo.play(...))")
public void playPc(){}
@Pointcut("execution(* top.tonybee.business.PlayVideo.pause(...))")
public void pausePc(){}
// 环绕增强
@Around("playPc()")
public Object aroundPlay(ProceedingJoinPoint joinPoint){
Object returnObj = joinPoint.proceed();
return "安装potplayer之后," + returnObj;
}
}
现在,我们已经定义好一切了,剩下的就是码核心流程,实现AOP环绕增强的功能。
需要实现的核心部分有:
- 解析切面配置类,具体有:
- 反射拿到注解,提取切点表达式
- 解析切点表达式。Spring中写的非常完善(和复杂),我们就简单一点,用一个正则表达式来捕获类名和方法名
- 实例化
ProceedingJoinPoint
,以便调用切面的逻辑方法 - 保存切面配置类中解析出来的目标类、目标方法和对应的代理逻辑
- 生成代理实例,具体有:
- 判定指定类是否为切面配置中的目标类,指定方法是否为目标方法
- 为目标方法设置好代理路由。这里要注意,如果目标类中有非目标方法,则需要路由到NoOp,它表示不走任何代理,只是执行原方法
- 生成代理实例并返回
思路理清了,通过一番敲敲打打,最终,我们的ProxyFactory
应该大致如下:
// ProxyFactory.java
import cn.hutool.core.util.ReUtil;
import cn.hutool.json.JSONUtil;
import net.sf.cglib.proxy.*;
import top.tonybee.annotation.Around;
import top.tonybee.annotation.Aspect;
import top.tonybee.annotation.Pointcut;
import top.tonybee.aspect.ProceedingJoinPoint;
import java.lang.reflect.Method;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class ProxyFactory {
public ProxyFactory() {}
public ProxyFactory(Object aspectInstance) {
registerAspect(aspectInstance);
}
private final Map<Method, Callback> methodCallbackMap = new HashMap<>();
private final Map<Class<?>, Map<Method, Callback>> clzCallbackMap = new HashMap<>();
// 匹配切面表达式的正则
// TODO 目前只支持execution切面表达式
private static final Pattern POINT_CUT_PATTERN = Pattern.compile("^execution\\(\\*\\s+(?<pkgPart>(\\w+\\.)+)(?<methodPart>\\w+?)\\(.+?\\)\\)$");
private static final String PKG_PART_KEY_STR = "pkgPart";
private static final String METHOD_PART_KEY_STR = "methodPart";
public <T> T createInstance(Class<T> clz) {
if (clzCallbackMap.containsKey(clz)) {
Map<Method, Callback> inClzMethodCallbackMap = clzCallbackMap.get(clz);
// 将该类内的这个method->callback的映射,拆分成两个有序数组,两者一一对应
Method[] inClzMethodArr = inClzMethodCallbackMap.keySet().toArray(new Method[0]);
// inClzMethodArr进一步初始化成一个 method -> index 的map,加速filter的路由速度
Map<Method, Integer> inClzMethodIndexMap = new HashMap<>();
for (int i = 0; i < inClzMethodArr.length; i++) {
inClzMethodIndexMap.put(inClzMethodArr[i], i);
}
List<Callback> callbackList = Arrays.stream(inClzMethodArr).map(inClzMethodCallbackMap::get).collect(Collectors.toList());
callbackList.add(NoOp.INSTANCE); // 追加一个NoOp,作为没有找到切点方法时的默认路由
Callback[] inClzCallbackArr = callbackList.toArray(new Callback[]{});
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(clz);
enhancer.setCallbacks(inClzCallbackArr);
enhancer.setCallbackFilter(method -> {
// 如果method是注册在案的方法,就路由到对应的callback里去
return inClzMethodIndexMap.getOrDefault(method, inClzCallbackArr.length - 1);
});
return (T) enhancer.create();
}
try {
return clz.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void registerAspect(Object aspectInstance) {
Class<?> clz = aspectInstance.getClass();
// 先判断传入的实例是否为Aspect
if (!clz.isAnnotationPresent(Aspect.class)) {
return;
}
// 反射解析获取切入点、切入方法
Method[] methods = clz.getDeclaredMethods();
Map<String, Method> pcMethodMap = new HashMap<>();
Map<String, List<Method>> targetMethodMap = new HashMap<>();
for (Method curMethod : methods) {
Pointcut pcAnno = curMethod.getAnnotation(Pointcut.class);
if (pcAnno != null) { // pc注解不是空的,说明这是一个注册的切入点
// 切入点表达式解析
List<Method> targetMethods = analyzePcExpr(pcAnno.value());
// 存一下目标方法的Method实例
targetMethodMap.put(curMethod.getName(), targetMethods);
// 存一下切入点的方法名,以便后续方便查找切入点
pcMethodMap.put(curMethod.getName(), curMethod);
}
}
for (Method curMethod : methods) {
Around aroundAnno = curMethod.getAnnotation(Around.class);
if (aroundAnno != null) { // 环绕增强注解不为空,说明这是具体的环绕增强方法
String aroundTarget = aroundAnno.value();
if (pcMethodMap.containsKey(removePar(aroundTarget))) {
// 绑定目标方法和执行逻辑
// 创建执行逻辑
List<Method> targetMethodList = targetMethodMap.get(removePar(aroundTarget));
for (Method method : targetMethodList) {
methodCallbackMap.put(method, new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// 组装ProceedingJoinPoint
ProceedingJoinPoint joinPoint = new ProceedingJoinPoint(obj, method, args, proxy);
// 以joinPoint为参数调用环绕方法(也就是curMethod)
return curMethod.invoke(aspectInstance, joinPoint);
}
});
}
}
}
}
// 将同一个类下的不同方法的代理合并
methodCallbackMap.forEach((k, v) -> {
Map<Method, Callback> inClzMethodCallbackMap = clzCallbackMap.computeIfAbsent(k.getDeclaringClass(), aClz -> new HashMap<>());
inClzMethodCallbackMap.put(k, v);
});
}
private String removePar(String content) {
return content.replaceAll("\\(\\)", "");
}
private List<Method> analyzePcExpr(String expr) {
if (expr.isEmpty()) return null;
if (expr.startsWith("execution")) {
// 这是一个方法执行时的切面表达式
Map<String, String> groupNameMap = ReUtil.getAllGroupNames(POINT_CUT_PATTERN, expr);
String pkgPart = groupNameMap.get(PKG_PART_KEY_STR);
String classQName = pkgPart.substring(0, pkgPart.length() - 1);
String methodName = groupNameMap.get(METHOD_PART_KEY_STR);
try {
Class<?> clz = Class.forName(classQName);
return findMethodsByName(clz, methodName);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// TODO 其它切面表达式的支持,例如within等
return null;
}
private List<Method> findMethodsByName(Class<?> clz, String methodName) {
if (clz == null) return new ArrayList<>();
List<Method> findMethods = new ArrayList<>();
Method[] methods = clz.getDeclaredMethods();
for (Method method : methods) {
if (method.getName().equals(methodName)) {
findMethods.add(method);
}
}
return findMethods;
}
}
好了,现在它已经可以生成代理实例,我们执行一下main函数看看:
// AopDemo.java
import top.tonybee.aspect.PlayAroundAspect;
import top.tonybee.business.PlayVideo;
public class AopDemo {
public static void main(String[] args) {
ProxyFactory proxyFactory = new ProxyFactory(new PlayAroundAspect());
PlayVideo instance = proxyFactory.createInstance(PlayVideo.class);
System.out.println(instance.play("黑雪公主"));
System.out.println(instance.pause("黑雪公主"));
}
}
// 输出为:
// 安装potplayer之后,播放视频:黑雪公主
// 暂停视频:黑雪公主
可以看到,play()
方法已经成功被AOP增强了,而pause()
方法不受影响,仍然按原定义执行。
我们尝试给pause()也指定一个切面增强一下:
// PlayAroundAspect.java
import top.tonybee.annotation.Around;
import top.tonybee.annotation.Aspect;
import top.tonybee.annotation.Pointcut;
@Aspect
public class PlayAroundAspect {
@Pointcut("execution(* top.tonybee.business.PlayVideo.play(...))")
public void playPc(){}
@Pointcut("execution(* top.tonybee.business.PlayVideo.pause(...))")
public void pausePc(){}
// 环绕增强
@Around("playPc()")
public Object aroundPlay(ProceedingJoinPoint joinPoint){
Object returnObj = joinPoint.proceed();
return "安装potplayer之后," + returnObj;
}
@Around("pausePc()")
public Object aroundPause(ProceedingJoinPoint joinPoint){
Object returnObj = joinPoint.proceed();
return "放下手中的奶茶," + returnObj;
}
}
// main执行后的输出:
// 安装potplayer之后,播放视频:黑雪公主
// 放下手中的奶茶,暂停视频:黑雪公主
可以看到,同一个方法的两个切点,都可以顺利执行各自定义好的切面逻辑aroundPlay()
和aroundPause()
。
到这里,我们就可以说我们成功模拟了Spring AOP的环绕增强。
开新坑环节:
虽然在JDK8下,CGLIB运行的很好,但如果我们的项目如果有需要升级到新版JDK,那么CGLIB在高版本JDK下运行报错、BUG等问题,就成为了它的致命缺陷。后面我会专门再写一篇文章介绍一个新的代理库ByteBuddy,它同样有简洁优雅的API和高性能的代理实例。另外,就在几天前,JDK发布了JDK24,带来了非常多的新特性,其中有一项与基于字节码的代理生成模式息息相关,我也会跟进这项重要改动,写一篇文章为大家梳理最新的关于字节码生成的JDK API。
附录
pom.xml中的依赖如下:
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.26</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>3.3.0</version>
</dependency>
</dependencies>
编译和运行时:JDK8
文章中提到的模拟Spring AOP的代码的仓库地址:github仓库