写点什么

记一次 SpringBoot 启动优化实践

  • 2023-02-15
    湖南
  • 本文字数:2461 字

    阅读完需:约 8 分钟

背景

最近看到一本关于 Java 应用通过基础设施于工具的改进,实现从构建到启动全方面提速的小册。想着自己目前在做的项目最让我觉得不爽的就是项目启动。尤其是需要频繁改代码,频繁重启项目的时候,每次项目启动起码都要两分钟才能启动完毕,挺影响开发效率的。就想着结合看的小册来做一个项目的启动优化,至少让自己在开发中更流畅


可能有人会说,可以使用devTools 来做热部署。对这玩意没有太了解就没有用

项目以及各项参数

  • SpringBoot 版本:1.5.6

  • 机器内存:16G

  • JVM 参数:默认值

  • AspectJ 版本:1.8.8

  • 启动整体耗时:150s~160s

优化结果

优化前

 2023-01-03 20:04:11.187 CST [main] INFO  s.d.s.w.p.DocumentationPluginsBootstrapper - Documentation plugins bootstrapped 2023-01-03 20:04:11.189 CST [main] INFO  s.d.s.w.p.DocumentationPluginsBootstrapper - Found 1 custom documentation plugin(s) 2023-01-03 20:04:11.210 CST [main] INFO  s.d.s.w.s.ApiListingReferenceScanner - Scanning for api listing references 2023-01-03 20:04:11.321 CST [main] INFO  com.xxx.XXApplication - Started XXApplication in 158.913 seconds (JVM running for 160.755)
复制代码

优化后

 2023-01-03 20:04:11.187 CST [main] INFO  s.d.s.w.p.DocumentationPluginsBootstrapper - Documentation plugins bootstrapped 2023-01-03 20:04:11.189 CST [main] INFO  s.d.s.w.p.DocumentationPluginsBootstrapper - Found 1 custom documentation plugin(s) 2023-01-03 20:04:11.210 CST [main] INFO  s.d.s.w.s.ApiListingReferenceScanner - Scanning for api listing references 2023-01-03 20:04:11.321 CST [main] INFO  com.xxx.XXApplication - Started XXApplication in 58.913 seconds (JVM running for 60.755)
复制代码

耗时排查

前提:我对 SpringBoot 以及 Spring 的理解还局限在八股文,


思路 1(笨方法) :所以对于排查的思路不说很聪明很有用,但至少很容易看懂。就是每一条日志看呗,如果明显的慢,在打印日志的时候肯定会停顿的,看一下接下来打印的日志是什么内容。排查的思路当然就是在上一句日志和打印出来的这句日志中找问题了。


思路 2:SpringBoot 的启动过程,肯定会加载很多 Bean,并且这些 Bean 都会走一遍 Bean 的生命周期。只要在加载每个 Bean 的过程中,记录每个 Bean 的初始化耗时,就可以大致知道问题出在哪里。可以使用 BeanPostProcessor接口来监控 Bean 的注入耗时。BeanPostProcessor是 Spring 提供的 Bean 初始化前后的钩子函数,用于执行一切自定义逻辑。下面是借鉴掘金一位大佬文章中的代码。


如果项目中的 Bean 数量不是特别多,我觉得可以换成 Stopwatch 来进行计时会更优雅一些!

 @Component public class TimeCostBeanPostProcessor implements BeanPostProcessor {     private Map<String, Long> costMap = Maps.newConcurrentMap();              @Override     public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {         costMap.put(beanName, System.currentTimeMillis());         return bean;     }     @Override     public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {         if (costMap.containsKey(beanName)) {             Long start = costMap.get(beanName);             long cost  = System.currentTimeMillis() - start;             if (cost > 0) {                 costMap.put(beanName, cost);                 System.out.println("bean: " + beanName + "\ttime: " + cost);             }         }         return bean;     } }
复制代码

思路 3: 遇事不决加内存!

优化方案

对于思路 1,就不多说了。其实没有那么好用。要不然我也不会去翻一开始说的那本书。

方案 1:SpringBoot 实现 Bean 的懒加载

SpringBoot 实现这一功能的版本是:2.2。不符合项目要求

方案 2:调整 JVM 参数实现启动优化

这个方案只是我个人认为是可行的,但其实没有什么用。主要是我理解的误区,我以为堆内存设置的足够大,减少 GC 频率,就可以提高启动速度。


有这种想法的原因是因为我在个人项目中使用过,因为这个项目刚起步,写的内容没那么多,随便设置几个 JVM 参数,堆内存设置大点,启动速度也就两三秒,不设置 JVM 参数,启动速度大概4~6秒。


而我在项目中使用,因为是本地,JVM 参数并没有设置过,堆内存最小就是电脑内存的 1/64,最大就是内存的 1/4。结果堆内存设置大一点,其实启动速度没什么变化。

方案 3(《Java 应用提速》中提供):升级 AspectJ 版本

这种方案的前提是AspectJ的版本要低于1.9.0,并且如果应用中有使用到通过注解(RetentionPolicy 需要是 RUNTIME)来做切面表达式


我看了书中的方案,就去翻了一下项目中关于AspectJ依赖的版本1.8.8,刚好在1.9.0下,并且翻了一下项目中所有做切面表达式的注解的生命周期都是RUNTIME的。


于是,我就试着修改 AspectJ版本,结果真的如书中所说,从 150 秒 降到了 70 多秒,提速了一半。

原理

在老版本的aspectJ 在判断一个bean的 method 上是否有 annotation的方法,发现它是去 jar 包里读取 Class 文件并解析类信息,耗时在类搜索和解析上。



unpackAnnotations()



annotaionFindler.getAnnotations()



而 Method 类本身就有getAnnotation方法,不需要自己加载类信息和解析



AspectJ 去 class 源文件中读取的原因是因为annotaion的生命周期如果不是RUNTIME的话,运行时是获取不到的


在 1.9.0 以后的版本中,已经解决该问题。


新版本在判断是否有注解的逻辑:与老版本的差异在于会判断 annotaion的生命周期是不是RUNTIME的,是的话,就直接从 Method 中获取了,不是才会加载源文件解析


其实应该还是可以继续优化的,后面再找找有没有别的思路。


作者:不知名玲玲

链接:https://juejin.cn/post/7184428718465482810

来源:稀土掘金

用户头像

还未添加个人签名 2021-07-28 加入

公众号:该用户快成仙了

评论

发布
暂无评论
记一次SpringBoot启动优化实践_Java_做梦都在改BUG_InfoQ写作社区