·

老生常谈系列之Aop--Spring Aop原理浅析

老生常谈系列之Aop--Spring Aop原理浅析

概述

上一篇介绍了AspectJ的编译时织入(Complier Time Weaver),其实AspectJ也支持Load Time Weaver, LTW依赖于java的agent,不了解的可以参考Oracle文档JSR-163,现在市面上很多APM厂商监控Java就是基于agent。 通过替换c参数即可生效。由于本文主要方向为Spring Aop的原理,AspectJ的只是提一下,下文不再深入介绍。

// ${path}替换成你的路径
-javaagent:/${path}/aspectjweaver-1.8.3.jar

这篇会简单介绍Spring Aop的动态代理和例子,以及Spring Aop的简单实现原理,不同于Compiler Time Weaver和Loadtime Weaver,Spring Aop使用的是Runtime Weaver。
摘抄官网原文一段话。

Weaving: linking aspects with other application types or objects to create an advised object. 
This can be done at compile time (using the AspectJ compiler, for example), load time, or at runtime. 
Spring AOP, like other pure Java AOP frameworks, performs weaving at runtime.

考虑到Spring Aop的实现兼顾了很多细节,因此一开始这里不进行过多的具体实现讨论,以免一开始就陷入细节而无法自拔。为什么这篇叫原理浅析,就是不过分深入的研究各种实现细节,通过一些简单的例子和文档介绍,力争对Spring Aop的实现有个全局的观念。分析别人的代码实现,首要任务就是清晰方向,知晓原理才能在后面的深入分析不迷路。

既然明确了目标,那我这篇文章就结论先行,后续再去分析验证这个结论。这里直接抛出结论:Spring Aop使用的是动态代理,并且同时支持JDK动态代理和CGLIB代理。这里先简单介绍动态代理原理,然后推断Spring Aop的实现机制。

那么这里就涉及一个代理的概念,相信大家都懂。AOP的实现手段之一是建立在Java语言的反射机制与动态代理机制之上的。业务逻辑组件在运行过程中,AOP容器会动态创建一个代理对象供使用者调用,该代理对象已经按代码编写人员的意图将切面成功切入到目标方法的连接点上,从而使切面的功能与业务逻辑的功能都得以执行。从原理上讲,调用者直接调用的其实是AOP容器动态生成的代理对象,再由代理对象调用目标对象完成原始的业务逻辑处理,而代理对象则已经将切面与业务逻辑方法进行了合成。

动态代理分为JDK动态代理和CGLib动态代理,这两种Spring Aop都支持,这两种代理各有优劣。

  • JDK 动态代理用于对接口的代理,动态产生一个实现指定接口的类,注意动态代理有个约束目标对象一定是要有接口的,没有接口就不能实现动态代理,只能为接口创建动态代理实例,而不能对类创建动态代理。

  • CGLIB 用于对类的代理,把被代理对象类的 class 文件加载进来,修改其字节码生成一个继承了被代理类的子类。使用 cglib 就是可以弥补动态代理的不足。

下面搞几个例子看一下用法。

JDK动态代理

代码例子

说到JDK动态代理,就不得不看java.lang.reflect包下的一个接口InvocationHandler,所有的代理类的InvocationHandler实例都要实现这个接口,那这个接口的作用是什么?摘取一段接口上的注释如下:

  {@code InvocationHandler} is the interface implemented by
  the <i>invocation handler</i> of a proxy instance.
 
  <p>Each proxy instance has an associated invocation handler.
  When a method is invoked on a proxy instance, the method
  invocation is encoded and dispatched to the {@code invoke}
  method of its invocation handler.

翻译一下即为:code InvocationHandler 是代理实例的invocation handler要实现的接口。 每个代理实例都有一个关联的调用处理程序。当在代理实例上调用方法时,方法调用被编码并分派到其调用处理程序的 invoke() 方法。

很显然,JDK动态代理的要实现这个接口,在代理实例上的方法调用都会被打包到这里的invoke()方法来,这里就给我们提供了操作的空间,在真正执行调用method.invoke()的前后,我们可以进行想要的操作。

Now,talk is cheap,show me your code.

首先自定义个MyInvocationHandler类实现InvocationHandler接口

/**
 * @author Codegitz
 * @date 2021/12/28 17:49
 **/
public class MyInvocationHandler implements InvocationHandler {

    private CalculateService calculateService;

    public MyInvocationHandler(CalculateService calculateService){
        this.calculateService = calculateService;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("before invoke...");
        return method.invoke(calculateService,args);
    }
}

CalculateService是我们需要代理的服务类,在invoke()方法里面method.invoke(calculateService,args)调用真正的业务逻辑,在前后插入逻辑实现想要的切面功能。

CalculateService类代码。

/**
 * @author Codegitz
 * @date 2021/12/28 17:49
 **/
public interface CalculateService {

    public void calculate();
}

public class CalculateServiceImpl implements CalculateService {
    @Override
    public void calculate() {
        System.out.println("calculate()");
    }
}

有了service,有了InvocationHandler,接下来就是根据这两个根据生成一个代理,那么谁来生成代理类呢?接下来新建一个CalculateServiceProxy生成代理类。

/**
 * @author Codegitz
 * @date 2021/12/28 17:53
 **/
public class CalculateServiceProxy {

    private CalculateService calculateService;

    public CalculateServiceProxy(CalculateService calculateService){
        this.calculateService = calculateService;
    }

    public Object getProxy(){
        return Proxy.newProxyInstance(calculateService.getClass().getClassLoader(),calculateService.getClass().getInterfaces(),new MyInvocationHandler(new CalculateServiceImpl()));
    }
}

以上就是一个简单的JDK动态代理的实现代码了,非常简单,三个要点业务Service,InvocationHandler,生成代理Proxy

万事俱备只欠东风,写个测试跑一下。

/**
 * @author Codegitz
 * @date 2021/12/28 17:57
 **/
public class ProxyTest {

    @Test
    public void proxyTest(){
        CalculateService proxy = (CalculateService) new CalculateServiceProxy(new CalculateServiceImpl()).getProxy();
        proxy.calculate();
    }

}

结果符合预期,在调用真正的业务方法前,执行了我们自己的逻辑。以上就是简单的实现,CalculateService类是写死的,实际上这里应该写得更为通用,但这是代码样例,点到为止。

1640745107773

简单分析

功能是实现了,是不是有点好奇proxy.calculate()中的proxy到底是什么,是通过什么方式实现了这个功能。下面直接把它的字节码扒拉下来,可以在测试方法的代码上加上这一句配置,但是这个只对JDK动态代理生效,CGLIB的不可通过此方式获取字节码。

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");

JDK动态代理的类就会保存在项目的com.sun.proxy目录下

1640768793362

然后用IDEA打开,可以看到反编译的代码如下:可以看到代理类重写了equals(),toString(),hashCode()calculate(),前面三个都是Object自带的方法,暂时忽略,重点来看 calculate()方法。

package com.sun.proxy;

import io.codegitz.service.CalculateService;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements CalculateService {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        //...
    }

    public final String toString() throws  {
        //...
    }

    // 重点关注
    public final void calculate() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        //...
    }

    // 静态代码块里获取对应的方法
    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m3 = Class.forName("io.codegitz.service.CalculateService").getMethod("calculate");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

可以看到super.h.invoke(this, m3, (Object[])null)这句是最后执行了,super.h就是我们自己实现的MyInvocationHandler类,MyInvocationHandler#invoke()方法会调用真正业务实现。

到这里,其实可以看出Proxy.newProxyInstance()会把我们传进去的InvocationHandler实现类和proxy类关联起来,当proxy类执行方法时,最终都会经过InvocationHandler#invoke()方法。这里雾里看花看了个大概,至于Proxy.newProxyInstance()方法是怎么实现字节码修改并且返回一个代理类的,可以看JDK动态代理的实现细节

你可以在测试代码加上这剩下的几个方法,执行结果是均会触发执行。

proxy.toString();
proxy.hashCode();

CGLIB代理

代码例子

上文介绍了JDK动态代理,那这里介绍另一个重要的代理CGLIB。CGLIB(Code Generation Library)是一个开源、高性能、高质量的Code生成类库(代码生成包)。

它可以在运行期扩展Java类与实现Java接口。Hibernate用它实现PO(Persistent Object 持久化对象)字节码的动态生成,Spring AOP用它提供方法的interception(拦截)。

CGLIB的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类。但不鼓励大家直接使用ASM框架,因为对底层技术要求比较高。

使用CGLIB就绕不开Enhancer类,查看一下类上注释

  Generates dynamic subclasses to enable method interception. This
  class started as a substitute for the standard Dynamic Proxy support
  included with JDK 1.3, but one that allowed the proxies to extend a
  concrete base class, in addition to implementing interfaces. The dynamically
  generated subclasses override the non-final methods of the superclass and
  have hooks which callback to user-defined interceptor
  implementations.

翻译一下:这个类用以生成动态子类以启用方法拦截。 此类最初是作为 JDK 1.3 中包含的标准动态代理支持的替代品,但除了实现接口之外,它还允许代理扩展具体的基类。 动态生成的子类覆盖超类的非final方法,并具有回调到用户定义的拦截器实现的钩子。

根据注释的描述,用得最多的是MethodInterceptor方法拦截器,下面的例子我们用MethodInterceptor实现,下面来看看例子。

首先实现MethodInterceptor拦截器

/**
 * @author Codegitz
 * @date 2021/12/29 10:59
 **/
public class MyMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("before invoke by cglib...");
        return methodProxy.invokeSuper(object,objects);
    }
}

业务类复用上面的CalculateService,直接写测试方法。

    @Test
    public void cglibProxyTest(){
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(CalculateServiceImpl.class);
        enhancer.setCallback(new MyMethodInterceptor());
        CalculateService calculateService  = (CalculateService) enhancer.create();
        calculateService.calculate();
    }

测试结果如下,符合预期,这就是CGLIB的简单用法,是不是简单的有一点难以置信。

1640747691537

简单分析

如法炮制,把CGLIB生成的字节码扒拉下来看看,在测试代码Enhancer前添加

System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"C:\\code");

可以看到对应的目录下生成了三个class文件,其中第二是我们想要的代理类的字节码。

1640770170030

反编译出来的码很长,这里只贴关注的部分出来。

public class CalculateServiceImpl$$EnhancerByCGLIB$$4f204217 extends CalculateServiceImpl implements Factory {
    private boolean CGLIB$BOUND;
    public static Object CGLIB$FACTORY_DATA;
    private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
    private static final Callback[] CGLIB$STATIC_CALLBACKS;
    private MethodInterceptor CGLIB$CALLBACK_0;
    private static Object CGLIB$CALLBACK_FILTER;
    private static final Method CGLIB$calculate$0$Method;
    private static final MethodProxy CGLIB$calculate$0$Proxy;
    // ...

    static void CGLIB$STATICHOOK1() {
        CGLIB$THREAD_CALLBACKS = new ThreadLocal();
        CGLIB$emptyArgs = new Object[0];
        Class var0 = Class.forName("io.codegitz.service.CalculateServiceImpl$$EnhancerByCGLIB$$4f204217");
        Class var1;
        Method[] var10000 = ReflectUtils.findMethods(new String[]{"equals", "(Ljava/lang/Object;)Z", "toString", "()Ljava/lang/String;", "hashCode", "()I", "clone", "()Ljava/lang/Object;"}, (var1 = Class.forName("java.lang.Object")).getDeclaredMethods());
        //...
        CGLIB$calculate$0$Method = ReflectUtils.findMethods(new String[]{"calculate", "()V"}, (var1 = Class.forName("io.codegitz.service.CalculateServiceImpl")).getDeclaredMethods())[0];
        CGLIB$calculate$0$Proxy = MethodProxy.create(var1, var0, "()V", "calculate", "CGLIB$calculate$0");
    }

    final void CGLIB$calculate$0() {
        super.calculate();
    }

    // 代理后的 calculate()方法
    public final void calculate() {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (var10000 == null) {
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
        }

        if (var10000 != null) {
            var10000.intercept(this, CGLIB$calculate$0$Method, CGLIB$emptyArgs, CGLIB$calculate$0$Proxy);
        } else {
            super.calculate();
        }
    }
    // 省略部分代码...
   } 

CGLIB的原理不多赘述,不然要跑偏陷入细节了。我们管中窥豹,可以看到这里获取了MethodInterceptor,那么这里显然就是我们实现的MyMethodInterceptor拦截类。如果获取到的拦截器不为空,那么就会通过MyMethodInterceptor#intercept()方法去调用,所以会执行到我们在拦截器里加入的逻辑。仔细一想,MethodInterceptor是不是跟JDK动态代理的InvocationHandler有异曲同工之妙,都是都过封装一次方法的调用,把原有的方法调用转发到我们自定义的invoke()方法上,通过一次转发,提供了额外操作的空间。至于CGLIB是怎么生成字节码和字节码结构的详细解析,可以看这篇CGLIB动态代理的实现细节

Spring Aop的原理浅析

前戏做足,到这里,才进入正题,这小节是后续文章的引子。前面花了比较大的篇幅去介绍了JDK动态代理和CGLIB代理,同时在文章的开头我们知道一个结论Spring Aop是使用动态代理,那么Spring Aop是怎么使用动态代理的呢?

先来看一下Spring Aop中注解实现的切面写法。我这里的例子使用的都是注解驱动,就不搞xml那一套了,原理都是一样的。

定义业务类

/**
 * @author Codegitz
 * @date 2021/12/29 17:55
 **/
@Component
public class LoginService {
    public void login(String name){
        System.out.println("login..." + name);
    }
}

定义切面

/**
 * @author Codegitz
 * @date 2021/12/29 17:53
 **/
@Aspect
@Component
public class SpringAopAspect {
    @Pointcut("execution(* io.codegitz.aop.demo.service.LoginService.login(..))")
    public void login(){}

    @Before("login()")
    public void before(){
        System.out.println("before login...");
    }
}

开启Aop支持

/**
 * @author Codegitz
 * @date 2021/12/29 18:01
 **/
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}

写个Application跑一下

/**
 * @author Codegitz
 * @date 2021/12/29 18:05
 **/
@ComponentScan(basePackages = "io.codegitz.aop.demo")
public class Application {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(Application.class);
        LoginService loginService = (LoginService) applicationContext.getBean("loginService");
        loginService.login("codegitz");
    }
}

1640773520743

可以看到代理生效了,这个类的命名方式是不是很熟悉,这就是CGLIB生成的动态代理的命名。切面逻辑生效,输出符合预期。

1640773831672

可以看到,Spring最后生成的代理类跟我们上面直接使用CGLIB生成的是一样的。那么我们可以开始推理,这两个代码的起点是不一样的,Spring 的起点是一个@Aspect注解的类,而我们的测试代码是直接实现一个MethodInterceptor拦截器。由此是不是可以猜测Spring是把切面类的切面逻辑封装成了拦截器,然后在创建Bean的时候判断是否需要代理,然后再把拦截器注入生成动态代理替换原有的bean,从而实现了Aop功能?

目前猜测的逻辑如下:

1640781706060

这个图展示了我们猜测的Spring Aop使用CGLIB的代理逻辑,使用JDK动态代理逻辑应该也类似。那接下来,我们就从例子入手,另起文章,分析Spring Aop的源码实现。估计源码解析会分上下两篇文章去分析,正在酝酿。

总结

这篇文件叫浅析,实际上并没有去分析源码,而是通过推断概况给出了Spring Aop的原理,所以命名可能有点不妥,但是我不想改,就这样吧。这篇文章主要介绍了两种动态代理,并且简单写了例子和分析。然后我们写了一个Spring Aop的例子,实现了Aop的逻辑,但是碍于篇幅,我并没有在这里展开Spring Aop的实现,而是准备另起文章去分析。同时,我们另起的文章依赖于本文的一个推断:Spring是把切面类的切面逻辑封装成了拦截器,然后在创建Bean的时候进行了代理。可以牢牢记住这个推断,下文会去验证是否就是如此。

有时候抓住了目标,就不会迷路,可以在繁杂的逻辑中,理清脉络,因为我们知道最终要达到的效果,所以会知道这一步的目的是做什么,而不是茫无目的,到处乱逛。走马观花,终究不会深刻。接下来写实现分析,行百里者半九十,坚持就是胜利。

posted @ 2021-12-29 22:00  Codegitz  阅读(402)  评论(4编辑  收藏  举报