加餐 —— Spring Boot 项目转 Solon 项目工具
说明
公司里面原来的项目都是 Spring Boot 和 Spring Cloud 框架的,自己手动迁移完一个项目后,发现迁移的过程有些还是能代码化的东西,于是整理了 SpringConverter 这个工具。
这个工具不是说你转换完就能无痛的启动,你还是需要手动处理一些错误。虽然工具内置了一部分的 Spring 与 Solon 的对照关系,但你仍然可能需要修改这个工具的代码,配置遗漏的对照关系,以便能将自己的项目进行迁移。
思路
在手动替换的过程中,会首先创建新项目,然后把旧代码复制到新项目中,之后对相关的包和类进行替换,然后逐步修复其中的错误。
这个工具做的事情就是从实现了复制代码和替换包名、类名的过程。这个过程当中,我发现通常是一个包对另一个包,一个类对一个类,当然也会有多对一的情况,和无法对应的情况,于是注意的工作就是配置类名、包名的映射关系,然后就可以通过代码实现自动替换,从而实现减少工作量的目的。
技巧
- 自己手动转换 pom 或者 gradle 文件,调整好依赖,不要交给转换工具。
- 替换原项目地址,原项目包名,新项目地址,新项目包名。
- 转化工具放置在另一个项目(或者模块中),这样方便多次转换新项目(边调整映射配置边替换)。必要时也可以方便的整个删除新项目。
- 如果类名从短变长(通常是后缀相同),需要先替换类名,再通过旧包名+新类名的方式,替换新包名。
- 如果无法对应的包,可以直接替换为空串或者回车(\n)
- 如果是类上无法使用注解,可以增加回车(\n),替换从空串或者回车(\n)。
- 注意字符串匹配是通过正则匹配的,替换的原字符串中包含中括弧(()),大括号({}),星号(*)等时需要进行转移。
- 根据不同的类库进行替换,方便维护。必要时可以自己修改成配置文件从配置文件中读取对照关系。
项目地址
https://gitee.com/CrazyAirhead/porpoise-demo/tree/ray-admin/
ray-tools/ray-spring-converter
代码
代码本身无特别之处,就是根据配置的信息不断地替换源代码。
package com.goldsyear.ray.converter;
import static java.io.File.separator;
import com.jfinal.kit.Kv;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.Matcher;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.dromara.hutool.core.collection.set.SetUtil;
import org.dromara.hutool.core.io.file.FileTypeUtil;
import org.dromara.hutool.core.io.file.FileUtil;
/**
* SpringBoot 项目迁移 Solon 项目工具,并不能迁移完就直接启动,只是减少迁移工作量
*
* @author Airhead
*/
@Slf4j
public class RayConverterApp {
/** 白名单文件,不进行重写,避免出问题 */
private static final Set<String> WHITE_FILE_TYPES =
SetUtil.of("gif", "jpg", "svg", "png", "eot", "woff2", "ttf", "woff");
public static void main(String[] args) {
long start = System.currentTimeMillis();
// 修改路径
String oldProjectDir = "旧项目路";
String oldPackageName = "旧包名";
String newProjectDir = "新项目路";
String newPackageName = "新包名";
log.info("原项目目录:{}, 原基础包名:{}", oldProjectDir, oldPackageName);
log.info("新项目目录:{}, 新基础包名:{}", newProjectDir, newPackageName);
// ========== 配置,需要你手动修改 ==========
List<Kv> mappingList = new ArrayList<>();
mappingList.add(Kv.by("old", oldPackageName).set("new", newPackageName));
// Spring 包调整
mappingSpringBoot(mappingList);
// Hutool 包调整
mappingHutool(mappingList);
// FastJson
mappingFastJson(mappingList);
// Mybatis 包调整
mappingMybatis(mappingList);
// TODO: 根据自己依赖的项目模块进行替换
// 业务包调整
mappingBusiness(mappingList);
// 获得需要复制的文件
log.info("开始获得需要重写的文件,预计需要 10-20 秒");
Collection<File> files = listFiles(oldProjectDir);
log.info("需要重写的文件数量:{},预计需要 15-30 秒", files.size());
// 写入文件
files.forEach(
file -> {
// 如果是白名单的文件类型,不进行重写,直接拷贝
String fileType = FileTypeUtil.getType(file);
if (WHITE_FILE_TYPES.contains(fileType)) {
copyFile(file, oldProjectDir, oldPackageName, newProjectDir, newPackageName);
return;
}
// 如果非白名单的文件类型,重写内容,在生成文件
String content = replaceFileContent(file, mappingList);
writeFile(file, content, oldProjectDir, oldPackageName, newProjectDir, newPackageName);
});
log.info("重写完成,共耗时:{} 秒", (System.currentTimeMillis() - start) / 1000);
}
private static void mappingBusiness(List<Kv> mappingList) {
// TODO
}
private static void mappingFastJson(List<Kv> mappingList) {
// TODO
}
private static void mappingMybatis(List<Kv> mappingList) {
// Page
mappingList.add(
Kv.by("old", "com.baomidou.mybatisplus.extension.plugins.pagination.Page")
.set("new", "com.baomidou.mybatisplus.solon.plugins.pagination.Page"));
// Model
mappingList.add(
Kv.by("old", "com.baomidou.mybatisplus.extension.activerecord.Model")
.set("new", "com.baomidou.mybatisplus.solon.activerecord.Model"));
// TenantLineHandler
mappingList.add(
Kv.by("old", "com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler")
.set("new", "com.baomidou.mybatisplus.solon.plugins.handler.TenantLineHandler"));
// MybatisPlusInterceptor
mappingList.add(
Kv.by("old", "com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor")
.set("new", "com.baomidou.mybatisplus.solon.plugins.MybatisPlusInterceptor"));
// PaginationInnerInterceptor
mappingList.add(
Kv.by("old", "com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor")
.set("new", "com.baomidou.mybatisplus.solon.plugins.inner.PaginationInnerInterceptor"));
// TenantLineInnerInterceptor
mappingList.add(
Kv.by("old", "com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor")
.set("new", "com.baomidou.mybatisplus.solon.plugins.inner.TenantLineInnerInterceptor"));
// FastjsonTypeHandler
mappingList.add(
Kv.by("old", "com.baomidou.mybatisplus.extension.handlers.FastjsonTypeHandler")
.set("new", "com.baomidou.mybatisplus.solon.handlers.FastjsonTypeHandler"));
}
private static void mappingHutool(List<Kv> mappingList) {
// StrUtil;
mappingList.add(
Kv.by("old", "cn.hutool.core.util.StrUtil")
.set("new", "org.dromara.hutool.core.text.StrUtil"));
// Convert;
mappingList.add(
Kv.by("old", "cn.hutool.core.convert.Convert")
.set("new", "org.dromara.hutool.core.convert.ConvertUtil"));
mappingList.add(Kv.by("old", "Convert\\.").set("new", "ConvertUtil."));
// IdUtil;
mappingList.add(
Kv.by("old", "cn.hutool.core.util.IdUtil")
.set("new", "org.dromara.hutool.core.data.id.IdUtil"));
// DateUtil;
mappingList.add(
Kv.by("old", "cn.hutool.core.date.DateUtil")
.set("new", "org.dromara.hutool.core.date.DateUtil"));
// ReUtil
mappingList.add(
Kv.by("old", "cn.hutool.core.util.ReUtil")
.set("new", "org.dromara.hutool.core.regex.ReUtil"));
// Spring CollectionUtils;
mappingList.add(
Kv.by("old", "org.springframework.util.CollectionUtils")
.set("new", "org.dromara.hutool.core.collection.CollUtil"));
mappingList.add(Kv.by("old", "CollectionUtils").set("new", "CollUtil"));
// CollectionUtil;
mappingList.add(
Kv.by("old", "cn.hutool.core.collection.CollectionUtil")
.set("new", "org.dromara.hutool.core.collection.CollUtil"));
mappingList.add(Kv.by("old", "CollectionUtil").set("new", "CollUtil"));
// LocalDateTimeUtil
mappingList.add(
Kv.by("old", "cn.hutool.core.date.LocalDateTimeUtil")
.set("new", "org.dromara.hutool.core.date.TimeUtil"));
mappingList.add(Kv.by("old", "LocalDateTimeUtil").set("new", "TimeUtil"));
// FileUtil
mappingList.add(
Kv.by("old", "cn.hutool.core.io.FileUtil")
.set("new", "org.dromara.hutool.core.io.file.FileUtil"));
// IoUtil
mappingList.add(
Kv.by("old", "cn.hutool.core.io.IoUtil").set("new", "org.dromara.hutool.core.io.IoUtil"));
}
private static void mappingSpringBoot(List<Kv> mappingList) {
// annotation
mappingList.add(
Kv.by("old", "org.springframework.web.bind.annotation\\.\\*")
.set("new", "org.noear.solon.annotation.*"));
// Component
mappingList.add(
Kv.by("old", "org.springframework.stereotype.Component")
.set("new", "org.noear.solon.annotation.Component"));
// Service
mappingList.add(
Kv.by("old", "org.springframework.stereotype.Service")
.set("new", "org.noear.solon.annotation.Component"));
mappingList.add(Kv.by("old", "@Service").set("new", "@Component"));
// Autowired
mappingList.add(
Kv.by("old", "org.springframework.beans.factory.annotation.Autowired")
.set("new", "org.noear.solon.annotation.Inject"));
mappingList.add(Kv.by("old", "@Autowired").set("new", "@Inject"));
// Controller
mappingList.add(
Kv.by("old", "org.springframework.web.bind.annotation.RestController")
.set("new", "org.noear.solon.annotation.Controller"));
mappingList.add(Kv.by("old", "@RestController").set("new", "@Controller"));
// RequestMapping
mappingList.add(
Kv.by("old", "org.springframework.web.bind.annotation.RequestMapping")
.set("new", "org.noear.solon.annotation.Mapping"));
mappingList.add(Kv.by("old", "@RequestMapping").set("new", "@Mapping"));
// MediaType;
mappingList.add(Kv.by("old", "import org.springframework.http.MediaType;").set("new", ""));
mappingList.add(
Kv.by("old", ", produces = MediaType.APPLICATION_JSON_UTF8_VALUE").set("new", ""));
mappingList.add(
Kv.by("old", "import org.springframework.core.env.Environment;").set("new", ""));
// PostMapping
mappingList.add(
Kv.by("old", "org.springframework.web.bind.annotation.PostMapping")
.set("new", "org.noear.solon.annotation.Post"));
mappingList.add(Kv.by("old", "@PostMapping").set("new", "@Post\n@Mapping"));
// GetMapping
mappingList.add(
Kv.by("old", "org.springframework.web.bind.annotation.GetMapping")
.set("new", "org.noear.solon.annotation.Get"));
mappingList.add(Kv.by("old", "@GetMapping").set("new", "@Post\n@Mapping"));
// PutMapping
mappingList.add(
Kv.by("old", "org.springframework.web.bind.annotation.PutMapping")
.set("new", "org.noear.solon.annotation.Put"));
mappingList.add(Kv.by("old", "@PutMapping").set("new", "@Put\n@Mapping"));
// DeleteMapping
mappingList.add(
Kv.by("old", "org.springframework.web.bind.annotation.DeleteMapping")
.set("new", "org.noear.solon.annotation.Delete"));
mappingList.add(Kv.by("old", "@DeleteMapping").set("new", "@Delete\n@Mapping"));
// @RequestBody
mappingList.add(
Kv.by("old", "org.springframework.web.bind.annotation.RequestBody")
.set("new", "org.noear.solon.annotation.Body"));
mappingList.add(Kv.by("old", "@RequestBody").set("new", "@Body"));
// @RequestParam
mappingList.add(
Kv.by("old", "org.springframework.web.bind.annotation.RequestParam")
.set("new", "org.noear.solon.annotation.Param"));
mappingList.add(Kv.by("old", "@RequestParam").set("new", "@Param"));
// RequestPart
mappingList.add(
Kv.by("old", "org.springframework.web.bind.annotation.RequestPart")
.set("new", "org.noear.solon.annotation.Param"));
mappingList.add(Kv.by("old", "@RequestPart").set("new", "@Param"));
// Repository;
mappingList.add(
Kv.by("old", "import org.springframework.stereotype.Repository;").set("new", ""));
mappingList.add(Kv.by("old", "@Repository").set("new", ""));
// Validated
mappingList.add(
Kv.by("old", "org.springframework.validation.annotation.Validated")
.set("new", "org.noear.solon.validation.annotation.Validated"));
mappingList.add(Kv.by("old", "@Validated\n").set("new", ""));
// Configuration;
mappingList.add(
Kv.by("old", "org.springframework.context.annotation.Configuration")
.set("new", "org.noear.solon.annotation.Configuration"));
// RefreshScope;
mappingList.add(
Kv.by("old", "import org.springframework.cloud.context.config.annotation.RefreshScope;")
.set("new", ""));
mappingList.add(Kv.by("old", "@RefreshScope\n").set("new", ""));
// EnableAsync;
mappingList.add(
Kv.by("old", "import org.springframework.scheduling.annotation.EnableAsync;")
.set("new", ""));
mappingList.add(Kv.by("old", "@EnableAsync").set("new", ""));
// Bean;
mappingList.add(
Kv.by("old", "org.springframework.context.annotation.Bean")
.set("new", "org.noear.solon.annotation.Bean"));
// ConfigurationProperties;
mappingList.add(
Kv.by("old", "org.springframework.boot.context.properties.ConfigurationProperties")
.set("new", "org.noear.solon.annotation.Inject"));
mappingList.add(Kv.by("old", "@ConfigurationProperties").set("new", "@Inject"));
// CommandLineRunner
mappingList.add(
Kv.by("old", "import org.springframework.boot.CommandLineRunner;")
.set(
"new",
"import org.noear.solon.core.event.AppLoadEndEvent;\nimport org.noear.solon.core.event.EventListener;"));
mappingList.add(Kv.by("old", "CommandLineRunner").set("new", "EventListener<AppLoadEndEvent>"));
// Resource;
mappingList.add(
Kv.by("old", "javax.annotation.Resource").set("new", "org.noear.solon.annotation.Inject"));
mappingList.add(Kv.by("old", "@Resource\\(name").set("new", "@Inject\\(value"));
// Lazy;
mappingList.add(
Kv.by("old", "import org.springframework.context.annotation.Lazy;\n").set("new", ""));
mappingList.add(Kv.by("old", "@Lazy").set("new", ""));
// Valid
mappingList.add(
Kv.by("old", "javax.validation.Valid")
.set("new", "org.noear.solon.validation.annotation.Valid"));
mappingList.add(
Kv.by("old", "javax.validation.constraints.NotEmpty")
.set("new", "org.noear.solon.validation.annotation.NotEmpty"));
mappingList.add(
Kv.by("old", "javax.validation.constraints.NotNull")
.set("new", "org.noear.solon.validation.annotation.NotNull"));
// TODO: 补充更多
// HttpServletResponse;
mappingList.add(
Kv.by("old", "javax.servlet.http.HttpServletResponse")
.set("new", "org.noear.solon.core.handle.Context"));
mappingList.add(Kv.by("old", "HttpServletResponse").set("new", "Context"));
// MultipartFile;
mappingList.add(
Kv.by("old", "org.springframework.web.multipart.MultipartFile")
.set("new", "org.noear.solon.core.handle.UploadedFile"));
mappingList.add(Kv.by("old", "MultipartFile").set("new", "UploadedFile"));
// consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}
mappingList.add(
Kv.by("old", ",\n\\s+consumes = \\{MediaType.MULTIPART_FORM_DATA_VALUE\\}").set("new", ""));
mappingList.add(
Kv.by("old", ", consumes = \\{MediaType.MULTIPART_FORM_DATA_VALUE\\}").set("new", ""));
}
private static Collection<File> listFiles(String projectBaseDir) {
Collection<File> files = FileUtil.loopFiles(new File(projectBaseDir));
// 移除 IDEA、Git 自身的文件、Node 编译出来的文件
files =
files.stream()
.filter(
file ->
!file.getPath().contains(separator + "target" + separator)
&& !file.getPath().contains(separator + "node_modules" + separator)
&& !file.getPath().contains(separator + ".idea" + separator)
&& !file.getPath().contains(separator + ".git" + separator)
&& !file.getPath().contains(separator + "dist" + separator)
&& !file.getPath().contains(separator + "build" + separator)
&& !file.getPath().contains(separator + "out" + separator)
&& !file.getPath().contains(".iml")
&& !file.getPath().contains(".html.gz")
&& !file.getPath().contains("build.gradle")
&& !file.getPath().contains("pom.xml"))
.collect(Collectors.toList());
return files;
}
private static String replaceFileContent(File file, List<Kv> mappingList) {
String content = FileUtil.readString(file, StandardCharsets.UTF_8);
// 如果是白名单的文件类型,不进行重写
String fileType = FileTypeUtil.getType(file);
if (WHITE_FILE_TYPES.contains(fileType)) {
return content;
}
for (Kv kv : mappingList) {
content = content.replaceAll(kv.getStr("old"), kv.getStr("new"));
}
return content;
}
private static void writeFile(
File file,
String fileContent,
String oldProjectDir,
String oldPageName,
String newProjectDir,
String newPackageName) {
String newPath =
buildNewFilePath(file, oldProjectDir, oldPageName, newProjectDir, newPackageName);
FileUtil.writeUtf8String(fileContent, newPath);
}
private static void copyFile(
File file,
String oldProjectDir,
String oldPackageName,
String newProjectDir,
String newPackageName) {
String newPath =
buildNewFilePath(file, oldProjectDir, oldPackageName, newProjectDir, newPackageName);
FileUtil.copy(file, new File(newPath), true);
}
private static String buildNewFilePath(
File file,
String oldProjectDir,
String oldPackageName,
String newProjectDir,
String newPackageName) {
return file.getPath()
.replace(oldProjectDir, newProjectDir)
.replace(
oldPackageName.replaceAll("\\.", Matcher.quoteReplacement(separator)),
newPackageName.replaceAll("\\.", Matcher.quoteReplacement(separator)));
}
}
注意
再次提醒这个工具不是无缝迁移的,也就是说不能说迁移完就可以直接启动的,这个工具只是减少工作量。