从Java代理到AOP用例

913 阅读11分钟

简介

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动态代理的实现流程如下:

  1. 确认代理目标类实例,目标类必须实现了接口 JDK动态生成的代理类同样实现了目标类的接口,但并没有继承目标类,其实现一般包含了目标类的行为,且可根据需要自定义额外操作。若目标类没有实现接口,类转换时将抛出ClassCastException异常。
  2. 创建调用处理器InvocationHandler实例,该实例通过invoke()方法代理调用目标类操作
  3. 通过Proxy.newProxyInstance()方法生成动态代理类$ProxyX实例,该实例包含了代理目标类的方法名,区别在于这些方法的执行都是交给InvocationHandler
  4. $ProxyX实例转为相应的目标类接口,执行所需要的方法

代理过程中涉及到的类的关系图如下所示: JDK-Proxy.png

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创建。其实现流程如下:

  1. 创建Enhancer实例,设置代理目标类,设置方法回调(方法拦截器)实例
  2. 通过Enhancercreate()方法创建代理类实例,代理类实例转为目标类实例
  3. 调用实例方法

例子

代理目标类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);
    }
    
    ......
}

cglib

静态代理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实现步骤如下:

  1. 定义切点所需的注解如@LogAspect
  2. 定义切面@Aspect、切点@Pointcut、编织操作@Before@After@Around
  3. 功能测试

之后便是个人的例子搭建了

添加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-DogClass.png

AspectJ静态代理与Spring AOP动态代理的切换

  • 如果保留AspectJ的maven编织器插件,切面LogAspect补充@Component注解,Dog添加@Component改为单例(切点注解一般用在单例的service或controller方法上,如controller请求与响应日志), Spring会怎么进行代理? 依旧会以AspectJ静态代理,不会生成任何的动态类,Dog字节码中依旧保留了编织器的织入,在IDEA中编译后的测试例子如下(输出的单例Dog依旧是原生类): AspectJ-SpringAopStaticTest.png

  • 去掉AspectJ的maven编织器插件后,Spring会用回动态代理,以下为去掉后的输出图(由于Dog没有实现接口,所以Spring AOP用的是cglib动态代理): AspectJ-SpringAopDynamicAspectTest.png

    由上可知,在Spring环境中AspectJ与Spring AOP动态代理的切换其实十分简单,只需添加一个AspectJ编织器maven插件即可,切面类(如LogAspect)与连接点所在类(如Dog)的单例注解虽然对AspectJ来说是冗余的,但加了也不会有负面作用。

AOP框架对比:Spring AOP与AspectJ

特点Spring AOPAspectJ
实现方式纯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中使用AspectJ的切面编写方式与使用动态代理的编写方式区别只在于一个编织器,切换也比较方便。至于源码层面的东西就不多逼逼了,记好Spring AOP动态代理的创建入口DefaultAopProxyFactory即可。 文中例子链接

上周无意中发现X书上有一篇文章直接从我之前发的二叉树文章里一字不漏的copy过去了,而且还没有标注任何原创信息,在评论区写了自己原创文章CSDN和掘金的地址不久后评论就被这个copy的删除了。说实话遇到这样的事真的很气愤,我吐血整理,别人顺手copy,后来在沸点说了下后也有不少掘友帮忙举报了,十分感谢。
我发文不为流量收益,觉得对别人没啥用的笔记也不会发到平台上只放到自己的知识梳理里,毕竟有不少都被写烂了,大部分是兴趣与笔记顺手使然,可能几周一个月才发一篇文章,我不介意别人借鉴我文章中的内容,如果转载只要标下我的在任一平台的名称即可,也不需要链接,但很不高兴的是看到自己的作品被别人拿去标注原创然后引流噶韭菜。总有一些人不付出的把别人的作品拿过来为自己赚取收益,比如一些开发者的开源项目被一些培训机构当作产品,或被拿去咸鱼卖,希望大家以后如果遇到这种事可以帮忙举报一下,每个创作者都希望自己的著作可以收到尊重,谢谢。

最后声明,我跟测试真的没仇。

p.s. 私家桶又添一文,有空分享下我是怎么用hexo搞首页文章索引来进行知识梳理的,你们的点赞和关注并不是我关注滴动力

hexo blog