CGLIB代理的门门道道

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非常简单易用,核心组件有两个:

  1. Enhancer类。生成子类并代理方法执行过程的核心。
  2. 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()方法的代理增强,其核心流程在:

  1. 创建Enhancer实例,设置需要被代理的类
  2. Enhancer实例中,设置好Callback的实现实例。本例中使用了lambda写法,如果有更复杂的状态需要管理,可以单独创建MethodInterceptor接口的实现类,实例化后传入即可。
  3. 最后,通过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这个业务类,创建一个切面配置。这个配置需要哪些要素呢:

  1. 配置中包含切点表达式。因为有切点表达式,这意味着我们还需要创建一个切点注解,来标识表达式所在的方法
  2. 切入后的具体逻辑方法。同样的,我们需要一个注解来标识某个方法是一个切入的逻辑方法
  3. 切点需要一个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环绕增强的功能。

需要实现的核心部分有:

  1. 解析切面配置类,具体有:
    1. 反射拿到注解,提取切点表达式
    2. 解析切点表达式。Spring中写的非常完善(和复杂),我们就简单一点,用一个正则表达式来捕获类名和方法名
    3. 实例化ProceedingJoinPoint,以便调用切面的逻辑方法
    4. 保存切面配置类中解析出来的目标类、目标方法和对应的代理逻辑
  2. 生成代理实例,具体有:
    1. 判定指定类是否为切面配置中的目标类,指定方法是否为目标方法
    2. 为目标方法设置好代理路由。这里要注意,如果目标类中有非目标方法,则需要路由到NoOp,它表示不走任何代理,只是执行原方法
    3. 生成代理实例并返回

思路理清了,通过一番敲敲打打,最终,我们的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仓库

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值