前言
这篇文章是从《图与循环依赖、死锁(一):为何我的循环依赖时好时坏?》拆出来的,在文章提到解决循环依赖的手段,其实还可以通过MethodHandle来解决循环依赖,但不在这篇讲述是因为又重新思考了一下Spring 中 AOP是运行机制,重新复习了一下AOP,放在《图与循环依赖、死锁(一):为何我的循环依赖时好时坏?》里面感觉篇幅有些大, 于是将这篇文章迁移到了这里。每当我回顾我旧有的文章,总是发现可以写的更好,有写的不到位的地方。
我在《图与循环依赖、死锁(一):为何我的循环依赖时好时坏?》里面提到如何解决循环依赖的时候,事实上有想通过MethodHandles来解决的,但是获取MethodHandler的时候,我写成了下面这样:
php
@Component
public class A02TestService {
private static final MethodHandle METHOD_HANDLE;
// A01TestService 是接口,有一个sayHello02方法,void是返回值
// 里面的实现就是打印一个hello world
// A01TestServiceImpl是A01TestService的实现类
static {
try {
METHOD_HANDLE = MethodHandles.lookup().findVirtual(A01TestServiceImpl.class, "sayHello02", MethodType.methodType(void.class));
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public void sayHello01() {
A01TestService a01TestService = SpringUtil.getBean(A01TestService.class);
try {
METHOD_HANDLE.invoke(a01TestService);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}
然后报了这个错, java.lang.ClassCastException: Cannot cast jdk.proxy2.$Proxy71 to com.example.faiz.controller.A01TestServiceImpl。
本篇看的代码基于Spring Boot 3.0 + JDK 17,思想是可以通用的。
代理的原理实现
原因也在我们的意料之中,我们在sayHello02这个方法上打上了@Async这个注解,这代表我们希望在这个方法执行的时候由同步执行转为异步执行。我们知道Java的代码都在确定的线程里面执行,执行到这个方法如果不开启线程,那么这个方法就进入当前线程的方法栈执行。那sayHello01在调用sayHello02的时候,并没有显式的开启线程,那么执行到sayHello02必然是在执行的时候获得了增强,那么怎么不改动方法对方法进行增强呢? 这也就是AOP了,AOP底层的实现是动态代理。
AOP的核心概念

-
Aspect 切面:
- Join point连接点: 方法的执行时机, 执行前、执行后、发生异常
- Advice 通知: 特定连接点的执行逻辑
- Pointcut 切点: 用来判断是否是连接点的表达式
-
Weaving 织入时机
- 编译期织入
- 加载期织入
- 运行期织入
-
Joint Point 连接点
- 通知,也就是增强逻辑的执行时机
-
Introduction : Spring AOP 允许把 新的接口及其对应的默认实现 动态引入到任何被增强的对象中。 例如,可通过 Introduction 让某个 Bean 额外实现
IsModified
接口,以简化缓存逻辑。 (在 AspectJ 社区,Introduction 也称 inter-type declaration,即"类型间声明"。)
常见实现AOP的方式大致可以分为两种,一种是在连接点直接将通知的字节码织入,一种是通过生成目标的代理对象,核心还是做向下转型实现对目标的方法的增强。
字节码植入
csharp
// 这里只是说明思路,不代表真实实现
public void sayHello02(){
new Thread(()->{
System.out.println("hello world");
})
}
常见的库是AspectJ,织入的时机有:
- 编译时织入: AspectJ 自带了一个编译器,可以将通知织入到连接点。
- 编译后织入: 就是在javac编译完成后,用AspectJ自带的编译器处理class,将通知织入连接点。
- 加载时织入:通过java agent机制在内存中操作类文件,可以不需要ajc的支持做到动态织入
这里简单介绍一下java agent,Java Agent 是一种特殊的Java程序,可以作为插件挂载到任何目标Java应用上,在Java加载类的字节码文件之前,获得拦截和修改字节码的机会,从而实现对目标应用的无侵入式功能增强。
运行期实现
另一种实现AOP的方式是在运行期产生代理类,代理类是被代理类的子类,利用的是向下转型的机制,分为两种一种是基于接口的代理,一种是基于继承的。两种实现本质上都是通过子类型重写目标方法来实现增强的。
基于子类的代理
基于子类的代理被为人熟知的是cglib, 但注意这个已经是魔改过的cglib库了,Spring自己fork了一个分支,一些LLM会告诉你,Spring 用ByteBuddy 替换了实现。但这是胡编的,详情可以看参考文档[2] 下面是用cglib做动态代理的代码:
java
public class Car {
public void start() {
System.out.println("start");
}
}
public class SuperMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
Object returnValue = methodProxy.invokeSuper(obj, args);
return returnValue;
}
}
public class MyMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("方法执行前执行");
System.out.println(Arrays.toString(args));
Object returnValue = methodProxy.invokeSuper(obj, args);
System.out.println("方法执行后执行");
return returnValue;
}
}
Enhancer enhancer = new Enhancer();
// 设置需要增强的类。
enhancer.setSuperclass(Car.class);
// 设置通知
enhancer.setCallbacks(new Callback[]{new SuperMethodInterceptor(),new MyMethodInterceptor()});
// 设置切点,因为通知会有多个
// 所以我们需要通过填入callBackFilter来判断,让方法走哪个通知
// 我们这里只增强start的方法, 其他还是调方法本身
enhancer.setCallbackFilter(new CallbackFilter() {
@Override
public int accept(Method method) {
if(method.getName().equals("start")){
return 1;
}
return 0;
}
});
// 创建实际的代理对象
Car car = (Car) enhancer.create();
car.start();
基于接口的代理
下面是基于接口的动态代理示例:
typescript
public interface ICar {
void start();
}
public class Car implements ICar {
public void start() {
System.out.println("start");
}
}
private static <T> T getProxyObject(Object target, final Class<T> interfaceType) {
if (!interfaceType.isInterface()) {
throw new IllegalArgumentException("interfaceType 必须是一个接口类型");
}
Object proxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), new Class<?>[]{interfaceType}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
for (Method interfaceTypeMethod : interfaceType.getMethods()) {
if (interfaceTypeMethod.getName().equals("start")) {
System.out.println("增强start接口开始");
Object obj = method.invoke(target, args);
System.out.println("只增强start接口");
return obj;
}
}
return method.invoke(target, args);
}
});
return (T) proxy;
}
Car car = new Car();
ICar proxyObject = getProxyObject(car, ICar.class);
proxyObject.start();
这里简单的谈一下本质, JDK的动态代理本质上基于接口做了一个匿名对象,这个匿名对象和需要增强的对象实现了相同的接口,或者说这个匿名对象本质上就是用接口创建的。这个匿名对象里面的成员变量就是我们传入的new InvocationHandler()。
回到上面的讨论:
我们在MethodHandlers拿到的是A01TestServiceImpl的sayHello02,但是我们我们从容器里面获取的时候,获取的是代理Bean,代理Bean不是A01TestServiceImpl。就报了类型转换错误。 动态代理本质上用的就是向下转型的思想,扩展父类的方法。那其实该上面的问题也比较简单,代理类仍然是A01TestService的子类,所以我们去拿接口的方法句柄即可:
php
private static final MethodHandle METHOD_HANDLE = MethodHandleUtils.findVirtual(A01TestService.class, A01TestService::sayHello02,MethodType.methodType(void.class));
public void sayHello01() {
A01TestService a01TestService1 = SpringUtil.getBean(A01TestService.class);
try {
METHOD_HANDLE.invoke(a01TestService1);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
但有时候你会发现你的代理没有开启成功向下面这样:
less
@Component
public class A01TestServiceImpl implements A01TestService {
@Autowired
private A02TestService a02TestService;
@Async
@Override
public void sayHello02(String currentThreadName){
if (Thread.currentThread().getName().equals(currentThreadName)){
System.out.println("代理未成功开启");
}
System.out.println("hello world");
}
@Override
public void sayHello01(){
this.sayHello02(Thread.currentThread().getName());
}
}
如果你调用的时候是从sayHello01开始的,这样就会输出代理未成功开启,这让我思考在Spring中AOP是如何发挥作用的。
我的猜想
我的第一个猜想是外部的Bean在通过A01TestService的实例来调用sayHello01的时候,判断sayHello01不满足AOP的条件,于是不为sayHello01()开启代理。那仔细想想这个猜想也有些不合理,如果不是代理对象,怎么做额外的判断逻辑,也就是说如你没有增强A01TestService这个类。我们看到上面的cglib的CallbackFilter只能返回一个通知,那我们如果有多个切面该怎么办呢? 可以将CallbackFilter里面做文章,也就是说我们制定的通知里面,传入当前Bean的切面,然后遍历这个切面即可。这是我的猜想,下面让我们去看源码。
AOP是如何发挥作用的
现在我们将验证我们的猜想,即Spring中注入的变量已经是代理对象了,不是原来的Bean。这里我的验证思路首先就是观察堆栈法,在执行到对应Bean的时候判断观察方法调用链路:
kotlin
@RestController
public class TestController {
@Autowired
private A01TestService a01TestService;
@GetMapping("/test")
public String test() {
a01TestService.sayHello02("sss"); // 这里打断点
return "hello world";
}
}

观察到这一点之后,我们就可以看代理的开启流程了,在IDEA的方法栈里面:

我们接着看这个obj是从哪里获取的即可:

其实是在InvocableHandlerMethod里面,截图的时候看错了,InvocableHandlerMethod是HandlerMethod的子类,getBean在HandlerMethod里面:
typescript
public Object getBean() {
return this.bean;
}
而这个Bean是在HandlerMethod初始化的时候赋值的:
ini
public HandlerMethod(Object bean, String methodName, Class<?>... parameterTypes) throws NoSuchMethodException {
super(bean.getClass().getMethod(methodName, parameterTypes));
this.bean = bean;
this.beanFactory = null;
this.messageSource = null;
this.beanType = ClassUtils.getUserClass(bean);
this.validateArguments = false;
this.validateReturnValue = false;
evaluateResponseStatus();
this.description = initDescription(this.beanType, getMethod());
}
现在我们只需要观察HandlerMethod 的初始化链路即可。沿着链路往上追在AbstractHandlerMethodMapping的getHandlerInternal方法我们可以看到查找Bean的逻辑:
java
@Override
@Nullable
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
String lookupPath = initLookupPath(request);
this.mappingRegistry.acquireReadLock();
try {
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
//在 createWithResolvedBean方法中我们可以看到这个查找Bean的逻辑
return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}
finally {
this.mappingRegistry.releaseReadLock();
}
}
在 createWithResolvedBean方法中我们可以看到这个查找Bean的逻辑:
kotlin
public HandlerMethod createWithResolvedBean() {
Object handler = this.bean;
if (this.bean instanceof String beanName) {
Assert.state(this.beanFactory != null, "Cannot resolve bean name without BeanFactory");
handler = this.beanFactory.getBean(beanName);
}
Assert.notNull(handler, "No handler instance");
return new HandlerMethod(this, handler, false);
}
观察handler可以看到,这里其实TestController里面已经是代理的Bean了,这里已经准备增强了嘛? 那我们这么想,如果一个方法上有多个增强声明,比如@Transactional、@Retryable,那应该怎么依次代理呢
进入代理的时机
我们在a01TestService调用sayHello02这一行,step Into 会发现进入到了DynamicAdvisedInterceptor这个类里面, DynamicAdvisedInterceptor实现了MethodInterceptor, MethodInterceptor是一个环绕通知(我们上面讲过的),方法在执行的时候会进入到intercept。
那现在我们有通知有了,去哪里找切点呢? 于是我下意识的找这个类在哪里被引用,然后没找到有点泄气。是不是要转换下思路了呢? 在我的意识里面,写切点,写通知,写连接点。然后就能AOP了,Spring帮我们做了这一切。那如果让我们站在Spring的开发者的角度呢? 那是不是应该在创建Bean的时候就要为这个类来创建代理类呢? 然后方法在执行的时候进入对应的代理链,看是否需要代理。
于是我们观察TestController成员变量a01TestService的结构:

可以看到TestController在注入的时候就是代理类对象,于是我们猜想A01TestService在创建Bean的时候,判断如果这个类里面存在需要代理,或者就给所有的Bean生成代理类,只是在执行Bean对应方法的时候,经过一个代理器链,或者说代理类里面就放了代理器链过一道执行即可。这是我们的猜想,现在让我们来验证这一点。注意观察a01TestService, 里面的成员变量,结合上面我们介绍的动态代理接口实现,应该是通过接口代理的形式传入的代理对象。
DynamicAdvisedInterceptor 的创建时间
我们知道使用类的普通方法的时候,一定要通过对象来进行调用,那既然我们在调用A01TestService中的方法的时候进入到了这个代理类,那么我们不妨看看这个DynamicAdvisedInterceptor 是在什么时候初始化的, DynamicAdvisedInterceptor是CglibAopProxy这个类的子类,而且是私有内部类,于是我们可以看到DynamicAdvisedInterceptor的初始化在CglibAopProxy的getCallbacks里面对DynamicAdvisedInterceptor进行了初始化:

我们在这一行打上断点观察即可,启动Spring Boot 可以观察到在启动的时候,断点悬停到了这里:

观察线程堆栈可以发现里面有createBean, doCreateBean这个逻辑:
scss
AbstractBeanFactory.doGetBean(...) // 获取或创建 Bean
└── AbstractAutowireCapableBeanFactory.createBean(...)
└── AbstractAutowireCapableBeanFactory.doCreateBean(...)
└── AbstractAutowireCapableBeanFactory.initializeBean(...)
└── AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(...)
└── AbstractAutoProxyCreator.postProcessAfterInitialization(...)
└── AbstractAutoProxyCreator.wrapIfNecessary(...)
└── AbstractAutoProxyCreator.createProxy(...)
└── AbstractAutoProxyCreator.buildProxy(...)
└── ProxyFactory.getProxy(...)
└── CglibAopProxy.getProxy(...)
└── CglibAopProxy.buildProxy(...)
└── CglibAopProxy.getCallbacks(...)
顺着这个方法链路, 在CglibAopProxy的buildProxy可以看到熟悉的Cglib代理类:
scss
Enhancer enhancer = createEnhancer();
if (classLoader != null) {
enhancer.setClassLoader(classLoader);
if (classLoader instanceof SmartClassLoader smartClassLoader &&
smartClassLoader.isClassReloadable(proxySuperClass)) {
enhancer.setUseCache(false);
}
}
enhancer.setSuperclass(proxySuperClass);
enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setAttemptLoad(true);
enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(classLoader));
Callback[] callbacks = getCallbacks(rootClass);
Class<?>[] types = new Class<?>[callbacks.length];
for (int x = 0; x < types.length; x++) {
types[x] = callbacks[x].getClass();
}
// fixedInterceptorMap only populated at this point, after getCallbacks call above
ProxyCallbackFilter filter = new ProxyCallbackFilter(
this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset);
// 注意这个callBackFilter
enhancer.setCallbackFilter(filter);
enhancer.setCallbackTypes(types);
// Generate the proxy class and create a proxy instance.
// ProxyCallbackFilter has method introspection capability with Advisor access.
try {
return (classOnly ? createProxyClass(enhancer) : createProxyClassAndInstance(enhancer, callbacks));
}
finally {
// Reduce ProxyCallbackFilter to key-only state for its class cache role
// in the CGLIB$CALLBACK_FILTER field, not leaking any Advisor state...
filter.advised.reduceToAdvisorKey();
}
}
catch (CodeGenerationException | IllegalArgumentException ex) {
throw new AopConfigException("Could not generate CGLIB subclass of " + this.advised.getTargetClass() +
": Common causes of this problem include using a final class or a non-visible class",
ex);
}
catch (Throwable ex) {
// TargetSource.getTarget() failed
throw new AopConfigException("Unexpected AOP exception", ex);
}
callBackFilter的逻辑

回到getCallbacks的逻辑:
scss
Callback[] mainCallbacks = new Callback[] {
aopInterceptor, // for normal advice
targetInterceptor, // invoke target without considering advice, if optimized
new SerializableNoOp(), // no override for methods mapped to this
targetDispatcher, this.advisedDispatcher,
new EqualsInterceptor(this.advised),
new HashCodeInterceptor(this.advised)
};
这个aopInterceptor 就是DynamicAdvisedInterceptor:

然后我们观察这个代理链是如何执行的就可以了,也就是CglibMethodInvocation中proceed的逻辑:
kotlin
@Override
@Nullable
public Object proceed() throws Throwable {
// We start with an index of -1 and increment early.
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
return invokeJoinpoint();
}
Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher dm) {
// Evaluate dynamic method matcher here: static part will already have
// been evaluated and found to match.
Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
if (dm.matcher().matches(this.method, targetClass, this.arguments)) {
return dm.interceptor().invoke(this);
}
else {
// Dynamic matching failed.
// Skip this interceptor and invoke the next in the chain.
return proceed();
}
}
else {
// It's an interceptor, so we just invoke it: The pointcut will have
// been evaluated statically before this object was constructed.
return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
}
}
这里我们看到在调用AOP链路。于是我们得到了我们想要的结果,那DynamicAdvisedInterceptor中的代理快照是什么时候传入的。
切面传入的时机
我们观察方法链路可以看到, 在AbstractAutoProxyCreator的wrapIfNecessary,我们可以看到在创建Bean的时候,这个方法拿到Bean对应的通知,然后一路传给DynamicAdvisedInterceptor:
kotlin
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}
if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
// 这里判断需要拦截
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
specificInterceptors获取Bean上对应的切面,然后传递给createProxy方法:
less
private Object buildProxy(Class<?> beanClass, @Nullable String beanName,
@Nullable Object[] specificInterceptors, TargetSource targetSource, boolean classOnly) {
if (this.beanFactory instanceof ConfigurableListableBeanFactory clbf) {
AutoProxyUtils.exposeTargetClass(clbf, beanName, beanClass);
}
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);
if (proxyFactory.isProxyTargetClass()) {
// Explicit handling of JDK proxy targets and lambdas (for introduction advice scenarios)
if (Proxy.isProxyClass(beanClass) || ClassUtils.isLambdaClass(beanClass)) {
// Must allow for introductions; can't just set interfaces to the proxy's interfaces only.
for (Class<?> ifc : beanClass.getInterfaces()) {
proxyFactory.addInterface(ifc);
}
}
}
else {
// No proxyTargetClass flag enforced, let's apply our default checks...
if (shouldProxyTargetClass(beanClass, beanName)) {
proxyFactory.setProxyTargetClass(true);
}
else {
evaluateProxyInterfaces(beanClass, proxyFactory);
}
}
// 注意这里 Advisor包含切点,切面,连接点
// proxyFactory 是代理工厂, 将切面放置到对应的连接点上
Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors);
proxyFactory.setTargetSource(targetSource);
customizeProxyFactory(proxyFactory);
proxyFactory.setFrozen(this.freezeProxy);
if (advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true);
}
// Use original ClassLoader if bean class not locally loaded in overriding class loader
ClassLoader classLoader = getProxyClassLoader();
if (classLoader instanceof SmartClassLoader smartClassLoader && classLoader != beanClass.getClassLoader()) {
classLoader = smartClassLoader.getOriginalClassLoader();
}
return (classOnly ? proxyFactory.getProxyClass(classLoader) : proxyFactory.getProxy(classLoader));
}
我们现在已经明白了Spring 中AOP是如何发挥作用的,底层还是Cglib,创建Bean的时候看看Bean身上是否有请求代理的声明,如果有进入到创建代理的逻辑,其实注入的已经是代理对象了,代理对象在执行目标方法的时候,会经过一道又一道的回调过滤器,满足对应的过滤器,会进入到对应的增强方法中,触发代理逻辑。

注意也不是所有的增强都是在AbstractAutoProxyCreator中完成代理的创建,比如@Async对应的增强,这一点我是在《再探Spring IOC的生命周期》观测到的。@Async的增强我们将会在一篇新的文章中讲述。
总结一下
我们到现在已经对Spring 中AOP的执行机制有了一个大致的了解,在创建Bean的时候根据Bean的元信息是否产生代理对象,放入到容器里面。这样目标方法在执行的时候,就依次进入到代理链,每个链都会判断当前方法是否要被代理。其实写这篇文章的时候思路倒又被重新梳理了一下。 这篇在梳理的时候是零零散散的,刚开始我以为AOP是在调用方法的时候判断切点是否满足,后面又觉得这个不太合适,因为判断是否满足条件就在方法逻辑之外,这个逻辑不属于原方法的逻辑。除非是通过字节码植入的方式进行增强的。
但我的判断并非没有依据,因为想到了内部调用代理不生效的这个场景,尝试为这个生效场景做解释,也就是下面这样:
typescript
@Service
public class UserService {
@Transactional // 会被 AOP 拦截
public void createUser(){ ... }
public void doBusiness(){
// 直接调用 -> 实际是 this.createUser(),绕过代理
createUser(); // 事务不生效
}
}
所以我理解应当是从外部调用的时候,已经是代理对象了,只不过首次在进入Bean的方法调用的时候才会判断是否开启代理方法。 像上面的代码调用,doBusiness调用的时候,这个this已经是目标Bean了,无法经过代理。那怎么解决呢? 我们可以请求容器暴露代理对象:
ini
@EnableAspectJAutoProxy(exposeProxy = true)
然后可以通过如下的方式拿到代理对象:
scss
public void doBusiness(){
// 拿到代理再调用,切面继续生效
((UserService) AopContext.currentProxy()).createUser();
}
参考资料
1\]Aopalliance-03-原生 AspectJ 3 种织入方式及 spring-aop 原理分析 [houbb.github.io/2019/02/26/...](https://link.juejin.cn?target=https%3A%2F%2Fhoubb.github.io%2F2019%2F02%2F26%2Faopalliance-03-aspectj-02-3ways "https://houbb.github.io/2019/02/26/aopalliance-03-aspectj-02-3ways") \[2\] Support for ByteBuddy as an alternative to CGLIB [github.com/spring-proj...](https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fspring-projects%2Fspring-framework%2Fissues%2F12840 "https://github.com/spring-projects/spring-framework/issues/12840")