info java module_检测并修复 Java 程序框架的非一致性

63970003ab5564d751d0751f7be4c2b7.png

引用

Ghorbani N, Garcia J, Malek S. Detection and repair of architectural inconsistencies in Java[C]//2019 IEEE/ACM 41st International Conference on Software Engineering (ICSE). IEEE, 2019: 560-571.

摘要

Java 是当下使用最广泛的编程语言之一。由于 Java 语言本身不提供对体系结构(例如软件组件)的明确支持,因此软件开发人员无法享受到基于体系结构的开发所带来的诸多好处。为了解决上述问题,Java 9 引入了 Java 平台模块系统(Java Platform Module System,JPMS),将丰富的软件架构接口补充到主流编程语言中,造就了第一个模块封装实例。JPMS 的主要目标是帮助开发团队有效地构建和维护大型应用程序,同时改善 Java 应用程序和 JDK 本身的封装性、安全性和可维护性。但是这项技术存在一个问题:模块声明并不一定能够反映应用程序中模块的实际使用情况,JPMS 的声明规范允许编程人员在模块间错误地施加不一致依赖关系。本文对 Java 9 应用程序中可能出现的 8 种不一致模块化依赖关系进行了正式定义,同时还引入了依赖不一致性检测框架 DARCY。该框架能够利用不一致的定义和静态程序分析技术来自动:① 检测 Java 应用程序中声明的不一致的依赖关系;② 修复模块实现与软件架构存在的依赖不一致问题。本文利用 38 个开源 Java 9 应用程序作为实验数据集,并对对 DARCY 进行了实验评估。实验结果表明:体系结构不一致问题在 Java 9 应用程序中普遍存在,并且 DARCY 在软件架构不一致性自动检测以及自动修复方面具有其独特的优势。

关键词:JPMS;模块一致性;软件架构

1 引言

软件系统的架构由构建系统时采用的主要设计决策组成。尽管每个系统都有自己的架构,但是大部分架构设计都没有被显示地记录下来。如何确保说明性架构(设计系统预期功能的架构)与描述性架构(反映系统实现的架构)能够互相匹配依然是架构设计领域的一项重大挑战。系统的架构通常是对软件系统的观念化表达,内容包括软件构件、软件连接器以及接口等,属于程序的高等级设计;而具体的编程语言则提供诸如类、方法和变量等元素,属于低等级描述。因此,实现软件架构与软件实现的一一对应是一项复杂的工作。

说明性架构与描述性架构之间的不一致性一直是软件项目的致命问题。减轻体系结构不一致现象的一种可行方法是简化构建抽象架构与具体实现之间映射的过程。为此,软件工程研究社区此前曾大力倡导基于架构的开发:通过编程语言(例如 ArchJava)或框架(例如 C2)提供用于实现抽象架构的所需的具体实现。

尽管学术界有着大量的前驱工作,但直到最近,Java(可以说是过去二十年来最受欢迎的编程语言)仍缺乏对“基于体系结构的开发”的广泛支持。随着 Java 9 中 Java 平台模块系统(JPMS)的引入,这一切都改变了。模块系统旨在帮助开发人员更轻松地构造大型应用程序,同时改善 Java 应用程序和 JDK 本身的封装性、安全性和可维护性。

在使用 JPMS 时,开发人员需要在名为 module-info 的文件中,显示地指定系统的组件(即 Java 中的模块)、以及这些组件依赖项的特定属性。但是,Java 9 没有提供任何机制来确保 module-info 文件中指定的说明性架构能够真正与软件实现对应的说明性架构保持一致,即:无法保证 module-info 文件声明的依赖项能够准确反映系统组件之间真实的依赖关系。JPMS 使用 module-info 文件来确定每个模块的访问级别、规定哪些模块应该打包在一起以进行部署。如果软件系统的说明性架构与描述性架构存在不一致,那么就有可能导致严重的安全和性能问题;此外,这些不一致现象还会影响工程师了解系统、进而做出进一步决策的过程。

在本文中,作者团队定义了 Java 9 应用程序中可能出现的 8 种模块化不一致现象;同时,介绍了不一致检测工具 DARCY。DARCY 能够利用上述不一致定义,通过静态分析技术来自动 ① 检测 Java 应用程序中存在的架构不一致问题,并 ② 修复这些不一致问题。

2 Java 平台模块系统

2.1 JPMS 的目标和潜在的误用

JPMS 允许用户对系统的说明性框架进行设计,开发人员可以自定义包括软件构件、软件接口在内的多种关键元素。JPMS 旨在优化 Java 开发工具包(JDK)和 Java 运行时环境(JRE),使其具有更可靠的配置、更强大的封装能力和模块化能力,以解决软件工程师在开发和部署 Java 应用程序时遇到的一些问题。

在使用 Java 9 设计软件系统时,设计人员和开发人员可以通过显示规定模块间接口和依赖的方式实现系统的高度封装。Java 9 中,系统的封装性主要通过各个模块的访问权限实现:一个模块必须对“自己包含的哪些公共类型可以被其他模块访问”做出显式规定;如果没有这种显式规定,一个模块将无法访问到其他模块的公有类型。由此,JPMS 改进了 Java 语言的可访问性控制功能。这一特性将允许架构师和开发人员根据需求自主降低某些包的访问权限,从而减少 Java 应用程序中存在的安全隐患。

尽管 JPMS 允许开发人员自定义说明性架构的规范,但是 Java 应用程序的描述性架构依然可能与说明性架构不一致。这种“不一致”可能是由架构师对软件系统架构的误解、或是开发人员进行了错误的实现所导致的。这种“误用”可能会导致 ① 架构封装性不完善,应用程序难以理解和维护;③ 臃肿的软件;③ 不安全的软件。

2.2 理解 Java 9 模块系统中的模块

在 JPMS 中,模块是一组唯一命名的、可重用的相关软件包。模块包含了模块依赖的资源(例如图像和 XML 文件)。每个模块都有一个描述文件 module-info.java,其中包含了元数据以及命名模块的声明。命名模块应指定:① 本模块对其他模块的依赖,即该模块需要或期望的类和接口;② 本模块中暴露给其他模块的内容。模块分为**普通模块****开放模块**两类:普通模块允许其他模块在编译时和运行时访问其显式开放(export)的包和类;开放模块则允许其他模块 ① 在编译时访问其显式开放的包和类;② 在运行时访问其包含的所有包。模块声明文件由唯一的模块名称和模块主体组成。任何模块主体都可以为空,也可以包含一个或多个模块指令。这些指令指定了模块对其他模块的暴露等级,同时也声明了本模块对其他模块的访问权限。图 1 展示了一个包含三个模块(bar,foo 和 service)的项目;图 2 则展示了这些模块间的依赖关系。

0e61a3776a0fafd7bfb1c47ed7159ee0.png

图 1 模块声明及相关描述

a9e55b407c58d6036a546e062a6dd74f.png

图 2 基于模块描述形成的模块间依赖

3 不一致的模块间依赖

用户在使用模块定义时可能会出现描述不一致的情况,Java 平台可以检测并反馈这些问题,但并不能处理模块依赖中存在的**过度依赖**(Excess Dependencies)问题。在这种依赖关系中,某个模块可能 ① 公开了过多的内部组件,或者 ②require 了一些其使用不到的其他模块的内部组件。过度依赖衍生的不一致性会影响架构的各种属性,具体属性有:

(1)封装性与可维护性(A1):当一个模块对一些本不需要的、其他模块的功能提出需求声明时,会不必要地增加一个模块的复杂性,在降低模块可维护性的同时损害了它的封装性;

(2)软件体量和可拓展性(A2):当一个模块对一些本不需要的、其他模块的功能提出需求声明时,会导致应用软件变得臃肿,损害其可拓展性;

(3)安全性(A3):过度暴露某个模块的内部组件可能导致会导致模块出现错误、引发一些安全问题。

表 1 包含 11 个函数,这些函数是针对 JPMS 中的五个模块指令的建模。例如:对于各个软件包涉及到的、实际使用到的代码,本文定义了 Dep 函数进行处理。

表 1 基于 JPMS 的模块指令描述依赖关系的若干函数

47137e1a54bd65ca4122a86acf096a76.png

通过利用表 1 中的函数,作者引入了八种不同类型的、属于过度依赖的架构不一致性问题。不一致问题的文字描述和公式描述如下:

(1)需求依赖不一致:当 ① 模块 m1 显式声明它需要另一个模块 m2,且 ②m1 的任何类实际上都没有使用 m2 开放包中的任何类时,就产生了需求(requires)不一致问题。这种不一致主要影响属性 A1,有时也会影响属性 A2。

890e5cc07be4c1e0888e7a1ddbae2cac.png

(2)JDK 需求依赖不一致:当模块 m1 显式声明了它需要 Java JDK 提供的一个内部模块 mjdk;但实际上,m1 内的任何类都没有使用 mjdk 开放包中的任何类时,就产生了 JDK 需求(requires)依赖不一致问题。这种不一致会影响 A1 和 A2。该场景应与前一个场景区分开来,原因是:涉及 JDK 模块的需求依赖不一致问题对软件可移植性的影响更加显著

56ba7cc50540961e8c6802158dc54054.png

(3)传递性需求依赖不一致:需求依赖关系中的过量传递修饰符由以下条件构成:① 模块 m1 在其 module-info 文件中显示声明其“传递地需要”(transitively require)另一个模块 m2;且 ② 一个需要 m1 的模块的任意类在实际中都没有使用到 m2 中的任何类。这种类型的不一致问题主要影响属性 A1,同时也影响 A2。

e84d0bfed63a58f535e1d47727eaa410.png

(4)导出/导出至依赖不一致:对于 exports 指令,当模块 m1 显示地将包 p1 导出到所有其他模块、而其他模块中并没有包使用 p1 时,就发生了导出依赖不一致问题;

4debe2770c9aeba9a843116328d476ad.png

对于 export to 指令,当 m1 将包 p1 导出到模块 M 的声明列表、而 m1 之外或模块列表 M 内的任何类都不使用 p1 内的任何类时,会发生导出至依赖不一致问题。

e4816bd921b15f24850b25560c06362a.png

这些不一致问题主要影响属性 A3。当软件系统的架构比较复杂时还会影响属性 A1。

(5)提供依赖不一致:提供(provide with)依赖不一致问题有两个关键的部分:① 模块 m 显示声明它提供的服务 s,表现为一个抽象类或接口。s 在实现过程中会被 m 中的一组具体类 E = {c1, c2, … ck}继承或实现;②m 以外的其他模块内部的类都不通过 java.util.ServiceLoader 提供 API 使用服务 s。这种不一致问题会影响属性 A1 和 A2,特殊情况下也有影响到 A3。

850351bfcc2613fafd3ce46ee211fe31.png

(6)使用依赖不一致:当 ① 模块 m 在其 module-info.java 文件中显式声明其使用了服务 s,且 ②m 内部的任何类都不会通过 java.util.ServiceLoader 提供的 API 使用服务 s 时,就会发生使用(uses)依赖不一致问题。使用依赖不一致问题将影响属性 A1 和 A2。

905f79c90ac49b0ae25844d03a6f5c4b.png

(7)开放修饰符不一致:当 ① 模块 m 声明将其所有的程序包都开放给所有其他模块,且 ② 在 m 内部至少有一个包装 p,使得 m 外部没有类可以通过反射方式进入时,就会产生开放(open)修饰符不一致问题。这种不一致问题将影响属性 A3,同时会使体系结构不准确且更加复杂,从而影响属性 A1。

32f69c865a2d26e6c01f76f0809faac8.png

(8)开放/开放至不一致:当模块 m 声明它将通过反射机制向所有其他模块开放一个包 p 时,而实际上 m 之外的任何类都不会通过反射机制访问包 p 中的任何类时,开放(opens)依赖不一致问题就会发生;

1395e79bb7df3eb3ee4ea40e6bb0ac9d.png

相似地,对于“打开至”(opens to)命令,to 修饰符将会指定一个模块列表 M。模块 m 将向 M 开放(opens)包 p,以供其通过反射机制访问,而实际上 m 中没有任何包会以反射的方式访问 p。

0dfb08a6d4f103ecbd92602f44e72523.png

这两种不一致问题会影响 A1 和 A3

4 DARCY

DARCY 的设计基于本文定义的八种不一致问题,在实现时采用了 Java、Python 两种语言。DARCY 总体架构如图 3 所示。

8fe4e5fe19c3a48c69a811e029889daf.png

图 3 DARCY 总体架构概览

4.1 检测阶段

检测阶段以一个 Java 应用程序为输入,将检测该应用程序中是否存在上一节中提到的八种不一致问题。为了识别 Java 应用程序中真实存在的依赖关系,DARCY 选择了一种静态分析技术——类依赖分析技术。在 DARCY 实现过程中,作者团队应用工具 Classycle 完成类依赖分析(Class Dependency Analysis)。通过运行 Classcyle,Java 应用源码中真实存在的依赖信息将会被搜集。Classcyle 将从包、类两个层级上搜集依赖信息,并最终给出一份完整的报告。

一个 Java 应用程序可能包含多个模块,每个模块都有一个 module-info 文件,用于描述模块间的依赖关系。为了提取说明性架构,作者团队开发了模块信息扫描器。扫描器检查输入的 Java 应用程序中包含的所有 module-info.java 文件,并提取其中指定的、所有包级别的依赖信息。收集的依赖信息将存储在另一个数据库组件“指定依赖项”(Specified Dependencies)中。

作者团队利用 Soot 框架进行自定义静态分析,用于识别输入的应用程序中存在的反射机制、实现 Java 反射分析(Java Reflection Analysis)。应用程序中所有实际存在的反射用法都将存储在“实际依赖关系”(Actual Dependencies)中。Java 反射分析将提取所有的、作为反射调用中目标方法的非常量字符串、程序输入。

4.2 修复阶段

为了修复不一致的依赖关系,模块信息变换器(Module-info Transformer)将对 module-info 文件中显式定义的依赖关系进行删除或修改。

对于应用程序中的每个模块,DARCY 都会找到在该模块中定义的、不一致依赖信息的相关记录,并修改 module-info 中受影响的部分。为了达成上述目的,作者团队利用 ANTLR 转换 module-info.java 文件,以修复其中声明不一致依赖。ANTLR 是一个解析器生成器,能够生成可以读取、处理、执行或翻译结构化的文本的解析器。作者团队根据 Java 9 语法设计并实现了自定义的解析器,用于检查检测阶段获取到的不一致依赖信息。

在某些情况下,用户可能会与 DARCY 产生分歧,例如:当用户创建了一个工具包,同时开放了其中的部分类以支撑后续的一些工作。这个时候,“分歧”往往就会产生。为了解决这种问题,作者团队赋予了使用者更高的优先级。当 Java 应用程序中出现不一致问题时,DARCY 将会警告开发人员和架构师:这类不一致问题可能会对系统产生威胁;同时,DARCY 允许用户按照自己的意愿对 DARCY 提供的修复结果进行覆写。

致谢

本文由南京大学软件学院 2020 级硕士生钱瑞祥翻译转述。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值