工具时间:使用jQAssistant防止API泄漏

发布者:    |       讨论

如果你看过伟大的节目《家居改进》,你就会知道一个“工具狂”仍然是个傻瓜。然而,同时,正确的工具以正确的方式使用可以非常有效地解决复杂问题。

在这篇文章中,我想介绍一个名为jQAssistant的工具,我发现它非常有助于对项目的代码库进行各种分析,例如防止库的公共API中内部类型的泄漏。这是关于我们在开发Hibernate家族的不同库时,所重视的以开发者为中心的工具的博客系列的第一篇文章。

保持API的清洁

当提供库时,一个既定的最佳实践是明确区分代码库中打算供用户访问的部分(库的API)和那些不打算对外部访问的部分。

有一个明确定义的API有助于减少用户的复杂性(他们只需要学习和理解API类,而不需要了解所有实现细节),同时给库作者提供自由,根据需要重构和更改实现类。通常,通过使用特定的包名来实现API和实现类型的分离,所有非公共部分都位于名为internalimpl或类似的包下。

但有一点你必须非常小心,不要在API中泄漏任何内部类型。例如,以下方法定义是应该避免的

package com.example;

import com.example.internal.Foo;

public interface MyPublicService {

    Foo doFoo();
}

MyPublicService是公共API的一部分(因为它不属于internal包),但doFoo()方法返回内部类型Foo。因此,使用doFoo()的用户必须处理实现类型,这正是你在将库的API和实现部分分开时想要防止的事情。

不幸的是,如果不小心,这种不一致的API定义起来很容易。这就是工具的用途所在:通过自动搜索这种格式错误的API,可以在早期发现并避免它们。

介绍jQAssistant

jQAssistant 是一个开源工具,允许您执行上述操作。它解析项目的代码库,并创建一个包含所有类、方法、字段等模型的 Neo4j 图数据库。使用 Neo4j 强大的 Cypher 查询语言,您可以执行查询以检测代码库中感兴趣的具体模式和结构。

为您的项目设置jQAssistant相当简单。假设您正在使用 Maven,您只需要在您的 pom.xml 中添加以下插件配置(有关更多详细信息配置选项,请参阅官方文档

pom.xml
...
<build>
    <plugins>
        <plugin>
            <groupId>com.buschmais.jqassistant.scm</groupId>
            <artifactId>jqassistant-maven-plugin</artifactId>
            <version>1.1.4</version>
            <executions>
                <execution>
                    <goals>
                        <goal>scan</goal>
                        <goal>analyze</goal>
                    </goals>
                    <configuration>
                        <failOnViolations>true</failOnViolations>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>
...

插件设置后,我们可以定义一个Cypher查询,查找任何具有内部返回类型的API方法

jqassistant/rules.xml
<?xml version="1.0" encoding="UTF-8"?>
<jqa:jqassistant-rules xmlns:jqa="http://www.buschmais.com/jqassistant/core/analysis/rules/schema/v1.0">

    <constraint id="my-rules:PublicMethodsMayNotExposeInternalTypes">
        <description>API/SPI methods must not expose internal types.</description>
        <cypher><![CDATA[
            MATCH
                (class)-[:`DECLARES`]->(method)-[:`RETURNS`]->(returntype)
            WHERE
                NOT class.fqn =~ ".*\\.internal\\..*"
                AND (method.visibility="public" OR method.visibility="protected")
                AND returntype.fqn =~ ".*\\.internal\\..*"
            RETURN
                method
        ]]></cypher>
    </constraint>

    <group id="default">
        <includeConstraint refId="my-rules:PublicMethodsMayNotExposeInternalTypes" />
    </group>

</jqa:jqassistant-rules>

jQAssistant规则默认保存在名为 jqassistant/rules.xml 的文件中。

规则本身使用Cypher查询语言定义。如果您之前没有使用过Cypher,一开始可能会感觉有点不常见,但根据我的经验,您可以很快适应。基本上,您描述图节点的模式、它们的属性、它们的类型(通过“标签”表示,类似于标签)以及它们之间的关系。

在上面的查询中,我们使用一个包含三个节点(“class”、“method”和“returntype”)和两个关系的模式。必须从“class”节点到“method”节点存在类型为“DECLARES”的关系,从“method”到“returntype”节点存在类型为“RETURNS”的关系。

由于我们只对通过API泄露内部类型的函数感兴趣,因此使用 WHERE 子句进一步细化选择

  • 声明类必须是API的一部分(它不能位于internal包中,通过完全限定类名进行过滤),

  • 方法必须有公共或受保护的可见性,

  • 返回类型必须位于internal包中。

要执行此规则,只需使用上面配置的jQAssistant Maven插件构建项目。插件将自动执行规则文件中给出的默认组的所有规则。如果任何执行规则有结果,则构建将失败,并显示受影响规则的(一些)结果

[INFO] --- jqassistant-maven-plugin:1.1.4:analyze (default) @ jqassistant-demo ---
...
[ERROR] --[ Constraint Violation ]-----------------------------------------
[ERROR] Constraint: my-rules:PublicMethodsMayNotExposeInternalTypes
[ERROR] Severity: INFO
[ERROR] API/SPI methods must not expose internal types.
[ERROR]   method=com.example.MyPublicService#com.example.internal.Foo doFoo()
[ERROR] -------------------------------------------------------------------
...
[INFO] BUILD FAILURE

API方法应仅返回API类型,但它们也应仅接受API类型作为参数。让我们扩展Cypher查询以涵盖这种情况

jqassistant/rules.xml
...
<constraint id="my-rules:PublicMethodsMayNotExposeInternalTypes">
    <description>API/SPI methods must not expose internal types.</description>
    <cypher><![CDATA[
      // return values
      MATCH
          (class)-[:`DECLARES`]->(method)-[:`RETURNS`]->(returntype)
      WHERE
          NOT class.fqn =~ ".*\\.internal\\..*"
          AND (method.visibility="public" OR method.visibility="protected")
          AND returntype.fqn =~ ".*\\.internal\\..*"
      RETURN
          method

      // parameters
      UNION ALL
      MATCH
          (class)-[:`DECLARES`]->(method)-[:`HAS`]->(parameter)-[:`OF_TYPE`]->(parametertype)
      WHERE
          NOT class.fqn =~ ".*\\.internal\\..*"
          AND (method.visibility="public" OR method.visibility="protected")
          AND parametertype.fqn =~ ".*\\.internal\\..*"
      RETURN
          method
    ]]></cypher>
</constraint>
...

类似于SQL,我们可以使用 UNION ALL 子句添加更多结果。搜索泄露的参数与返回值搜索的方式类似,唯一的区别是我们需要检测的节点模式:必须有一个节点(类)具有到另一个节点(方法)的DECLARES关系,该节点具有到第三个节点(参数)的HAS关系,该节点最终具有到表示参数类型的第四个节点的OF_TYPE关系。包名和方法可见性的规则与返回值检查相同。

浏览您的项目的模型

当声明如上所示的规则时,了解您项目图形表示的元模型至关重要,例如有哪些节点类型(即它们有哪些标签),有哪些关系类型,节点有哪些属性等等。这在jQAssistant的参考文档中有详细的描述。

但是,由于 jQAssistant 基于Neo4j,您也可以使用数据库自带的浏览器应用来交互式地探索您项目的结构。为此,只需运行以下命令

mvn jqassistant:scan jqassistant:server

这将把您项目的结构填充到 jQAssistant 内置的 Neo4j 数据库中,并启动一个 Neo4j 服务器。在浏览器中,访问 https://127.0.0.1:7474/browser/,您就可以探索项目代码库,运行 Cypher 查询等。开始时,可以在左侧选择节点标签或关系类型,或者提交一个 Cypher 查询

Browsing a jQAssistant model in Neo4j, align=

自己尝试

您可以在 GitHub 上找到使用 jQAssistant 在 Maven 项目中的完整示例 这里。您还可以查看 Hibernate Validator 使用的 规则文件。除了检查暴露内部类型的方法之外,此文件还定义了一些其他规则

  • API 类型中的公共字段不得暴露实现类型

  • API 类型不得扩展实现类型

这些检查在我们的 CI 服务器上定期运行,可以非常有效地防止意外引入泄漏的 API。

在图数据库中创建软件项目的模型是一个好主意。功能强大的 Cypher 查询语言允许您以相当直观的方式在项目中搜索有趣的结构和模式。检测泄漏的 API 只是其中一个例子。例如,您可能定义了业务应用的分层架构,并确保应用层之间没有非法依赖。

此外,jQAssistant 不限于 Java 类。除了 Java 插件,该工具还提供了许多其他扫描器,例如用于 Maven POM 文件JPA 持久化单元XML 文件 的扫描器,允许您运行针对项目特定需求的所有类型的分析。


返回顶部