0%

Memo Java动态代理

在业务中使用动态代理,一般是为了给需要实现的方法添加预处理或者添加后续操作,但是不干预实现类的正常业务,把一些基本业务和主要的业务逻辑分离。我们一般所熟知的 Spring 的 AOP 原理就是基于动态代理实现的。

反射机制在生成类的过程中比较高效,而 ASM 在生成类之后的相关执行过程中比较高效(可以通过将 ASM 生成的类进行缓存,这样解决 ASM 生成类过程低效问题)。还有一点必须注意:JDK 动态代理的应用前提,必须是目标类基于统一的接口。如果没有上述前提,JDK 动态代理不能应用。由此可以看出,JDK 动态代理有一定的局限性,CGLIB 这种第三方类库实现的动态代理应用更加广泛,且在效率上更有优势。

下面是公用的一小部分代码

public interface Subject {
    void say(String str);
}

public class SubjectImpl implements Subject {
    @Override
    public void say(String str) {
        System.out.println("hello  " + str);
    }
}

JDK 动态代理

public class JDKProxy {

    public static <T> T getProxy(Class<?> clazz) throws Exception {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        return (T) Proxy.newProxyInstance(
                classLoader,
                new Class[]{clazz},
            	//需要实现 InvocationHandler 接口
                new InvocationHandler() {
                    public Object invoke(Object proxy, Method method, Object[] args) {
                        System.out.println(method.getName() + " 被 JDK 代理");
                        return null;
                    }
                });
    }

    public static void main(String[] args) throws Exception {
        Subject subject = JDKProxy.getProxy(Subject.class);
        subject.say("world");
    }
}

通过反射调用程序,无法享受编译器对代码的优化策略。

CGLIB动态代理

public class CglibProxy implements MethodInterceptor {

    public Object newInstall(Object object) {
        return Enhancer.create(object.getClass(), this);
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("say 被 CGLIB 代理");
        return methodProxy.invokeSuper(o, objects);
    }

    public static void main(String[] args) {
        CglibProxy cglibProxy = new CglibProxy();
        SubjectImpl subject = (SubjectImpl) cglibProxy.newInstall(new SubjectImpl());
        subject.say("world");
    }
}

CGLIB 不同于 JDK,它的底层使用 ASM 字节码框架在类中修改指令码实现代理,所以这种代理方式也就不需要像 JDK 那样需要接口才能代理。同时得益于字节码框架的使用,所以这种代理方式也会比使用 JDK 代理的方式快 1.5~2.0 倍。

ASM代理方式

public class ASMProxy extends ClassLoader {

    public static <T> T getProxy(Class clazz) throws Exception {

        ClassReader classReader = new ClassReader(clazz.getName());
        ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);

        classReader.accept(new ClassVisitor(ASM5, classWriter) {
            @Override
            public MethodVisitor visitMethod(int access, final String name, String descriptor, String signature, String[] exceptions) {

                // 方法过滤
                if (!"say".equals(name))
                    return super.visitMethod(access, name, descriptor, signature, exceptions);

                final MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);

                return new AdviceAdapter(ASM5, methodVisitor, access, name, descriptor) {

                    @Override
                    protected void onMethodEnter() {
                        // 执行指令;获取静态属性
                        methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                        // 加载常量 load constant
                        methodVisitor.visitLdcInsn(name + " 被 ASM 代理");
                        // 调用方法
                        methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
                        super.onMethodEnter();
                    }
                };
            }
        }, ClassReader.EXPAND_FRAMES);

        byte[] bytes = classWriter.toByteArray();

        return (T) new ASMProxy().defineClass(clazz.getName(), bytes, 0, bytes.length).newInstance();
    }

    public static void main(String[] args) throws Exception {
        Subject proxy = ASMProxy.getProxy(SubjectImpl.class);
        proxy.say("world");
    }
}

使用字节码编程的方式进行处理,它的实现方式相对复杂,而且需要了解 Java 虚拟机规范相关的知识。因为你的每一步代理操作,都是在操作字节码指令。但这种最接近底层的方式,也是最快的方式。所以在一些使用字节码插装的全链路监控中,会非常常见。

Byte-Buddy代理方式

public class ByteBuddyProxy {

    public static <T> T getProxy(Class clazz) throws Exception {

        DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
                .subclass(clazz)
                .method(ElementMatchers.<MethodDescription>named("say"))
                .intercept(MethodDelegation.to(InvocationHandler.class))
                .make();

        return (T) dynamicType.load(Thread.currentThread().getContextClassLoader()).getLoaded().newInstance();
    }

}

@RuntimeType
public static Object intercept(@Origin Method method, @AllArguments Object[] args, @SuperCall Callable<?> callable) throws Exception {
    System.out.println(method.getName() + " 被 Byte-Buddy 代理");
    return callable.call();
}

public static void main(String[] args) throws Exception {
    Subject subject = ByteBuddyProxy.getProxy(SubjectImpl.class);
    subject.say("world");
}

Byte Buddy 也是一个字节码操作的类库,但 Byte Buddy 的使用方式更加简单。无需理解字节码指令,即可使用简单的 API 就能很容易操作字节码,控制类和方法。比起 JDK 动态代理、CGLIB,Byte Buddy 在性能上具有一定的优势。另外,2015年10月,Byte Buddy 被 Oracle 授予了 Duke’s Choice 大奖。该奖项对 Byte Buddy 的“Java 技术方面的巨大创新 ”表示赞赏。

Javassist代理方式

public class JavassistProxy extends ClassLoader {

    public static <T> T getProxy(Class clazz) throws Exception {

        ClassPool pool = ClassPool.getDefault();
        // 获取类
        CtClass ctClass = pool.get(clazz.getName());
        // 获取方法
        CtMethod ctMethod = ctClass.getDeclaredMethod("say");
        // 方法前加强
        ctMethod.insertBefore("{System.out.println(\"" + ctMethod.getName() + " 被 Javassist 代理\");}");

        byte[] bytes = ctClass.toBytecode();

        return (T) new JavassistProxy().defineClass(clazz.getName(), bytes, 0, bytes.length).newInstance();
    }

    public static void main(String[] args) throws Exception {
        Subject subject = JavassistProxy.getProxy(SubjectImpl.class);
        subject.say("world");
    }
}

Javassist 是一个使用非常广的字节码插装框架,几乎一大部分非入侵的全链路监控都是会选择使用这个框架。因为它不想 ASM 那样操作字节码导致风险,同时它的功能也非常齐全。另外,这个框架即可使用它所提供的方式直接编写插装代码,也可以使用字节码指令进行控制生成代码,所以综合来看也是一个非常不错的字节码框架。