CrazyAirhead

疯狂的傻瓜,傻瓜也疯狂——傻方能执著,疯狂才专注!

0%

加餐 —— 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

代码

代码本身无特别之处,就是根据配置的信息不断地替换源代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
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)));
}
}

注意

再次提醒这个工具不是无缝迁移的,也就是说不能说迁移完就可以直接启动的,这个工具只是减少工作量。

欢迎关注我的其它发布渠道