本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
转载自夜明的孤行灯
本文链接地址: https://www.huangyunkun.com/2024/03/29/openrewrite-multi-recipe-to-rewrite-project/
OpenRewrite是一个源代码的自动重构生态系统,使开发人员能够有效地消除代码中的技术债务。
OpenRewrite可以实现代码解析和改写,理论上可以适用于很多场景,不仅仅是消除基础债务,只要能够通过固定规则进行代码变化的工作它理论上都能胜任。本文演示用OpenRewrite将已有Spring MVC项目迁移到阿里云云原生网关中。
背景
当前我们有若干个微服务工程,由于早期没有网关基建,所有建设了若干个Spring MVC项目对外提供HTTP能力,这些项目运行于Jetty环境,内部通过Dubbo协议调用后端一个或者多个后端服务,由于早期研发规范缺乏,这些HTTP项目包含大量业务逻辑和编排,所以HTTP项目中的逻辑需要保留。
从微服务长期治理看,我们希望这些逻辑更加内聚,将HTTP项目和后端的微服务合并成一个服务,通过云原生网关的能力实现HTTP和Dubbo项目的转换。
思路
如果是我们手动进行相关工作,大体有以下几步骤
- 找到要迁移的Controller(一般由@Controller或者@RestController作为注解)
- 找到要迁移的方法(一般由@RequestMapping或者@GetMapping等作为注解)
- 提取其中的关键信息,包括请求路径和参数
- 生成Dubbo接口定义和请求参数
- 拷贝原有Controller方法,尽可能保留原有逻辑,只改写参数相关的逻辑
- 拷贝原有工程代码到服务化工程中(单独的package包),并处理所有import
- 在云原生网关配置接口信息,包括HTTP参数和后端Dubbo服务信息
要自动化这些步骤我们需要以下技术/工具
无损语义树
要完成以上工作,我们需要和Java代码打交道,常规的AST解析会丢失一些信息,我们需要保留他们,包括但不限于注释、空格等,这里就需要用到OpenRewrite的无损语义树了。
OpenRewrite 的 "Lossless Semantic Trees" 是指 OpenRewrite 使用的一种特殊的抽象语法树(AST),它能够在进行代码分析和重构时保留所有源代码的信息,包括注释、格式和空白字符。这种 AST 的设计允许 OpenRewrite 在不丢失任何原始代码细节的情况下,进行精确的代码修改。在传统的 AST 中,通常会忽略空格、换行和注释等信息,因为这些元素对于代码的语义分析不是必需的。
OpenRewrite 的 Lossless Semantic Trees 保留了这些信息,使得代码重构操作可以像编辑器中手动修改代码一样,保持代码的原始风格和注释。这样,当使用 OpenRewrite 进行代码重构时,不仅能够保持代码逻辑的正确性,还能够保留代码的原始风格和意图,从而使重构后的代码更易于阅读和维护。
要手动获得一个java文件的无损语义树需要手动转换一下
ExecutionContext executionContext = initExecutionContext();
JavaTypeCache javaTypeCache = new JavaTypeCache();
JavaParser javaParser = JavaParser.fromJavaVersion()
.classpath(CLASS_PATH_LIST).typeCache(javaTypeCache)
.logCompilationWarningsAndErrors(false).build();
javaParser.parse(javaFilePath, null, executionContext);
自定义OpenRewrite配方
OpenRewrite的运行关键是它的配方,内置的配方和三方开源社区的配方都无法直接满足我们的需求,所以我们需要一个复合配方。
复合配方由多个配方组成,它们串行执行,配方直接可以通过上下文共享信息。下面是一个典型的复合配方
package org.example.testing;
import org.openrewrite.java.ChangeType;
public class JUnit5Migration extends Recipe {
@Override
public List<Recipe> getRecipeList() {
return Arrays.asList(
new ChangeType("org.junit.Test", "org.junit.jupiter.api.Test", false),
new AssertToAssertions(),
new RemovePublicTestModifiers()
);
}
}
复合配方的运行遵循以下流程
简而言之配方是针对每个源文件顺次运行的,如果某一步操作需要在扫描整个源代码以后再执行,则需要使用scan配方。
配方示例
为了更好的维护配方,我们可以把配方拆解成几个独立配方,可以参考最开始的手动迁移步骤。我们先来看一个最简单的,那就是找到我们需要的Controller
@Value
@EqualsAndHashCode(callSuper = false)
public class FindTargetControllerRecipe extends ScanningRecipe<Void> {
@Option(displayName = "Target Controller Name")
List<String> targetControllerList;
private final List<String> controllerAnnotationList = Lists.newArrayList("org.springframework.stereotype.Controller",
"org.springframework.web.bind.annotation.RestController");
@Override
public TreeVisitor<?, ExecutionContext> getScanner(Void acc) {
SharedTaskInfo sharedTaskInfo = new SharedTaskInfo();
return new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext executionContext) {
J.ClassDeclaration classDeclaration = super.visitClassDeclaration(classDecl, executionContext);
if (classDecl.getLeadingAnnotations().stream().anyMatch(anno -> {
return controllerAnnotationList.stream().anyMatch(annoType ->
TypeUtils.isOfClassType(anno.getType(), annoType));
})) {
if (targetControllerList.contains(classDecl.getSimpleName())) {
J.CompilationUnit cu = getCursor().getParent().getValue();
sharedTaskInfo.addController(ControllerInfo.newByFileAndClassDelId(cu.getId(), classDecl.getId()));
executionContext.putMessage(SharedTaskInfo.KEY, sharedTaskInfo);
}
}
return classDeclaration;
}
};
}
}
找到以后我们就可以分析了
@Slf4j
public class ApiAnalysisRecipe extends ScanningRecipe<Boolean> {
@Override
public TreeVisitor<?, ExecutionContext> getScanner(Boolean acc) {
return new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext executionContext) {
J.MethodDeclaration methodDeclaration = super.visitMethodDeclaration(method, executionContext);
J.ClassDeclaration belongClassDecl = getCursor().firstEnclosing(J.ClassDeclaration.class);
SharedTaskInfo sharedTaskInfo = executionContext.getMessage(SharedTaskInfo.KEY, new SharedTaskInfo());
//获取信息存储到sharedTaskInfo
return methodDeclaration;
}
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext executionContext) {
J.ClassDeclaration classDeclaration = super.visitClassDeclaration(classDecl, executionContext);
SharedTaskInfo sharedTaskInfo = SharedTaskInfo.getFromContext(executionContext);
//获取信息存储到sharedTaskInfo
return classDeclaration;
}
};
}
}
然后生成一些必要的参数类,比如原始请求如下
@Controller("/admin")
public class HelloController {
@RequestMapping(value = "/addUser")
public String addUser(String name, int age) {
//省略
}
}
改写成Dubbo服务以后
@DubboService
public class HelloServiceImpl implments HelloService {
@Override
public String addUser(HelloServiceAddUserParams requestParams) {
String name = requestParams.getName();
int age = requestParams.getAge();
//省略
}
}
这里涉及到了一些代码生成,可以直接使用java parser来生成。由于配方设计到生成,需要复写generate方法
public class GenerateWebParamRecipe extends ScanningRecipe<GenerateWebParamRecipe.Scanned> {
@Override
public Collection<? extends SourceFile> generate(GenerateWebParamRecipe.Scanned acc, ExecutionContext ctx) {
List<SourceFile> sourceFiles = Lists.newArrayList();
SharedTaskInfo sharedTaskInfo = SharedTaskInfo.getFromContext(ctx);
for (ControllerInfo controllerInfo : sharedTaskInfo.getControllerInfos()) {
for (MethodInfo methodInfo : controllerInfo.getMethodInfoMap().values()) {
//生成SourceFile
}
}
return sourceFiles;
}
}
最后复合配方如下
@Value
@EqualsAndHashCode(callSuper = false)
public class AliyuApinDubboAllInOneRecipe extends Recipe {
@Option
String webPackage;
@Option
String newWebPackage;
@Option
String clientWebPackage;
@Option
String webProjectPath;
@Option
String newWebProjectPath;
@Option
String clientProjectPath;
@Option
List<String> targetControllerList;
@Override
public List<Recipe> getRecipeList() {
List<Recipe> recipes = Lists.newLinkedList();
recipes.add(new FindTargetControllerRecipe(targetControllerList));
recipes.add(new ApiAnalysisRecipe());
recipes.add(new GenerateDubboParamRecipe(clientWebPackage, clientProjectPath));
recipes.add(new GenerateDuuboClientRecipe(clientWebPackage, clientProjectPath));
recipes.add(new GenerateDubboClientImplRecipe(newWebPackage));
recipes.add(new ChangePackage(webPackage, newWebPackage, true));
recipes.add(new MoveFileRecipe(webProjectPath, newWebProjectPath));
recipes.add(new SmartMavenRecipe(newWebProjectPath));
recipes.add(new SyncApiDefineToAliyun());
return recipes;
}
@Override
public String getDisplayName() {
return "自动迁移SpringMvc到阿里云微服务云网关";
}
@Override
public String getDescription() {
return "自动迁移SpringMvc到阿里云微服务云网关,包括生成接口文档、生成Dubbo客户端、生成Dubbo服务端、修改包名、合并项目、创建API等操作";
}
}
参考
https://www.aliyun.com/product/apigateway
https://www.alibabacloud.com/help/zh/mse/user-guide/configure-http-to-dubbo-protocol-conversion
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
转载自夜明的孤行灯
本文链接地址: https://www.huangyunkun.com/2024/03/29/openrewrite-multi-recipe-to-rewrite-project/