OpenRewrite 学习笔记(一):基本知识与原理解析

OpenRewrite 学习笔记(一):基本知识与原理解析

最近的工作多与系统改造相关,涉及框架与平台的迁移。偶然接触到 OpenRewrite 这款有趣的工具,于是打算整理一些学习笔记,也是对学习过程的记录。后续还会持续探索并更新文章,只是目前尚不确定会写多少篇。


随着软件项目日趋复杂、版本迭代和新技术引进的加快,工程师往往需要对成千上万行的代码进行统一风格化处理、批量重构或版本迁移。然而手动完成这些工作不仅费时费力,而且容易遗漏。大规模场景下,整个过程像是一个大的工程实施,因此就有了 Migration Engineering。

Migration Engineering 迁移工程是一种系统化的工程实践,广泛应用于技术栈更新、代码现代化、架构优化和大规模变更管理。通过自动化工具和流程的结合,它能够在确保质量的同时减少人工介入,从而提高效率,降低风险,并确保代码库在技术演进中保持一致性。

  • From ChatGPT

为了实现精确可控且可重复的代码自动化重构(Automated Refactoring),出现了多种工具和框架。OpenRewrite 正是其中备受关注的一个,它能帮助开发者在大规模代码重构中以一种结构化、可持续和可扩展的方式对代码进行批量改写和重构,从而提升开发团队的生产力和代码质量。

接下来,和我一起从从基础认识入手,了解什么是 OpenRewrite 以及它所依据的基本原理。

什么是 OpenRewrite

OpenRewrite 是由 Moderne 公司开源的自动化重构框架。它的目标是通过一系列可组合的“配方”(官方术语 Recipes,可以理解为重构规则,为了更贴近官方文档的术语,将其翻译为配方。)在无需手动干预的情况下对代码进行有条理的结构重写。简言之,它不是简单的全局替换字符串工具,而是能基于 无损语义树(LST) 进行语义级别的代码修改。

应用场景

在深入原理之前,我们先来看几个常见的场景,这能帮助我们理解为什么要使用 OpenRewrite:

  • 统一代码风格和格式化规则: 当团队决定采用统一的代码风格(如使用特定的命名规范、去除不必要的注释、多余的空格或换行),可以通过 OpenRewrite 的规则对全仓库进行自动化的清理。
  • 批量升级依赖版本和框架迁移: 想象你维护着一个庞大的微服务体系结构,需要从 Spring Boot 2.x 升级到 3.x,这往往意味着在几十个甚至上百个仓库中同步修改数百个依赖项。OpenRewrite 可以通过一条 Recipe 来自动化完成这些变更。
  • 统一日志框架或安全库: 如果团队决定弃用某个日志库或安全框架,需要全仓库替换相关调用。手动修改非常繁琐,而 OpenRewrite 能确保精确定位和替换代码。
  • 遗留代码现代化: 对老旧项目的遗留代码进行现代化改造,例如将早期的 Java 语言特性(如匿名内部类)改为更现代的 Lambda 表达式,将旧的集合框架替换为流式处理 API,快速为老系统注入新特性和代码规范。

关键特性

  • 语法级与语义级分析:基于 LST 进行代码解析,确保代码修改的精准性。
  • 可扩展的配方(Recipes):提供大量内置和可扩展的规则集,用于自动化处理常见的代码问题。配方可与其他配方组合成更复杂的配方。
  • 语言与框架的广泛支持:编程语言如 Java、Kotlin、Groovy;数据格式:XML、YAML、Properties、JSON、ProtoBuf;构建工具 Maven、Gradle;框架:Quarkus、Spring、Micronaut、Jakarta。

如果说 Recipe 是 OpenRewrite 进行自动化重构的核心机制,那么 LST 就是是确保代码在重构时具备精确语义结构的基石。

基本原理概述

OpenRewrite 的核心思想是:通过对源代码进行解析和抽象(LST 建模),从而在结构化的层面上对代码进行精准、可控的修改

无损语义树 LST

与传统的 AST(Abstract Syntax Tree)相比,OpenRewrite 使用的是一种被称为 Lossless Semantic Tree (LST) 的数据结构。LST 的特点是无损地保留了原始源代码的所有细节信息,这不仅包括代码的语法和语义,还包含了空格、换行、注释、格式化风格等通常会在简单 AST 转换中丢失的内容。

配方 Recipe 与访问者 Visitors

OpenRewrite 的核心概念之一是 Recipe(配方)。Recipe 是一个或者一组行为逻辑的集合,它定义了如何对代码进行分析或修改。配方可以是非常简单的(如在类名中增加一个后缀)或非常复杂的(如将一套老旧框架的使用逻辑迁移到新框架中)。

在更底层的实现中,Recipe 中经常会用到 Visitor 模式(访问者模式)。Visitor 会遍历 LST 中的所有节点,对符合条件的节点进行相应改写。这个过程类似于为代码中的各个部分定制“检阅官”:当检阅官走到满足条件的代码元素时,就应用对应的规则进行修改。

LST 的生命周期

OpenRewrite 针对不同的编程语言和数据格式,提供了对应的解析器。解析器会将源文件作为静态文本进行解析,解析成 LST 数据结构。

当在本地运行 OpenRewrite 的配方时:

  1. OpenRewrite 会创建一个 LST 并将其存储在内存中。这个 LST 代表了磁盘上当前代码仓库的状态。
  2. 接下来,执行指定的配方对 LST 进行各种转换操作。
  3. 当配方执行完成后,LST 会被转换回纯文本形式,然后使用这些文本覆盖本地那些已被修改的文件。
  4. 在所有文件成功被覆写持久化后,整个过程结束。在不同的配方运行之间不会存留任何信息。
  5. 如果你在第一次配方运行结束后继续运行另外一个配方,那么在那个时候会重新生成一个新的 LST。
  6. 如果上一个配方对仓库作出了修改,这些修改已经以文本形式存在于本地文件中,那么新生成的 LST 将包含所有这些变更。如果上一个配方没有作出任何修改,那么新生成的 LST 将与之前的 LST 在内容上基本保持一致(不过是重新生成的,因为之前的 LST 已不复存在)。

快速上手

OpenRewrite 官方提供了大量拆箱即用的配方,也有 社区提供的大量配方,通过 Recipe Catalog 可以查看配方的使用说明。同时,这些配方也为我们编写自己的配方提供了参考。

OpenRewrite 提供了多种方式让我们不写任何代码就可以运行已有的配方。OpenRewrite 提供了 MavenGradle 的插件,同时在 IDEA 2024.1 的 Ultimate 版本中加入了对 OpenRewrite 的支持

  • 在命令行中使用 Maven 或 Gradle 插件,比如 mvn -U org.openrewrite.maven:rewrite-maven-plugin:run -Drewrite.activeRecipes=org.openrewrite.java.RemoveUnusedImports。不过,命令行的方式无法为配方配置参数。
  • pom.xml 或者 build.gradle 中使用插件,这种方式我们可以对插件和配方进行灵活的配置。
  • 通过在项目中创建 YAML 文件,通过 IDEA 来执行。

当然还可以自己编写代码来执行配置。这里我们选择最后一种,使用已有的配方来演示如何修改。Java 项目用以前演示 Tekton 时用的 tekton-demo

在代码中有两个 endpoint,我想讲其中一个的方法名修改为 greeting

package com.example.tekton.test;  
  
import org.springframework.boot.SpringApplication;  
import org.springframework.boot.autoconfigure.SpringBootApplication;  
import org.springframework.web.bind.annotation.GetMapping;  
import org.springframework.web.bind.annotation.PathVariable;  
import org.springframework.web.bind.annotation.RestController;  
  
@SpringBootApplication  
@RestController  
public class TektonTestApplication {  
  
    public static void main(String[] args) {  
       SpringApplication.run(TektonTestApplication.class, args);  
    }  
  
    @GetMapping("/hi")  
    public String hello() {  
       return "hello, devops";  
    }  
  
    @GetMapping("/hi/{name}")  
    public String hello(@PathVariable String name) {  
       return "hello, " + name + "!";  
    }  
}

在项目的根目录中创建一个文件 rewrite.yml,使用配置 ChangeMethodName,指定要替换的方法签名,以及新的方法名。

---  
type: specs.openrewrite.org/v1beta/recipe  
name: com.atbug.demo.ChangeMethodNameExample  
displayName: Change method name example  
recipeList:  
  - org.openrewrite.java.ChangeMethodName:  
      methodPattern: com.example.tekton.test.TektonTestApplication hello(String)  
      newMethodName: greeting

在 IDEA 中可以识别到该配置文件,点击按钮就可以运行。在 IDEA 中执行,实际也是运行 Maven Rewrite 插件,只是无需修改 pom 文件。

查看结果可以看到方法名已经被修改了。

[INFO] Using active recipe(s) [com.atbug.demo.ChangeMethodNameExample]
[INFO] Using active styles(s) []
[INFO] Validating active recipes...
[INFO] Project [tekton-test] Resolving Poms...
[INFO] Project [tekton-test] Parsing source files
[INFO] Running recipe(s)...
[WARNING] Changes have been made to src/main/java/com/example/tekton/test/TektonTestApplication.java by:
[WARNING]     org.openrewrite.java.ChangeMethodName: {methodPattern=com.example.tekton.test.TektonTestApplication hello(String), newMethodName=greeting}
[WARNING] Please review and commit the results.
[WARNING] Estimate time saved: 5m

总结

本篇文章介绍了 OpenRewrite 的基本概念与原理,并以较为简单的示例进行演示。实际应用中,可以灵活组合多种配方,或自行编写更复杂的规则,以实现大规模且可控的代码迁移。此外,还可将 OpenRewrite 集成至 CI/CD 流程,实现自动化的迁移与持续测试。

尽管官方和社区已提供大量现成的配方,但在企业中往往会遇到许多特定的业务场景,因此自行编写更为复杂的配方在所难免。要实现这一点,我们需要更深入地理解 LST、配方与访问者等核心概念,以确保能够定制出满足实际需求的解决方案。后续我将继续整理更多相关笔记并分享给大家。

(转载本站文章请注明作者和出处乱世浮生,请勿用于任何商业用途)

comments powered by Disqus