在 Java 工程开发过程中,一般情况下,软件工程师以及项目管理人员都很清楚自己的工程项目都依赖于哪些外部组件接口,但是在某些情况,尤其是工程比较庞大时,一个工程分成多个组件由不同的项目组负责开发时,想要了解各个的工程依赖关系就变得有些困难。我们开发了一个简单易用工具(Java 工程的外部依赖显示工具),通过简单的配置就能清晰地显示 Java 工程的外部依赖关系。例如,一个项目都依赖于哪些接口,一个接口被哪些工程所引用以及引用的文件分别是什么,而且结果还可以生成网页用于发布以供其他相关人员参考。本文将介绍这个工具实现方式及使用方法。

背景

在测试中发现,我们的一个类错误地引用了外部接口,这个接口有不同的实现方案,应该如何确保调用了正确的接口?整个项目中是否还有其他类似错误的引用?为此我们开发了这个工具,能够清晰地列出接口引用关系,希望对其他碰到类似情况的开发人员有所帮助。

实现原理

分析源文件引用接口及 Jar 文件导出接口

分析源文件的引用接口可以直接通过逐行扫描源代码,读取导入的包,然后找出所有的符号,再进一步分析符号调用的 Jar 文件接口,或者可以通过 Java 编译器编译源代码,再从编译后的符号表中读取符号信息。第一种方法要自己分析源代,实现复杂写,但是运行效率要比 Java 编译器编译源代码高很多,因为 Java 编译器编译过程对源码所有的符号做了详细的分析,而我们只了解源码中引用了哪些外部接口。第二种方法虽然实现比较简单,可以使用开源的通用 Java 编译器 GJC(Generic Java Compiler),但是 GJC 本身还是比较复杂,需要了解 GJC 的接口,本章会在后面做介绍。当然并不局限这两种方法,例如,可以只使用 GJC 或者其他 Java 编译器提供的接口做源代码解析,不进行完整的编译,然后对解析后的符号表进行过滤只留下感兴趣的引用外部引用接口的符号,最后再分析接口关系。

利用扫描源代码的方法进行接口分析

清单 1. 读取 Jar 文件获取类名代码

Java.util.jar.JarFile jarFile =          new          Java.util.jar.JarFile(file);        
         Enumeration<JarEntry> entries = jarFile.entries();        
                  
         //读取 jar 文件把类名保存到 map 中        
         public          boolean          append(File file)          throws          IOException{          //jar file        
                  String path = file.getAbsolutePath();        
                  JarFile jarFile=         new          JarFile(file);        
                  Enumeration<JarEntry> entries = jarFile.entries();        
                  while          (entries.hasMoreElements()){        
                  JarEntry ent = entries.nextElement();        
                  String name = ent.getName();        
                  if         (name.endsWith(         ".class"         )){        
                  name = name.substring(         0         ,name.length()-         6         ).replace(         '/'         ,          '.'         );        
                  map.put(name, path);        
                  }        
                  }        
                  return          true         ;        
         }

Java 源文件中读取 import 关键字可以获取所有导入类或者静态常量及方法名,因为我们只关心导入的类名,如果导入静态常量或方法,可以认为导入了它们所引用的类名。另外外部依赖显示工具并不关心所有的导入类,例如 JRE 中的大部分类我们并不关心哪些是否导入了,而只关心某些特殊的类,所以这个步骤中可以把那些不关心的类过滤掉。

上一步骤中我们已经分析了导入类名,但是这并不够,导入类名在类定义中可能并没有使用到,或者类定义中使用类有可能是写了完整的包名,并不需要导入类名。因此这个步骤需要做两件事情,一是要分析哪些导入类是真正使用的,二是要分析源码中使用了哪些未申明导入的带完整包名的类。

利用 OpenJDK 的 Langtools 工具进行代码分析

环境安装:

Langtools 工具langtools.zip 下载:下载 V7 版本,如果下载 V8 版本,需要安装 JRE8

Langtools version7 需要 JRE 7 支持,JRE 7 可以从oracle 官网下载。

利用 Javac 编译器来实现导出接口分析,我们的目的不是要让它编译成 class 文件,而是要让它帮助我们分析外部接口引用关系,所以有必要对编译器做写修改,这可以通过两种方式进行,一是直接修改编译器,另外一种是通过接口继承方式修改接口功能,第一种方式比较简单,但是比较粗暴,继承方式显得比较优雅,因此我们采用后者来做说明。

修改 JavaCompiler 只调用 flow 接口,并把结果保存, 当然还要修改其他接口,这里只是列出修改的关键地方。

清单 2. 重载编译器代码

public          class          VmiJavaCompiler          extends          com.sun.tools.javac.main.JavaCompiler{        
                  public          boolean          saveTree =          false         ;        
                  public          Queue<Env<AttrContext>> envs=         null         ;        
                  private          void          compile2() {         //编译入口函数        
                  try          {        
                  if          (saveTree)        
                  envs=flow(attribute(todo));        
                  else        
                  flow(attribute(todo));        
                  }          catch          (Abort ex) {        
                  if          (devVerbose)        
                  ex.printStackTrace(System.err);        
                  }        
         }

编译器源码由遍历器 com.sun.tools.javac.TreeScanner 继承这个接口,便可遍历所有符号。

清单 3. 遍历符号代码

public          class          SymbolVisitor          extends          TreeScanner{        
                  SymbolVisitor(SymbolRecorder recorder){         //符号遍历        
                  this         .recorder = recorder;        
                  }        
                  SymbolRecorder recorder =          null         ;        
                  public          void          analyzeTree(JCTree tree) {        
                  if         (!(tree          instanceof          JCClassDecl))        
                  return         ;        
                  try          {        
                  scan (tree);        
                  }          finally          {        
                  // note that recursive invocations of this method fail hard        
                  if         (tree!=         null         ){        
                  recorder.DebugPrint ();        
                  }        
                  }        
                  }        
                  public          void          visitSelect(JCFieldAccess tree) {         //遍历 JCFieldAccess        
                  //Todo:add code here        
                  recorder.add(tree);        
                  super         .visitSelect (tree);        
                  }        
                  
                  public          void          visitIdent(JCIdent tree) {         //遍历 JCIdent        
                  //Todo:add code here        
                  recorder.add(tree);        
                  super         .visitIdent(tree);        
                  }        
         }

导出 xml 接口与引用关系文件

为了方便显示输出 html 树形表格,把数据结果导出为 xml 格式的文件。

表 1. 导出文件说明表

文件名

说明

jp.xml

Jar 文件与 Java 包关系

jrc.xml

Jar 文件导出了哪些接口,接口被哪些 Java 文件使用

pjr.xml

源文件的一个包使用了哪些 Jar 文件,每个 Jar 文件用的接口是什么

rj.xml

项目都使用了哪些接口,接口由哪个 Jar 文件提供

rp.xml

项目都使用了哪些接口,该接口被哪些 Java 包使用

注释:j-Jar 文件 p-Java 包 r-引用接口 c-Java 文件。

利用 web 树形控件显示 xml 文件中数据分析结果

树形控件下载地址:树形控件下载地址及说明文档。

这个控件只要简单修改 javascript,就能达到如下的显示效果,不再详细介绍。

图 1. tabletree4j 表格控件示例

使用配置

清单 4. 运行 java 包,命令

java –jar java-project-dependency.jar –f config.properties

java-project-dependency.jar 是外部依赖显示工具包

config.properties 是配置文件

表 2. 配置文件说明表

属性

说明

jarPathes

需要分析的 Jar 文件路径或文件名,可以多个用分号 (;) 分割

javaPathes

需要分析的 Java 源文件路径或文件名,可以多个用分号 (;) 分割

components

可以不设置,可以设置为导出包名

outputPath

输出文件保存路径

应用实例

样例工程介绍

为了简单起见,我们就用外部依赖显示工具来分析自身的依赖关系,这些代码可以从参考资料中的svn工程源码下载。

图 2. 工程目录结构图

如上图所示,该工程由 com.ibm.vmi.lsdep 和 com.ibm.vmi.lsdep.resource 两个 package 构成。我们要利用外部依赖显示工具来分析样例工程的外部依赖关系。这个工程依赖关系比较简单,我们把依赖于 rt.jar(属于 JRE 的 jar 文件)的接口也列出来,以便展示效果。

配置样例工程

首先,创建配置文件 config.properties 文件,文件内容如下:

清单 5. 配置样例文件内容

jarPathes=.\\lib;C:\\Program Files\\Java\\jre7\\lib\\rt.jar        
         javaPathes=.\\src        
         components=com.ibm.vmi.javac;com.ibm.vmi.lsdep        
         outputPath=.\\html

其次,从参考资料中下载外部依赖显示工具及 svn 源码。

最后,运行外部依赖显示工具,生成 html 格式报告。

清单 6. 运行命令及参数

java -jar java-project-dependency.jar -f config.properties

运行外部依赖显示工具命令后,html 文件夹中会生成结果,直接打开 html 文件夹中的 index.html 可以查看样例的外部依赖关系。

另外,还可以从参考资料中直接下载样例工程,解压文件,到样例文件夹中直接运行命令 run.bat 文件样例工程。

展示效果

由上图可以看出,com.ibm.vmi.javac 这个 Java 包使用了 Langtools.jar 和 rt.jar,还可以看到 Langtools.jar 中哪些接口被 Java 包使用。

 

图 4. 依赖关系-Jar 文件-Java 包

如上图所示,工程使用了 Langtools.jar 和 rt.jar 这两个 Jar 文件(当然还有其他 Jar 文件,根据样例工程配置,没有显示),其中 Langtools.jar 仅 com.ibm.vmi.javac 这个包使用,这是我们继承 javac 开发的新的编译器,com.ibm.vmi.lsdep 不直接引用 Langtools.jar。

 

如上图所示,工程使用了 com.sun.tools.javac.code.Flages 接口,这个接口是被 com.ibm.vmi.javac 所引用的。

总结

通过本文的讲述及样例配置,我们可以清楚地了解整个工程项目各个组件的依赖关系及分析接口间的调用关系,这不仅对项目管理有所帮助,对于软件工程师来说也是很有益处的。外部依赖显示工具采用树形表的形式来展示结果,方便查看外部依赖关系,同时,它也输出 xml 格式文件,可以提供给使用者做工程项目的进一步分析之用。

参考资料

学习
  • svn 工程源码:外部依赖显示工具工程源文件。
  • 外部依赖显示工具:编译后结果,可执行的 Jar 文件。
  • 样例:包含源文件、外部依赖显示工具、样例工程及结果。
  • langtools.zip:Langtools 工具集下载,包含 Java 编译器。
  • Javac 说明文档:Java 编译器说明文档。
  • developerWorks Java 技术专区:这里有数百篇关于 Java 编程各个方面的文章。
讨论
  • 加入 developerWorks 中文社区,查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。