OpenRewrite复合配方用于自动化迁移

本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

转载自夜明的孤行灯

本文链接地址: 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/

发表评论