有哪些方法可以在运行时动态生成一个Java类?

之前的文章中,提到了动态代理,先举一个比较常见的应用场景,在JPA中仅仅是实现了Jps两个接口,就自动生成了类,帮助我们完成Sql基本操作,并且层层封装,从配置、驱动、到SQL语句的写入都帮我们完成,换句话说,动态的生成了一个方法完成了操作,这也就是动态代理操作,但是文章中并没有很仔细的进行说明,因此在本文中将进行深入的分析。

概述

我们从原始的java类来进行源分析,通常是我们编写完程序,调用javac编译成class文件,通过类加载机制载入jvm,然后就可以运行了。

但是我们也可以不使用这个办法,也可以用官方的java.compiler去编译,因为只要是符合 jvm字节码标准的都可以拿来编译,而java.compiler库下的方法拥有和javac对等的编译方式,进一步思考,我们已经知道了jvm编译可以识别的字节码,那么,自然也可以用之前提到的cglib asm去转化实现。

面试角度来说,主要有以下几点:

  • 字节码和类加载到底是怎么实现无缝转换的?发生在整个类加载过程的哪一步?
  • 如何利用字节码操作技术、实现基本动态代理
  • 除了动态代理,字节码操作还有哪些应用场景

扩展

首先了解一下类从字节码到Class对象的转换:

@Deprecated
    protected final Class<?> defineClass(byte[] b, int off, int len)
        throws ClassFormatError
    {
        return defineClass(null, b, off, len, null);
    }


    protected final Class<?> defineClass(String name, byte[] b, int off, int len)
        throws ClassFormatError
    {
        return defineClass(name, b, off, len, null);
    }	

可以看出,只要能生出规范的字节码,不管是作为byte数组形式还是放到ByteBuffer里,都可以平滑的完成字节码到Java对象的转换过程

JDK提出的defineClass方法,最终都是本地代码实现的

    private native Class<?> defineClass1(String name, byte[] b, int off, int len,
                                         ProtectionDomain pd, String source);

    private native Class<?> defineClass2(String name, java.nio.ByteBuffer b,
                                         int off, int len, ProtectionDomain pd,
                                         String source);

更进一步,我们来看看JDK dynamic proxy的实现代码。你会发现,对应逻辑是实现在ProxyBuilder这个静态内部类中,ProxyGenerator生成字节码,并以byte数组的形式保 存,然后通过调用Unsafe提供的defneClass入口。

byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                proxyName, interfaces, accessFlags);
    try {
        return defineClass0(loader, proxyName,
         proxyClassFile, 0, proxyClassFile.length);
    } catch (ClassFormatError e) {
                
         throw new IllegalArgumentException(e.toString());
    }

从以上的代码我们知道了字节码->Class对象的转换过程,但是还不知道如何生成自己需要的字节码,因此接下来看看字节码的操作逻辑:

JDK内部动态代理的逻辑,可以参考java.lang.refect.ProxyGenerator的内部实现。可以认为这是种另类的字节码操纵技术,其利用了DataOutputStrem提供的能力,配合hard-coded的各种JVM指令实现方法,生成所需的字节码数组,可以参考下面代码:

private void codeLocalLoadStore(int var1, int var2, int var3, DataOutputStream var4) throws IOException {
        assert var1 >= 0 && var1 <= 65535;

        if (var1 <= 3) {
            var4.writeByte(var3 + var1);
        } else if (var1 <= 255) {
            var4.writeByte(var2);
            var4.writeByte(var1 & 255);
        } else {
            var4.writeByte(196);
            var4.writeByte(var2);
            var4.writeShort(var1 & '\uffff');
        }

    }

我们可以看出,这种代码实现起来没有什么依赖关系,简单实用但是又需要知道一些JVM命令,知道如何处理偏移地址。因此不适合普通的开发场景

知道了字节码相关操作逻辑,可以简单思考一下,如果需要实现一个简单的动态代理,如何使用字节码技术来实现走通这个流程

对于一个普通的Java动态代理,实现过程可以简化为:

  • 提供一个基础的接口(可以参照动态代理那篇专栏举的例子),根据接口对应的实现类,使用getClassLoader,利用Proxy的newProxyInstance方法实现调用接口即能直接实现最终的java实体类方法(JPA就是此原理)

可以看下面方法签名具体来了解:

public satic Object newProxyInsance(
ClassLoader loader,  
Class<?>[] interfaces,  
InvocationHandler h
)

字节码的操作技术,除了使用在动态代理,还可以使用在以下:

  • Mock
  • ORM
  • IOC
  • Profiler工具
  • 生成形式化代码

甚至还可以认为,字节码操纵技术是工具和基础框架必不可少的部分,大大减少了开发者的负担。

Java运行时动态生成class的方法一般有两种:使用Java字节码操作库ASM或者使用Java自带的反射API。 使用ASM: ASM是一个Java字节码操作库,它可以在不加载的情况下操作字节码。以下是使用ASM生成一个简单的的示例: ``` ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "MyClass", null, "java/lang/Object", null); MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "myMethod", "()V", null, null); mv.visitCode(); mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn("Hello, world!"); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv.visitInsn(Opcodes.RETURN); mv.visitMaxs(2, 1); mv.visitEnd(); byte[] code = cw.toByteArray(); ClassLoader classLoader = new ClassLoader() { public Class<?> defineClass(String name, byte[] code) { return defineClass(name, code, 0, code.length); } }; Class<?> clazz = classLoader.defineClass("MyClass", code); Object obj = clazz.newInstance(); Method method = clazz.getDeclaredMethod("myMethod"); method.invoke(obj); ``` 使用反射API: Java自带的反射API可以在运行时获取的信息并操作的属性和方法。以下是使用反射API动态添加方法的示例: ``` public static void addMethod(Object obj, String methodName, Class<?>[] parameterTypes, Class<?> returnType, String methodBody) throws Exception { Method method = Method.class.getDeclaredMethod("getDeclaredFields"); method.setAccessible(true); Field[] fields = (Field[]) method.invoke(null); Method[] newMethods = new Method[fields.length + 1]; for (int i = 0; i < fields.length; i++) { newMethods[i] = (Method) fields[i].get(null); } CtClass cc = ClassPool.getDefault().get(obj.getClass().getName()); CtMethod cm = CtNewMethod.make("public " + returnType.getName() + " " + methodName + "(" + Arrays.stream(parameterTypes).map(Class::getName).collect(Collectors.joining(",")) + ") {" + methodBody + "}", cc); cc.addMethod(cm); Class<?> newClass = cc.toClass(); newMethods[fields.length] = newClass.getDeclaredMethod(methodName, parameterTypes); Field methodField = Method.class.getDeclaredField("declaredMethods"); methodField.setAccessible(true); methodField.set(null, newMethods); } ``` 这个方法在目标对象上添加一个方法。在这个示例,我们使用了Javassist库,它是一个字节码操作库,它可以生成和修改Java字节码。我们可以使用它在运行时动态创建和修改
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值