之前维护的一个项目,项目启动需要12分钟,开发环境需要频繁启停,极大影响开发效率
分析
使用IDEA profiler收集启动阶段的数据,并用idea打开火焰图分析
发现启动时间大部分都消耗在注入Autowired资源以及创建Aop切面
同时整理了项目的模块发现开发环境下有一部分是不常用的
优化方案
首先是修改pom使开发环境不加载不常用的模块,启动速度快了60秒左右
然后编写了脚本静态分析源码,收集了所有没有使用的@Autowired和@Resource,并手动处理(项目代码规范不是很好, 不确定是否有特殊的引用, 虽然最后没发现这样的引用),启动速度快了30秒左右
import java.io.File
val dir = File("/path/to/a/project")
val unused = mutableListOf<Pair<String, String>>()
val autowiredPattern = Regex("(private|public|protected)\\s+\\w+\\s+(?<varName>\\w+)\\s*;")
fun collect(file: File) {
if (file.isDirectory) {
// 忽略编译产物
file.name == "target" && file.parentFile.resolve("pom.xml").exists() && return
file.listFiles()?.forEach { collect(it) }
return
}
if (!file.name.endsWith(".java")) {
return
}
// println("scan ${file.name}")
// 找到所有autowired
val autowiredList = mutableListOf<String>()
var nextIsAutowired = false
val lines = file.readLines().filter { it.startsWith("//") }
for (line in lines) {
if (line.trim().run{ startsWith("@Autowired") || startsWith("@Resource") }) {
nextIsAutowired = true
}
if (nextIsAutowired) {
val result = autowiredPattern.find(line)
if (result != null) autowiredList.add(result.groups["varName"]!!.value)
nextIsAutowired = false
continue
}
}
if (autowiredList.isEmpty()) return
// 查询没有使用的autowired
val filePath = file.absolutePath
.replace("\\", "/")
.substringAfter("src/main/java/")
.replace("/", ".")
.removeSuffix(".java")
autowiredList.filter { varName ->
lines.all { line -> varName !in line || line.trim().matches(autowiredPattern) }
}.let {
unused += it.map { v -> filePath to v }
}
}
collect(dir)
File("unused-autowired-${dir.name}.txt").writeText(unused.joinToString("\n") { (k, v) -> "$k: $v" })
此时启动速度依然很慢,于是使用 lazy-initialization 的方案,环境变量中添加 spring.main.lazy-initialization=true
但是所有bean都懒加载会导致一部分模块出错最终导致启动失败,于是添加filter
@Bean
public LazyInitializationExcludeFilter filter() {
return (beanName, beanDefinition, beanType) -> {
String className = beanType.getName();
return className.startsWith("com.example.module.plugin.important")
|| className.startsWith("com.example.module.common")
;
};
}
最终启动速度从12分钟优化到最快4分钟(不启用可选的模块)
总结
- 模块拆分解耦是好文明,在这种场景下直接禁用不用的模块也不影响项目启动
- 在bean初始化里写逻辑是坏文明,遇到一个上游依赖的组件,自己封装了一层XxlJobSpringExecutor,还把class设置成package-private,导致不加载该模块就无法正常初始化xxljob