你好,我是何辉。首先祝你新年快乐。
今天是我们深入研究Dubbo源码的第五篇,Compiler 编译。
在“[点点直连]”中,我们实现了一套万能管控平台来修复数据,其中就有通过市场上的 Groovy 插件编译 Java 源代码来生成类信息。
而上一讲“[Wrapper 机制]”中,在实现自定义代理的时候,我们也使用了 JavaCompiler 来编译源代码,只不过编译的时候,借助了磁盘上的 class 文件才得以生成类信息。
掌握了这两种动态编译方式,相信你在动态编译这块已经有了一定的基础,如果你还是觉得有点胆怯,今天我们上点难度,再学习 2 种 Compiler 方式,帮助你在底层框架开发层面拥有更强大的技术支撑。
Javassist 编译 还是以上一讲的自定义代理为例。这张图你应该还有印象,我们尝试通过添加一层代理的方式,把各种 if…else 的硬编码逻辑转变为动态生成:
在实现自定义代理的过程中,我们采用的是最纯粹的字符串拼接的方式,拼接出了动态的源代码,虽然实用,但是写起来也费劲。
有没有通过 set 或 get 操作就能实现创建类的简单方式,来改造图中的代理实现过程呢?
我们在脑内检索一番,平常都是直接将编写好的代码交给 Javac 编译器去编译的,现在要通过某种工具简单的进行 set 或 get 动态创建一个类,怎么办呢,突然灵光一闪,在上一讲“Wrapper 机制的原理”代码流程中,我们看到了一段 makeClass 的样例代码,难道 Dubbo 已经有了类似的先进操作么?
不管真假,我们先去验证看看,找到了相应的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 public Class<?> toClass(Class<?> neighborClass, ClassLoader loader, ProtectionDomain pd) { if (mCtc != null ) { mCtc.detach(); } long id = CLASS_NAME_COUNTER.getAndIncrement(); try { CtClass ctcs = mSuperClass == null ? null : mPool.get(mSuperClass); if (mClassName == null ) { mClassName = (mSuperClass == null || javassist.Modifier.isPublic(ctcs.getModifiers()) ? ClassGenerator.class.getName() : mSuperClass + "$sc" ) + id; } mCtc = mPool.makeClass(mClassName); if (mSuperClass != null ) { mCtc.setSuperclass(ctcs); } mCtc.addInterface(mPool.get(DC.class.getName())); if (mInterfaces != null ) { for (String cl : mInterfaces) { mCtc.addInterface(mPool.get(cl)); } } if (mFields != null ) { for (String code : mFields) { mCtc.addField(CtField.make(code, mCtc)); } } if (mMethods != null ) { for (String code : mMethods) { if (code.charAt(0 ) == ':' ) { mCtc.addMethod(CtNewMethod.copy(getCtMethod(mCopyMethods.get(code.substring(1 ))), code.substring(1 , code.indexOf('(' )), mCtc, null )); } else { mCtc.addMethod(CtNewMethod.make(code, mCtc)); } } } if (mDefaultConstructor) { mCtc.addConstructor(CtNewConstructor.defaultConstructor(mCtc)); } if (mConstructors != null ) { for (String code : mConstructors) { if (code.charAt(0 ) == ':' ) { mCtc.addConstructor(CtNewConstructor .copy(getCtConstructor(mCopyConstructors.get(code.substring(1 ))), mCtc, null )); } else { String[] sn = mCtc.getSimpleName().split("\\$+" ); mCtc.addConstructor( CtNewConstructor.make(code.replaceFirst(SIMPLE_NAME_TAG, sn[sn.length - 1 ]), mCtc)); } } } try { return mPool.toClass(mCtc, neighborClass, loader, pd); } catch (Throwable t) { if (!(t instanceof CannotCompileException)) { return mPool.toClass(mCtc, loader, pd); } throw t; } } catch (RuntimeException e) { throw e; } catch (NotFoundException | CannotCompileException e) { throw new RuntimeException (e.getMessage(), e); } }
凭着印象从代码中找到了一些特别有趣的 API,比如 setSuperclass 设置父类属性、addInterface 添加实现类属性、addField 添加字段、addMethod 添加方法、addConstructor 添加构造方法等等。刚刚还在寻思,有没有一种简单的赋值操作的方式来创建类,结果就发现了意外收获,从名字上看,感觉这些 API 都和类创建有关,好像都能用上。
我们继续深挖,发现这些 API 都是属于 Javassist 插件中的 ,这就难怪了,既然该插件能提供这样的 API 来创建类,如果不将这些类最终编译成为 Class 类信息,未免也有点做事做半截的感觉。
不过这只是我们从源码层面的推测,到底有没有这个效果呢,还是要确认一下。至于如何确认,我给个小小的建议,如果你在源码中发现一些比较感兴趣的插件,可以去官方网站大致了解一下,也许疑惑和推测也就烟消云散了。
我们进入 Javassist 的官网 会看到这段英文解释:
Javassist (Java Programming Assistant) makes Java bytecode manipulation simple. It is a class library for editing bytecodes in Java; it enables Java programs to define a new class at runtime and to modify a class file when the JVM loads it. Unlike other similar bytecode editors, Javassist provides two levels of API: source level and bytecode level. If the users use the source-level API, they can edit a class file without knowledge of the specifications of the Java bytecode. The whole API is designed with only the vocabulary of the Java language. You can even specify inserted bytecode in the form of source text;Javassist compiles it on the fly . On the other hand, the bytecode-level API allows the users to directly edit a class file as other editors.
大致含义就是,Javassist 让用 Java 编辑字节码变为一件非常简单的事情,众多开发人员可以在不懂字节码规范的情况下,针对字节码文件进行编辑修改,改完之后 Javassist 可以实时编译它。
这下验证了我们刚才对源码的猜想。是不是很兴奋又接触到了新技术,别着急,我们还是先看官网关于 API 介绍的教程 。
基本了解如何使用之后,上一讲的代码模板,我们可以用 Javassist 实现一遍,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 public class JavassistProxyUtils { private static final AtomicInteger INC = new AtomicInteger (); public static Object newProxyInstance (Object sourceTarget) throws Exception{ ClassPool pool = ClassPool.getDefault(); Class<?> targetClazz = sourceTarget.getClass().getInterfaces()[0 ]; String proxyClassName = "$" + targetClazz.getSimpleName() + "CustomInvoker_" + INC.incrementAndGet(); CtClass ctClass = pool.makeClass(proxyClassName); ctClass.setSuperclass(pool.get("com.hmilyylimh.cloud.compiler.custom.CustomInvoker" )); CtClass returnType = pool.get("java.lang.Object" ); CtMethod newMethod=new CtMethod ( returnType, "invokeMethod" , new CtClass []{ returnType, pool.get("java.lang.String" ), pool.get("java.lang.Class[]" ), pool.get("java.lang.Object[]" ) }, ctClass); newMethod.setModifiers(Modifier.PUBLIC); newMethod.setBody(buildBody(targetClazz).toString()); ctClass.addMethod(newMethod); Class<?> clazz = ctClass.toClass(); String filePath = JavassistProxyUtils.class.getResource("/" ).getPath() + JavassistProxyUtils.class.getPackage().toString().substring("package " .length()).replaceAll("\\." , "/" ); ctClass.writeFile(filePath); return clazz.newInstance(); } private static StringBuilder buildBody (Class<?> targetClazz) { StringBuilder sb = new StringBuilder ("{\n" ); for (Method method : targetClazz.getDeclaredMethods()) { String methodName = method.getName(); Class<?>[] parameterTypes = method.getParameterTypes(); String ifHead = "if (\"" + methodName + "\".equals($2)) {\n" ; String ifContent = null ; if (parameterTypes.length != 0 ){ ifContent = "return ((" + targetClazz.getName() + ") $1)." + methodName + "(" + String.class.getName() + ".valueOf($4[0]));\n" ; } else { ifContent = "return ((" + targetClazz.getName() + ") $1)." + methodName + "();\n" ; } String ifTail = "}\n" ; sb.append(ifHead).append(ifContent).append(ifTail); } String invokeMethodTailContent = "throw new " + org.apache.dubbo.common.bytecode.NoSuchMethodException.class.getName() + "(\"Method [\" + $2 + \"] not found.\");\n}\n" ; sb.append(invokeMethodTailContent); return sb; } }
可以发现确实比拼接字符串简单多了,而且 API 使用起来也比较清晰明了,完全按照平常的专业术语命名规范,马上就能找到对应的 API,根本不需要花很多准备工作。
改造代码需要注意 3 点。
在获取各种类对应的 CtClass 类型对象时,可以通过从 ClassPool 的 get 方法中传入类路径得到。 在对方法的入参字段名进行逻辑处理时,就得替换成 $ 占位符 ,方法中的 this 引用,用 $0 表示,方法中的第一个参数用 $1 表示,第二个参数用 $2 表示,以此类推。 若要重写的父类的方法,是否设置 @Override 属性不太重要,但是千万别为了重写而拿父类的 CtMethod 属性一顿乱改。 用新方案编译源代码后,我们验证一下结果,编写测试验证代码。
1 2 3 4 5 6 7 8 public static void main (String[] args) throws Exception { DemoFacadeImpl demoFacade = new DemoFacadeImpl (); CustomInvoker invoker = (CustomInvoker) JavassistProxyUtils.newProxyInstance(demoFacade); invoker.invokeMethod(demoFacade, "sayHello" , new Class []{String.class}, new Object []{"Geek" }); }
如预期所料,正常打印出了结果,没想到一句简短的 CtClass.toClass() 方法就帮我们快速编译代码并转成 Class 对象信息了,非常简单实用。
ASM 编译 现在你有没有觉得信心大增,原来操作字节码这么简单,Javassist 简直是神器,有了它的存在,我想你再去理解那些 Java Agent 技术知识点,会发现在编译的世界修改字节码已经是小菜一碟了。
不过做技术我们讲究精益求精,既然 Javassist 这么好用,为什么公司的大佬还在用 ASM 进行操作呢?
其实,ASM 是一款侧重于性能的字节码插件,属于一种轻量级的高性能字节码插件,但同时实现的难度系数也会变大。这么讲你也许会好奇了,能有多难?
我们还是举例来看,例子是把敏感字段加密存储到数据库。
1 2 3 4 5 6 7 8 public class UserBean { private String name; public UserBean (String name) { this .name = name; } public String getName () { return name; } public void setName (String name) { this .name = name; } @Override public String toString () { return "UserBean{name='" + name + '\'' + '}' ; } }
上层业务有一个对象,创建对象后,需要给对象的 setName 方法进行赋值。
如果想要给传入的 name 字段进行加密,一般我们会这么做。
1 2 3 4 5 6 UserBean userBean = new UserBean ();userBean.setName(AESUtils.encrypt("Geek" )); userDao.insertData(userBean);
把传入 setName 的值先进行加密处理,然后把加密后的值放到 userBean 对象中,在入库时,就能把密文写到数据库了。
但是这样就显得很累赘,今天这个字段需要加密,明天那个字段需要加密,那就没完没了,于是有人就想到了,可以将加密的这段操作内嵌到代理对象中,比如这样:
在上层业务中,该怎么赋值还是继续怎么赋值,不用感知加密的操作,所有加密的逻辑全部内嵌到代理对象中。当然,如果这么做,就得设计一个代码模板,借助自定义代理的经验,想必你也有了设计思路:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class UserBeanHandler implements Handler <UserBean> { @Override public void addBefore (UserBean u) { if (u.getName() != null && u.getName().length() > 0 ) { u.setName("#BEFORE#" + u.getName()); } } } public interface Handler <T> { public void addBefore (T t) ; }
代码模板的思路也很简单,主要注意 2 点。
设计一个对象的代理类,暴露一个 addBefore 方法来将字段进行加密操作。 代理类为了迎合具备一个 addBefore 方法,就得设计出一个接口,避免 Java 类单继承无法扩展的瓶颈。 代码模板是定义好了,可是操作字节码的时候,去哪里弄到该 UserBeanHandler 的字节码呢?
其实 IDEA 工具已经为你预留了一个查看字节码的入口。
选中代码模板后,展开顶部的 View 菜单,选中 Show Bytecode 看到该类对应的字节码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 public class com /hmilyylimh/cloud/compiler/asm/UserBeanHandler extends Ljava /lang/Object; implements com /hmilyylimh/cloud/compiler/asm/Handler { public <init>()V ALOAD 0 INVOKESPECIAL java/lang/Object.<init> ()V RETURN MAXSTACK = 1 MAXLOCALS = 1 public addBefore (Lcom/hmilyylimh/cloud/compiler/asm/UserBean;) V ALOAD 1 INVOKEVIRTUAL com/hmilyylimh/cloud/compiler/asm/UserBean.getName ()Ljava/lang/String; IFNULL L0 ALOAD 1 INVOKEVIRTUAL com/hmilyylimh/cloud/compiler/asm/UserBean.getName ()Ljava/lang/String; INVOKEVIRTUAL java/lang/String.length ()I IFLE L0 ALOAD 1 NEW java/lang/StringBuilder DUP INVOKESPECIAL java/lang/StringBuilder.<init> ()V LDC "#BEFORE#" INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; ALOAD 1 INVOKEVIRTUAL com/hmilyylimh/cloud/compiler/asm/UserBean.getName ()Ljava/lang/String; INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; INVOKEVIRTUAL com/hmilyylimh/cloud/compiler/asm/UserBean.setName (Ljava/lang/String;)V L0 FRAME SAME RETURN MAXSTACK = 3 MAXLOCALS = 2 public synthetic bridge addBefore (Ljava/lang/Object;) V ALOAD 0 ALOAD 1 CHECKCAST com/hmilyylimh/cloud/compiler/asm/UserBean INVOKEVIRTUAL com/hmilyylimh/cloud/compiler/asm/UserBeanHandler.addBefore (Lcom/hmilyylimh/cloud/compiler/asm/UserBean;)V RETURN MAXSTACK = 2 MAXLOCALS = 2 }
看到一大片密密麻麻的字节码指令,想必你已经头都大了,不过别慌,这个问题在 ASM 的官网指引 中也解答了,我们只需要按部就班把字节码指令翻译成为 Java 代码就可以了。
好吧,既然官网都这么贴心了,那就勉强当一回工具人,我们按照官网的指示,依葫芦画瓢把代码模板翻译出来。
经过一番漫长的翻译之后,我们终于写出了自己看看都觉得头皮发麻的长篇大论的代码,关键位置我都加注释了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 public class AsmProxyUtils implements Opcodes { public static Object newProxyInstance (Class originClass) throws Exception{ String newClzNameSuffix = "Handler" ; byte [] classBytes = generateByteCode(originClass, newClzNameSuffix); String filePath = AsmProxyUtils.class.getResource("/" ).getPath() + AsmProxyUtils.class.getPackage().toString().substring("package " .length()).replaceAll("\\." , "/" ); FileOutputStream fileOutputStream = new FileOutputStream (new File (filePath, originClass.getSimpleName() + newClzNameSuffix + ".class" )); fileOutputStream.write(classBytes); fileOutputStream.close(); ClassLoader loader = Thread.currentThread().getContextClassLoader(); Class<?> loaderClass = Class.forName("java.lang.ClassLoader" ); Method defineClassMethod = loaderClass.getDeclaredMethod("defineClass" , String.class, byte [].class, int .class, int .class); defineClassMethod.setAccessible(true ); Object respObject = defineClassMethod.invoke(loader, new Object []{ originClass.getName() + newClzNameSuffix, classBytes, 0 , classBytes.length }); return ((Class)respObject).newInstance(); } private static byte [] generateByteCode(Class originClass, String newClzNameSuffix) { String newClassSimpleNameAndSuffix = originClass.getSimpleName() + newClzNameSuffix + ".java" ; ClassWriter classWriter = new ClassWriter (0 ); classWriter.visit( V1_6, ACC_PUBLIC + ACC_SUPER, Type.getInternalName(originClass) + newClzNameSuffix, Type.getDescriptor(Object.class)+Type.getDescriptor(Handler.class).replace(";" ,"" )+"<" +Type.getDescriptor(originClass)+">;" , Type.getDescriptor(Object.class), new String []{ Type.getInternalName(Handler.class) } ); classWriter.visitSource(newClassSimpleNameAndSuffix, null ); MethodVisitor initMethodVisitor = classWriter.visitMethod( ACC_PUBLIC, "<init>" , "()V" , null , null ); initMethodVisitor.visitCode(); initMethodVisitor.visitVarInsn(ALOAD, 0 ); initMethodVisitor.visitMethodInsn(INVOKESPECIAL, Type.getInternalName(Object.class), "<init>" , "()V" ); initMethodVisitor.visitInsn(RETURN); initMethodVisitor.visitMaxs(1 , 1 ); initMethodVisitor.visitEnd(); MethodVisitor addBeforeMethodVisitor = classWriter.visitMethod( ACC_PUBLIC, "addBefore" , "(" + Type.getDescriptor(originClass) + ")V" , null , null ); addBeforeMethodVisitor.visitCode(); addBeforeMethodVisitor.visitVarInsn(ALOAD, 1 ); addBeforeMethodVisitor.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(originClass), "getName" , "()" + Type.getDescriptor(String.class)); Label L0 = new Label (); addBeforeMethodVisitor.visitJumpInsn(IFNULL, L0); addBeforeMethodVisitor.visitVarInsn(ALOAD, 1 ); addBeforeMethodVisitor.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(originClass), "getName" , "()" + Type.getDescriptor(String.class)); addBeforeMethodVisitor.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(String.class), "length" , "()I" ); addBeforeMethodVisitor.visitJumpInsn(IFLE, L0); addBeforeMethodVisitor.visitVarInsn(ALOAD, 1 ); addBeforeMethodVisitor.visitTypeInsn(NEW, Type.getInternalName(StringBuilder.class)); addBeforeMethodVisitor.visitInsn(DUP); addBeforeMethodVisitor.visitMethodInsn(INVOKESPECIAL, Type.getInternalName(StringBuilder.class), "<init>" , "()V" ); addBeforeMethodVisitor.visitLdcInsn("#BEFORE#" ); addBeforeMethodVisitor.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(StringBuilder.class), "append" , "(" + Type.getDescriptor(String.class) + ")" + Type.getDescriptor(StringBuilder.class)); addBeforeMethodVisitor.visitVarInsn(ALOAD, 1 ); addBeforeMethodVisitor.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(originClass), "getName" , "()" + Type.getDescriptor(String.class)); addBeforeMethodVisitor.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(StringBuilder.class), "append" , "(" + Type.getDescriptor(String.class) + ")" + Type.getDescriptor(StringBuilder.class)); addBeforeMethodVisitor.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(StringBuilder.class), "toString" , "()" + Type.getDescriptor(String.class)); addBeforeMethodVisitor.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(originClass), "setName" , "(" + Type.getDescriptor(String.class)+")V" ); addBeforeMethodVisitor.visitLabel(L0); addBeforeMethodVisitor.visitFrame(F_SAME, 0 , null , 0 , null ); addBeforeMethodVisitor.visitInsn(RETURN); addBeforeMethodVisitor.visitMaxs(3 , 2 ); addBeforeMethodVisitor.visitEnd(); MethodVisitor bridgeAddBeforeMethodVisitor = classWriter.visitMethod(ACC_PUBLIC + ACC_SYNTHETIC + ACC_BRIDGE, "addBefore" , "(" + Type.getDescriptor(Object.class) + ")V" , null , null ); bridgeAddBeforeMethodVisitor.visitCode(); bridgeAddBeforeMethodVisitor.visitVarInsn(ALOAD, 0 ); bridgeAddBeforeMethodVisitor.visitVarInsn(ALOAD, 1 ); bridgeAddBeforeMethodVisitor.visitTypeInsn(CHECKCAST, Type.getInternalName(originClass)); bridgeAddBeforeMethodVisitor.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(originClass) + newClzNameSuffix, "addBefore" , "(" + Type.getDescriptor(originClass) + ")V" ); bridgeAddBeforeMethodVisitor.visitInsn(RETURN); bridgeAddBeforeMethodVisitor.visitMaxs(2 , 2 ); bridgeAddBeforeMethodVisitor.visitEnd(); classWriter.visitEnd(); return classWriter.toByteArray(); } }
写的过程有些卡壳,难度系数也不低,我们有 3 个小点要注意。
有些字节码指令不知道如何使用 ASM API,比如 INVOKESPECIAL 不知道怎么调用 API,你可以网络检索一下“MethodVisitor INVOKESPECIAL ”关键字,就能轻松找到与之对应的 API 了。 重点关注调用 API 各参数的位置,千万别放错了,否则问题排查起来比较费时间。 生成的字节码文件直接保存到文件中,然后利用 ClassLoader.defineClass 方法,把字节码交给 JVM 虚拟机直接变成一个 Class 类型实例。 在写的时候,你一定要沉下心慢慢转换,一步都不能错,否则时间浪费了还得不到有效的成果。
写好之后,你一定非常兴奋,我们还是先写个测试代码验证一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 public static void main (String[] args) throws Exception { UserBean userBean = new UserBean ("Geek" ); Handler handler = (Handler) AsmProxyUtils.newProxyInstance(userBean.getClass()); handler.addBefore(userBean); System.out.println(userBean); }
打印输出的内容为:
1 打印一下加密内容: UserBean{name='#BEFORE#Geek' }
结果如预期所料,把入参的数据成功加密了,我们终于可以喘口气了,不过辛苦是值得的,学到了大量的底层 ASM 操控字节码的知识,也见识到了底层功能的强大威力。
Compiler 编译方式的适用场景 今天我们见识到 Javassist 和 ASM 的强大威力,之前也用过JavaCompiler和Groovy 插件,这么多款工具可以编译生成类信息,有哪些适用场景呢?
JavaCompiler:是 JDK 提供的一个工具包,我们熟知的 Javac 编译器其实就是 JavaCompiler 的实现,不过JDK 的版本迭代速度快,变化大,我们升级 JDK 的时候,本来在低版本 JDK 能正常编译的功能,跑到高版本就失效了。 Groovy:属于第三方插件,功能很多很强大,几乎是开发小白的首选框架,不需要考虑过多 API 和字节码指令,会构建源代码字符串,交给 Groovy 插件后就能拿到类信息,拿起来就可以直接使用,但同时也是比较重量级的插件。 Javassist:封装了各种API来创建类,相对于稍微偏底层的风格,可以动态针对已有类的字节码,调用相关的 API 直接增删改查,非常灵活,只要熟练使用 API 就可以达到很高的境界。 ASM:是一个通用的字节码操作的框架,属于非常底层的插件了,操作该插件的技术难度相当高,需要对字节码指令有一定的了解,但它体现出来的性能却是最高的,并且插件本身就是定位为一款轻量级的高性能字节码插件。 有了众多动态编译方式的法宝,从简单到复杂,从重量级到轻量级,你都学会了,相信再遇到一堆神乎其神的Compiler 编译方式,内心也不会胆怯了。
不过工具多了,有同学可能就有选择困难症,这里我也讲下个人的选择标准。
如果需要开发一些底层插件,我倾向使用 Javassist 或者 ASM。使用 Javassist 是因为用API 简单而且方便后人维护,使用 ASM 是在一些高度频繁调用的场景出于对性能的极致追求。如果开发应用系统的业务功能,对性能没有太强的追求,而且便于加载和卸载,我倾向使用 Groovy 这款插件。
总结 今天,我们接着上一讲刚学会的自定义代理案例,在不使用 Wrapper 已有机制的能力下,自己尝试使用简单的 API 操作来创建代理类,见识到了 Javassist 的强大之处,有一套非常齐全的 API 来创建代理类并实时编译成类信息。
使用 Javassist 编译的三大基本步骤。
首先,设计一个代码模板。 然后,使用 Javassist 的相关 API,通过 ClassPool.makeClass 得到一个操控类的 CtClass 对象,然后针对 CtClass 进行 addField 添加字段、addMethod 添加方法、addConstructor 添加构造方法等等。 最后,调用 CtClass.toClass 方法并编译得到一个类信息,有了类信息,就可以实例化对象处理业务逻辑了。 然后借助把敏感字段加密存储到数据库的案例,我们研究了大佬常用的 ASM 开发底层框架,尝试先将代码模板变成字节码指令,然后按照 ASM 的规范,将字节码指令一个个翻译成为 ASM 对应的方法,最终通过 ClassLoader.defineClass 将字节码变成了类信息。
也总结下使用 ASM 编译的四大基本步骤。
首先,还是设计一个代码模板。 其次,通过 IDEA 的协助得到代码模板的字节码指令内容。 然后,使用 Asm 的相关 API 依次将字节码指令翻译为 Asm 对应的语法,比如创建 ClassWriter 相当于创建了一个类,继续调用 ClassWriter.visitMethod 方法相当于创建了一个方法等等,对于生僻的字节码指令实在找不到对应的官方文档的话,可以通过“MethodVisitor + 字节码指令 ”来快速查找对应的 Asm API。 最后,调用 ClassWriter.toByteArray 得到字节码的字节数组,传递到 ClassLoader.defineClass 交给 JVM 虚拟机得出一个 Class 类信息。 总的来说,无论使用哪种方式进行动态编译,不管是出于对性能的极致追求,还是出于对项目具体业务功能的通用处理,只要适合自身业务,且不会带来沉重性能开销,都是一种好方式。只有合适的才是最好的。
思考题 留个作业给你,前面讲到 Javassist 动态编译时,其实里面也支持直接将编写的源代码编译成类信息,你可以试着从 Dubbo 的 org.apache.dubbo.common.compiler.Compiler 接口研究一番,看看能不能找到现成的方法来编译源代码?
期待看到你的思考,如果你对今天的内容还有什么困惑,欢迎在留言区提问,我会第一时间回复。我们下一讲见。
参考资料 考虑到 ASM 比较难,我之前也录制了ASM实操一步步教学视频 ,你可以学习。
15 思考题参考 上一期留了个作业,总结下 java.lang.Class#getDeclaredMethod 方法的调用流程,以及排查下存在哪些耗时和占用内存的地方。
既然要研究方法的调用流程,那最简单的方式就是直接去看代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 public Method getDeclaredMethod (String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException { checkMemberAccess(Member.DECLARED, Reflection.getCallerClass(), true ); Method method = searchMethods(privateGetDeclaredMethods(false ), name, parameterTypes); if (method == null ) { throw new NoSuchMethodException (getName() + "." + name + argumentTypesToString(parameterTypes)); } return method; } ↓ private Method[] privateGetDeclaredMethods(boolean publicOnly) { checkInitted(); Method[] res; ReflectionData<T> rd = reflectionData(); if (rd != null ) { res = publicOnly ? rd.declaredPublicMethods : rd.declaredMethods; if (res != null ) return res; } res = Reflection.filterMethods(this , getDeclaredMethods0(publicOnly)); if (rd != null ) { if (publicOnly) { rd.declaredPublicMethods = res; } else { rd.declaredMethods = res; } } return res; } ↓ private ReflectionData<T> reflectionData () { SoftReference<ReflectionData<T>> reflectionData = this .reflectionData; int classRedefinedCount = this .classRedefinedCount; ReflectionData<T> rd; if (useCaches && reflectionData != null && (rd = reflectionData.get()) != null && rd.redefinedCount == classRedefinedCount) { return rd; } return newReflectionData(reflectionData, classRedefinedCount); } ↓ private static class ReflectionData <T> { volatile Field[] declaredFields; volatile Field[] publicFields; volatile Method[] declaredMethods; volatile Method[] publicMethods; volatile Constructor<T>[] declaredConstructors; volatile Constructor<T>[] publicConstructors; volatile Field[] declaredPublicFields; volatile Method[] declaredPublicMethods; volatile Class<?>[] interfaces; final int redefinedCount; ReflectionData(int redefinedCount) { this .redefinedCount = redefinedCount; } } ↓ private static Method searchMethods (Method[] methods, String name, Class<?>[] parameterTypes) { Method res = null ; String internedName = name.intern(); for (int i = 0 ; i < methods.length; i++) { Method m = methods[i]; if (m.getName() == internedName && arrayContentsEq(parameterTypes, m.getParameterTypes()) && (res == null || res.getReturnType().isAssignableFrom(m.getReturnType()))) res = m; } return (res == null ? res : getReflectionFactory().copyMethod(res)); }
代码看下来,总结起来,获取方法的流程主要有三步。
第一步,检查方法权限。 第二步,从 Class 对象的所有方法列表中查找匹配的 Method 对象。 第三步,返回 Method 对象的克隆对象。 第二步每次都是 O(n) 时间复杂度的检索,并且缓存中的 ReflectionData 内容实在是太多了,占据了大量的内存开销。
第三步每次都是返回一个克隆对象,而 Method 对象本身就有很多东西,每次都拷贝出一个新对象,又是吃内存的操作。
一个简简单单的直接调用,换做反射后,即使不谈内存占用,就光凭代码量的行数,就相当于调用了很多字节码指令,虽然字节码指令执行很快,但是蚊子腿也是肉,时间上也有数十倍甚至百倍的耗时增长开销。
有时候,虽然 Java 反射性能会存在一定损耗开销,但并不是告诉我们不能使用 Java 反射,其实,我们应该根据程序设计来按需考量设计。
如果你只是根据某些条件或配置文件来使用 Java 反射创建对象,然后就做其它操作(如数据库查询等),那 Java 反射损耗的那点性能可以忽略不计。
但如果你在连续循环中通过反射不断创建大量对象,这样的操作就得好好斟酌了,看看是不是可以不用反射的机制实现,避免大量耗时和内存的开销。