Google 了一下,强大的 JavaScript 生态里竟然找不到一个纯 JS 实现的 Java 解析器,所有的我能找到的解析器都是通过启动一个 Java 进程来完成解析,然后再对输出结果进行后续处理。大概是因为 Java 的语法实在是复杂吧🤣。
为了实现一个在线查看 Jar 包内容的功能,我希望在浏览器中解析 Jar 包,并分析依赖关系,所以需要使用纯 JS 实现对 Java 代码的解析。
目标
- 快:在 web 的世界里,我们的一帧最多只有 33ms 的时间,所以第一个要求就是快。
- 准:至少要保证类型和依赖关系的解析的准确性,对于其他细节,其实也不用特别精确。
思路
-
基于 PEG 的解析,这是最靠谱最正统的解析方式,比如 Babel 对 JavaScript 的解析就是基于此。
但是我并不能列举出 Java 所有的语法细节,所以也无法写出一个靠谱的 PEG 解析器。
而且 PEG 在这种场景下有两个弊端:
- 过于严谨,因而慢;
- 容错能力差,一处错误可能导致后面所有的语法都不能识别。
-
逐字解析,参考 highlight.js 的实现方式。
这种方式也需要对 Java 语法有足够的了解,实现起来也有一定的难度。
我的第一个版本就是用了这种方式,最后发现总有一些边缘情况考虑不周,导致识别失败。
而实际上,我对解析的要求没有那么高,一些特殊语法原样展示就可以了,不用完全识别出来。
-
正则一把梭。
这其实是一种取巧的方法,模拟一个不那么了解 Java 的人的角度,来从 Java 代码中获取关键信息。
跟逐字解析的方式不太一样的地方在于,我们从大到小的提取信息,先用正则提取大块的内容,再逐渐细分,直到识别不出来为止。这样可以最大程度地保留关键信息,而且有很强的容错能力。
所以,最后我选择了正则一把梭。
实现
-
为了防止误匹配,第一步要把注释提取出去,因为注释里可能包含代码,干扰识别。
所以我直接正则匹配所有的
/* ... */
,替换成占位符,这样就可以确保后面的匹配不会命中注释中的内容,提高准确率。 - 正则提取所有的
import
,存下来到最后用于解析所有的依赖关系。 -
正则识别每个文件导出的类型,一般都是这种形式:
public interface XxxInterface { boolean sayHi(); }
-
识别完导出类型之后,根据当前类型下的典型的语法提取出关键信息,比如
interface
的方法名,class
的属性名。这里要用一个比较通用的形式,避免处理细节,比如
interface
中提取出任何的type methodName(params)
,然后在后面的步骤中再做细分。 -
解析数据类型,从大到小。
举个例子:对于一个复杂的类型
A<B<C>>
,我们先提取最大的部分,得到泛型A
和具体类型B<C>
,然后再进一步解析B<C>
。 - 最后把所有解析出来的信息汇总,所有的类都存到一个池子里,然后将所有用到的数据类型补全,即完成了一次粗糙的在线解析。
不足
这毕竟是一个取巧的方式,对于复杂的 Java 代码还是无能为力的,不过处理仅仅用于定义接口的 Jar 包基本可以满足需求。
目前对于 Jar 包中存在复杂代码,或是嵌套定义的时候,还是可能会存在问题,以后有机会再继续改进。