java程序在mySQL中编译_深入理解Java的动态编译

前提

笔者很久之前就有个想法:参考现有的主流ORM框架的设计,造一个ORM轮子,在基本不改变使用体验的前提下把框架依赖的大量的反射设计去掉,这些反射API构筑的组件使用动态编译加载的实例去替代,从而可以得到接近于直接使用原生JDBC的性能。于是带着这样的想法,深入学习Java的动态编译。编写本文的时候使用的是JDK11。

基本原理

下面这个很眼熟的图来源于《深入理解Java虚拟机》前端编译与优化的章节,主要描述编译的过程:

5c5a387ee74831d305a2c233bd4bfc9f.png

上图看起来只有三步,其实每一步都有大量的步骤,下图尝试相对详细地描述具体的步骤(图比较大难以分割,直接放原图):

72d46fe88006c00bdb3d14615a88a09b.png

实际上,仅仅对于编译这个过程来说,开发者或者使用者不必要完全掌握其中的细节,JDK提供了一个工具包javax.tools让使用者可以用简易的API进行编译(其实在大多数请下,开发者是面向业务功能开发,像编译和打包这些细节一般直接由开发工具、Maven、Gradle等工具完成):

119067b05d333635d20f3a3a78db43b8.png

具体的使用过程包括:

获取一个javax.tools.JavaCompiler实例。基于Java文件对象初始化一个编译任务javax.tools.JavaCompiler$CompilationTask实例。CompilationTask实例执行结果代表着编译过程的成功与否。我们熟知的javac编译器其实就是JavaCompiler接口的实现,在JDK11中,对应的实现类为com.sun.tools.javac.api.JavacTool。在JDK8中不存在JavaCompiler接口,具体的编译入口类为com.sun.tools.javac.main.JavaCompiler。

因为JVM里面的Class是基于ClassLoader隔离的,所以编译成功之后可以通过自定义的类加载器加载对应的类实例,然后就可以应用反射API进行实例化和后续的调用。

JDK动态编译

JDK动态编译的步骤在上一节已经清楚地说明,这里造一个简单的场景。假设存在一个接口如下:

faf46cc833fe9617b84f3b2e4ae9c2d5.png

我们可以通过字符串SOURCE_CODE定义一个类:

79d8355d7548bf09318bedfa0b9945f3.png

在组装编译任务实例之前,还有几项工作需要完成:

内置的JavaFileObject标准实现SimpleJavaFileObject是面向类源码文件,由于动态编译时候输入的是类源码文件的内容字符串,需要自行实现。内置的JavaFileManager是面向类路径下的Java源码文件进行加载,这里也需要自行实现JavaFileManager。需要自定义一个ClassLoader实例去加载编译出来的动态类。实现JavaFileObject

自行实现一个JavaFileObject,其实可以简单点直接继承SimpleJavaFileObject,覆盖需要用到的方法即可:

0cc7b476b57d0614f57c684004268548.png

1b19fb1a076669d839e07428045a9f72.png

如果编译成功之后,直接通过自行添加的CharSequenceJavaFileObject#getByteCode()方法即可获取目标类编译后的字节码对应的字节数组(二进制内容)。这里的CharSequenceJavaFileObject预留了多个构造函数用于兼容原有的编译方式。

实现ClassLoader

只要简单继承ClassLoader即可,关键是要覆盖原来的ClassLoader#findClass()方法,用于搜索自定义的JavaFileObject实例,从而提取对应的字节码字节数组进行装载,为了实现这一点可以添加一个哈希表作为缓存,键-值分别是全类名的别名(xx.yy.MyClass形式,而非URI模式)和目标类对应的JavaFileObject实例。

2025ce6eab1a554ba771e98705ef3509.png

7037bfc84f6286e115237b485a9da41e.png

实现JavaFileManager

JavaFileManager是Java文件的抽象管理器,它用于管理常规的Java文件,但是不局限于文件,也可以管理其他来源的Java类文件数据。下面就通过实现一个自定义的JavaFileManager用于管理字符串类型的源代码。为了简单起见,可以直接继承已经存在的ForwardingJavaFileManager:

109b8a2e45a5a52ea1b20a8c5d8bfca0.png

5483c9b5698a99ca5ad31fe13363840a.png

d33f563ac39626d41f9203ae1d45c2d1.png

注意在这个类中引入了自定义类加载器JdkDynamicCompileClassLoader,目的是为了实现JavaFileObject实例的共享以及为文件管理器提供类加载器实例。

动态编译和运行

前置准备工作完成,我们可以通过JavaCompiler去编译这个前面提到的字符串,为了字节码的兼容性更好,编译的时候可以指定稍低的JDK版本例如1.6:

e50f8765aed860c32073d54d0b88b2b0.png

41b2cca892729e3d0a9173ea85e9dfc8.png

输出结果如下:

编译[club.throwable.compile.JdkDynamicCompileHelloService]结果:truethrowable say hello [by jdk dynamic compile]

可见通过了字符串的类源码,实现了动态编译、类加载、反射实例化以及最终的方法调用。另外,编译过程的诊断信息可以通过DiagnosticCollector实例获取。为了复用,这里可以把JDK动态编译的过程抽取到一个方法中:

d4b0be31600d628687b9b7529146985f.png

b0b8e64bc9549c8d8e5036b41cefafe7.png

Javassist动态编译

既然有JDK的动态编译,为什么还存在Javassist这样的字节码增强工具?撇开性能或者效率层面,JDK动态编译存在比较大的局限性,比较明显的一点就是无法完成字节码插桩,换言之就是无法基于原有的类和方法进行修饰或者增强,但是Javassist可以做到。再者,Javassist提供的API和JDK反射的API十分相近,如果反射平时用得比较熟练,Javassist的上手也就变得比较简单。这里仅仅列举一个增强前面提到的DefaultHelloService的例子,先引入依赖:

1dbacfada34ec93cdb0725262727753f.png

编码如下:

549cbb40380b263791d3474446aa1919.png

输出结果如下:

f8fbe67f6f59606ef9feeb3ec81aa460.png

Javaassist这个单词其实是Java和Assist两个单词拼接在一起,意为Java助手,是一个Java字节码增强类库:

可以基于已经存在的类进行字节码增强,例如修改已经存在的方法、变量,甚至是直接在原有的类中添加新的方法等。可以完全像积木拼接一样,动态拼出一个全新的类。不像ASM(ASM的学习曲线比较陡峭,属于相对底层的字节码操作类库,当然从性能上来看ASM对字节码增强的效率远高于其他高层次封装的框架)那样需要对字节码编程十分了解,Javaassist降低了字节码增强功能的入门难度。

进阶例子

现在定义一个接口MysqlInfoMapper,用于动态执行一条已知的SQL,很简单,就是查询MySQL的系统表mysql里面的用户信息SELECT Host,User FROM mysql.user:

99375057ddce65ba4164fe0e23b0be7c.png

假设现在只提供一个MySQL的驱动包(mysql:mysql-connector-java:jar:8.0.20),暂时不能依赖任何高层次的框架,要动态实现MysqlInfoMapper接口,优先整理需要的组件:

需要一个连接管理器去管理MySQL的连接。需要一个SQL执行器用于执行查询SQL。需要一个结果处理器去提取和转换查询结果。为了简单起见,笔者在定义这三个组件接口的时候顺便在接口中通过单例进行实现(部分配置完全写死):

7de6ea9a47834de318eaffbd68d8fcf4.png

3737675b29fd1cb39f02a731bb2405f5.png

接着需要动态编译MysqlInfoMapper的实现类,它的源文件的字符串内容如下(注意不要在类路径下新建这个DefaultMysqlInfoMapper类):

fc419e784c50387a88afac063c3aafed.png

然后编写一个客户端进行动态编译和执行:

56e303e7f0e401e7faea669df0a2aa41.png

abfa16057204e5705b5a9cdfd83c5e1e.png

最终的输出结果是:

编译[club.throwable.compile.DefaultMysqlInfoMapper]结果:true[{"host":"%","user":"canal"},{"host":"%","user":"doge"},{"host":"localhost","user":"mysql.infoschema"},{"host":"localhost","user":"mysql.session"},{"host":"localhost","user":"mysql.sys"},{"host":"localhost","user":"root"}]

然后笔者查看本地安装的MySQL中的结果,验证该查询结果是正确的。

4f5129aabff37adc1bc154491ed53c85.png

这里笔者为了简化整个例子,没有在MysqlInfoMapper#selectAllMysqlUsers()方法中添加查询参数,可以尝试一下查询的SQL是SELECT Host,User FROM mysql.user WHERE User = 'xxx'场景下的编码实现。

如果把动态实现的DefaultMysqlInfoMapper注册到IOC容器中,就可以实现MysqlInfoMapper按照类型自动装配。如果把SQL和参数处理可以抽离到单独的文件中,并且实现一个对应的文件解析器,那么就可以把类文件和SQL隔离,Mybatis和Hibernate都是这样做的。

小结

动态编译或者更底层的面向字节码层面的编程,其实是一个十分有挑战性但是可以创造无限可能的领域,本文只是简单分析了一下Java源码编译的过程,并且通过一些简单的例子进行动态编译的模拟,离使用于实际应用中还有不少距离,后面需要花更多的时间去分析一下相关领域的知识。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值