在 ArchGuard 中是根据消费者-生产者这一基本的模式,来对 API 进行建模的。 生产者在代码者以 resource 命名,消费者则是 Demand。对于一个前端应用来说,它是一个 demand,包含了一系列 API 调用。对于一个后端应用来说,它可能即是 resource,又或者是 demand,即它即会提供 API,又要消费 API。

API 分析主要是在 Scanner(https://github.com/archguard/archguard)的 analyser_sourcecode/feat_apicalls 模块中,主要代码文件:

  • CSharpApiAnalyser
  • JavaApiAnalyser。支持 Java/Kotlin
  • FrontendApiAnalyser。支持 TypeScript/JavaScript

从名称中就能知道它们的用途了。

Resource:从一个 Spring 的 Controller 说起

对于一个 Java/Kotlin 的 Web 应用来说,它的 HTTP 声明如下:

@RestController
@RequestMapping("/api/systems/{systemId}/methods")
class MethodController(val methodService: MethodService) {

    @GetMapping("/callees")
    fun getMethodCallees(@PathVariable("systemId") systemId: Long,
                         @RequestParam("name") methodName: String,
                         @RequestParam(value = "clazz") clazzName: String,
                         @RequestParam(value = "deep", required = false, defaultValue = "3") deep: Int,
                         @RequestParam(value = "needIncludeImpl", required = false, defaultValue = "true") needIncludeImpl: Boolean,
                         @RequestParam(value = "module") moduleName: String): ResponseEntity<List<JMethod>> {
        val jMethod = methodService.findMethodCallees(systemId, moduleName, clazzName, methodName, deep, needIncludeImpl)
        return ResponseEntity.ok(jMethod)
    }
}

Spring 采用的是注解的方式来声明 API 的,所以只需要分析注解,我们就能分析出他们的 API。只需要将 RequestMappingGetMapping 的值拼到一起,就是一个 resource。如下代码所示:

fun analysisByNode(node: CodeDataStruct, _workspace: String) {
    val routeAnnotation = node.filterAnnotations("RestController", "RequestMapping")
    if (routeAnnotation.isNotEmpty()) {

    }
} 

从使用上来说,Resource 的分析并没有什么难度,随便找个工具都是能使用的。

Demand:多种多样的消费端

Demand:消费一个 API

API 的消费就是一个比较复杂的工程,需要更规范的 API 模式。如下是一个使用 REST Template 调用 GET 请求的示例:

@Component
class QualityGateClientImpl(@Value("\${client.host}") val baseUrl: String) : QualityGateClient {
    override fun getQualityGate(qualityGateName: String): CouplingQualityGate {
        val couplingQualityGate = RestTemplate().getForObject("$baseUrl/api/quality-gate-profile/$qualityGateName", CouplingQualityGate::class.java)
        return couplingQualityGate ?: CouplingQualityGate(null, qualityGateName, emptyList(), null, null)
    }
}

对于分析来说,代码里的模式也比较简单,在 AST 在查看匹配 RestTemplate().getForObject的情况,即调用的类是 RestTemplate,方法是 get(getForObject )开头则说明是一个 Get 请求:

if (call.NodeName == "RestTemplate" && call.FunctionName != "<init>") {
    var method = ""
    val lowercase = functionName.lowercase()
    when {
        lowercase.startsWith("get") -> {
            method = "Get"
        }
        lowercase.startsWith("post") -> {
            method = "Post"
        }
        lowercase.startsWith("delete") -> {
            method = "Delete"
        }
        lowercase.startsWith("put") -> {
            method = "Put"
        }
    }
   ...
}

唯一麻烦的地方是 "$baseUrl/api/quality-gate-profile/$qualityGateName" 的形式,对于这样的形式,我们只需要做一些特殊的规则处理就行了。但是,如果不是类似于这样的方式,那么我们就需要修改代码中的逻辑了。

Demand:消费一个 API(Axios)

对于前端来说,这种方式也是类似的:

export function querySystemInfo() {
  return axios<SystemInfo[]>({
    url: "/api/system-info",
    method: "GET",
  });
}

同样的,也是解析 URL,还有对应的 method,进而生成一个消费端的内容

拉线:匹配消费者-生产者

对于匹配 URL 来说,充满了各种不确定性,或者说是不准确性。每个 URL 都是千差万别的,需要通过一些规则来进行特殊处理,即将参数转换为 @uri@ 的形式,方便我们进行匹配。

诸如于:$baseUrl/api/quality-gate-profile/$qualityGateName -> @uri@/api/quality-gate-profile/@uri@

如此一来,才能将生产者与消费者进行匹配。