优化单元测试效率:Spring 工程启动耗时统计

news2024/10/2 8:37:42

相关文章:

  • Java Agent 的简单使用

本文相关代码地址:https://gitee.com/dongguabai/blog

单元测试在软件项目的可持续发展中扮演着不可或缺的角色,这一点毫无疑问。不久前,公司大佬在内部分享时也提到过:单元测试是每个研发工程师的基本技能。

然而,在实际的开发过程中,我们发现很多研发人员并不愿意编写单元测试。其中一个普遍的原因是很多单元测试需要启动 Spring 容器(而许多 Mock 工具本身并不好用),导致启动过程非常耗时。

以我们的核心项目为例,一次单元测试的运行时间接近 5 分钟。考虑到一般需求开发涉及多个接口,再加上中间调试的时间,可能测试几个接口就会耗费一整天的时间。这严重影响了研发工作的效率。

[2023-10-23 12:34:07] [xxx_release] [Dongguabai.local] [INFO] [dfe7d0de51f548dd90e31411312acbd1] 705c1661f4044444b10b53009a32e8e1 main org.springframework.boot.StartupInfoLogger.logStarted(StartupInfoLogger.java:57) - Started SrvCustomerMapperProxyTest in 277.059 seconds (JVM running for 279.544) [12:34:07.659]

也有同学吐槽过:
在这里插入图片描述

本文就是希望可以解决这一痛点问题。

切入点

现在的问题是我们的项目启动 Spring 容器慢,那么我们就需要知道启动到底慢在哪里,结合 Spring 的生命周期,就需要去统计每个 Bean 实例化的时间,有几种很常见的切入点:

  • 基于 BeanPostProcessor

    BeanPostProcessor 提供了两个函数可以帮助我们在 Bean 实例化之后,属性赋值前做一些事情,这也是网上很多博客介绍的一种方案,但是这个方案有个细节要注意,一个 Bean 可能被多个 BeanPostProcessor 去处理,所以要注意计算耗时的 BeanPostProcessorpostProcessBeforeInitialization 是第一个调用,且 postProcessAfterInitialization 是最后一个调用。这里就涉及到多个 BeanPostProcessor 处理的顺序问题。

    在 Spring 容器中,多个 BeanPostProcessor ,它们的执行顺序是:

    1. postProcessBeforeInitialization 方法:按照 BeanPostProcessor 在 Spring 容器中的注册顺序依次执行。
    2. InitializingBean.afterPropertiesSet 方法:这是Bean初始化后调用的方法。
    3. Bean 配置中的 init-method:如果有配置,则执行该方法。
    4. postProcessAfterInitialization 方法:按照 BeanPostProcessor 在 Spring 容器中的注册顺序依次执行。

​ 很明显,与我们期望的计算耗时的 BeanPostProcessorpostProcessBeforeInitializationpostProcessAfterInitialization 的执行顺序是有冲突的,所以需要特殊处理。当然如果实际场景影响不大,直接基于 BeanPostProcessor 来做问题也不大。

  • 基于 InstantiationStrategy

    它负责基于默认构造器,实例化 Bean。需要在 instantiate 函数执行前后进行统计,但整体实现相对比较复杂

  • 基于 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean

    统计的时间范围太宽了,属性赋值的情况也会计入其中,不太准确

  • 其实以上三点皆有缺陷,在业务系统中,每个 Bean 的耗时点可能各不一样,有的在实例化阶段,有的在初始化阶段(初始化又分多种情况,如 @PostConstructafterPropertiesSetinit-method)。所以这几块要单独统计,便于真正知道 Bean 初始化的耗时

耗时统计实现思路

其实耗时统计说白了就是在方法前后记录一下时间,然后求出差值即可。很常见的方案就是基于代理模式去做,但无论是基于 BeanPostProcessor 还是 InstantiationStrategy 其实都需要考虑 Spring 很多基础组件的顺序问题,相对比较麻烦。本文是基于 Java Agent 实现(关于 Java Agent 可以参看:Java Agent 的简单使用),对 Bean 的实例化和初始化两个阶段分别统计耗时。

耗时记录拦截器:

package blog.dongguabai.spring.bean.timestatistics;

import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.Argument;
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import net.bytebuddy.implementation.bind.annotation.This;

import java.lang.reflect.Method;
import java.util.Objects;
import java.util.concurrent.Callable;

/**
 * @author dongguabai
 * @date 2023-10-22 14:18
 */
public class BeanLifeInterceptor {

    @RuntimeType
    public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable, @Argument(1) String beaName, @AllArguments Object[] args, @This Object obj) throws Exception {
        long start = System.currentTimeMillis();
        try {
            return callable.call();
        } finally {
            long end = System.currentTimeMillis();
            if (Objects.equals(method.getName(), "instantiate")) {
                instantiateLog(beaName, end - start);
            }
            if (Objects.equals(method.getName(), "postProcessBeforeInitialization")) {
                beforeInitializationLog(beaName, start);
            }
            if (Objects.equals(method.getName(), "postProcessAfterInitialization")) {
                afterInitializationLog(beaName, end);
            }
        }
    }

    private synchronized static void instantiateLog(String beaName, long time) {
        Log log = LogInterceptor.logs.get(beaName);
        if (log == null) {
            log = new Log(beaName);
        }
        TimeDate timeDate = log.getTimeDate();
        timeDate.setBeanName(beaName);
        timeDate.setInstantiateTime(time);
        LogInterceptor.logs.put(beaName, log);
    }

    private synchronized static void beforeInitializationLog(String beaName, long time) {
        Log log = LogInterceptor.logs.get(beaName);
        if (log == null) {
            log = new Log(beaName);
        }
        log.setStartInitializationTime(Math.min(log.getStartInitializationTime() == 0L ? Long.MAX_VALUE : log.getStartInitializationTime(), time));
        LogInterceptor.logs.put(beaName, log);
    }

    private synchronized static void afterInitializationLog(String beaName, long time) {
        Log log = LogInterceptor.logs.get(beaName);
        if (log == null) {
            log = new Log(beaName);
        }
        log.setEndInitializationTime(Math.max(time, log.getEndInitializationTime()));
        LogInterceptor.logs.put(beaName, log);
    }
}

装载耗时记录拦截器:

package blog.dongguabai.spring.bean.timestatistics;

import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;

import java.lang.instrument.Instrumentation;

/**
 * @author dongguabai
 * @date 2023-10-22 14:19
 */
public class PreMainAgent {

    public static void premain(String agentArgs, Instrumentation inst) {
        new AgentBuilder.Default().type(ElementMatchers.hasSuperType(ElementMatchers.named("org.springframework.beans.factory.support.InstantiationStrategy")))
                .transform((builder, typeDescription, classLoader, javaModule) -> builder
                        .method(ElementMatchers.named("instantiate"))
                        .intercept(MethodDelegation.to(BeanLifeInterceptor.class)))
                .type(ElementMatchers.hasSuperType(ElementMatchers.named("org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter"))).
                transform((builder, typeDescription, classLoader, javaModule) -> builder
                        .method(ElementMatchers.named("postProcessBeforeInitialization").or(ElementMatchers.named("postProcessAfterInitialization")))
                        .intercept(MethodDelegation.to(BeanLifeInterceptor.class)))
                .installOn(inst);
        new AgentBuilder.Default().type(ElementMatchers.named(agentArgs.split("#")[0]))
                .transform((builder, typeDescription, classLoader, javaModule) -> builder
                        .method(ElementMatchers.named(agentArgs.split("#")[1]))
                        .intercept(MethodDelegation.to(LogInterceptor.class)))
                .installOn(inst);
    }
}

将 Agent 工程进行打包:

➜  spring-bean-timestatistics mvn package
[INFO] Scanning for projects...
...
[WARNING] Replacing pre-existing project main-artifact file: /Users/dongguabai/IdeaProjects/github/spring-bean-timestatistics/target/dongguabai-spring-bean-timestatistics-1.0-SNAPSHOT.jar
with assembly file: /Users/dongguabai/IdeaProjects/github/spring-bean-timestatistics/target/dongguabai-spring-bean-timestatistics-1.0-SNAPSHOT.jar
...

查看耗时

在启动单元测试时,增加 VM options:

-javaagent:/Users/dongguabai/IdeaProjects/github/spring-bean-timestatistics/target/dongguabai-spring-bean-timestatistics-1.0-SNAPSHOT.jar=com.xxx.SrvCustomerMapperProxyTest#testInsert

这里 com.xxx.SrvCustomerMapperProxyTest 是指要代理的类,testInsert 是指要代理的函数。

接下来启动单元测试看看效果(可以看到这次启动居然只用了 200s,也说明每次启动的时间都不稳定):

...
[2023-10-24 18:04:45] [maf_release] [Dongguabai.local] [INFO] [b4f3a146566b46a49214e48ffabfb615] 06df0988e4364d499d90aa4f7b702b2f main org.springframework.boot.StartupInfoLogger.logStarted(StartupInfoLogger.java:57) - Started SrvCustomerMapperProxyTest in 199.152 seconds (JVM running for 204.326) [18:04:45.346]
...
BEANS TIME STATISTICS:
【shardingDatasourceRoutingDataSource-> totalTime=50526, instantiateTime=76, initializeTime=50450】
【basicDataDubboService-> totalTime=5876, instantiateTime=0, initializeTime=5876】
【com.alibaba.boot.dubbo.DubboProviderAutoConfiguration-> totalTime=4641, instantiateTime=0, initializeTime=4641】
【orgBaseManager-> totalTime=4481, instantiateTime=0, initializeTime=4481】
【srvCommonDubboServiceImpl-> totalTime=2845, instantiateTime=17, initializeTime=2828】
【storageProxyService-> totalTime=2708, instantiateTime=0, initializeTime=2708...

可以看到各个 Bean 实例化和初始化的耗时,接下来要做的就是逐个分析降低它们的耗时,这就比较简单了。

耗时分析

这里分析几个 Top 耗时。

shardingDatasourceRoutingDataSource

可以看到耗时在 initializeTime,而这个 Bean 实现了 AbstractRoutingDataSource,重写了 afterPropertiesSet 方法:

    private void initDataSource(DruidDataSource druidDataSource) {
        try {
            druidDataSource.init();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

而耗时主要就在连接池初始化,这个其实有两种方案:

  1. 异步初始化

    设置一下即可,但是这个要注意,可能我们跑单测的时候涉及到数据库访问,如果此时连接池没有初始化完成的话会出现异常

    druidDataSource.setAsyncInit(true);
    
  2. 初始化连接数为 0

    就是不需要一开始就设置数据库连接:

    ds.setInitialSize(NumberUtils.toInt(env.getProperty("ds.initialSize"), 10));
    

我这边使用第二种方案。

basicDataDubboService

这是一个普通被 @Service 标注的 Bean,可以看到也是耗时在 initializeTime,这是因为这个项目使用的是自己封装的 Dubbo 接口,依赖注入 Dubbo Reference 是依赖于 BeanPostProcessor 实现。

basicDataDubboService 中引入 Dubbo 接口方式是使用的自定义注解 @DubboConsumer

@Service
@Slf4j
public class BasicDataDubboService {
   @DubboConsumer(check = false)          
    private FaultService faultService;
}

@DubboConsumer 会被 BeanPostProcessor 处理:

 @Bean
    public BeanPostProcessor beanPostProcessor() {
        return new BeanPostProcessor() {
            @Override
            public Object postProcessBeforeInitialization(Object bean, String beanName)
                    throws BeansException {
                Class<?> objClz = bean.getClass();
                if (org.springframework.aop.support.AopUtils.isAopProxy(bean)) {
                    objClz = org.springframework.aop.support.AopUtils.getTargetClass(bean);
                }

                try {
                    for (Field field : objClz.getDeclaredFields()) {
                        DubboConsumer dubboConsumer = field.getAnnotation(DubboConsumer.class);
...

分析后可以设置 @DubboConsumer 属性 lazytrue,相当于 ReferenceBeanlazy 属性,这样可以懒加载创建 Reference 代理。

同时也发现有很多无用的注入,这里可以直接清除:

在这里插入图片描述

com.alibaba.boot.dubbo.DubboProviderAutoConfiguration

这个也是团队内部封装的 Dubbo 配置类,它依赖 @PostConstruct 对外暴露 Dubbo 服务:

    @PostConstruct
    public void init() throws Exception {
        Map<String, Object> beans = this.applicationContext.getBeansWithAnnotation(Service.class);
        for (Map.Entry<String, Object> entry : beans.entrySet()) {
            this.initProviderBean(entry.getKey(), entry.getValue());
            if(this.properties.getProviderDefaultGroup()){
                this.initProviderBeanWithGroup(entry.getKey(), entry.getValue());
            }
        }
    }
 private void initProviderBeanWithGroup(String beanName, Object bean) throws Exception {
   ...
   serviceConfig.export();
 }

可以看到整个实现也是相对比较粗糙(比如根本没必要获取全部的 @Service 标注的 Bean,可以自定义一个实现注解,降低查找范围),先不纠结于细节,至少本地启动单元测试是没必要暴露 Dubbo 服务的,所以是否暴露 Dubbo 服务可以基于 profile 做一个限制。

结果查看

上面三耗时分析,除了 com.alibaba.boot.dubbo.DubboProviderAutoConfiguration 由于涉及到代码改造,暂时没有处理,其余的两个已经处理,重新启动,看看效果:

...
[2023-10-24 18:52:03] [maf_release] [Dongguabai.local] [INFO] [d53b3ebcc40240f19b12ca0bdf434436] 375000afa08a48b49f9121a5c4760386 main org.springframework.boot.StartupInfoLogger.logStarted(StartupInfoLogger.java:57) - Started SrvCustomerMapperProxyTest in 142.282 seconds (JVM running for 148.136) [18:52:03.086]
...
【basicDataDubboService-> totalTime=5567, instantiateTime=0, initializeTime=5567】
【shardingDatasourceRoutingDataSource-> totalTime=272, instantiateTime=70, initializeTime=202...

可以看到整个启动速度大大提升,主要还是 shardingDatasourceRoutingDataSource 的初始化速度从 50450ms 降低至 202ms。至于 basicDataDubboService,效果相对一般,但也从中找到了内部封装的 Dubbo 组件优化点。

总结

基于 TOP3 耗时 Bean 的分析来看,耗时有这么几种原因:

  1. 数据库连接可以延迟初始化
  2. 移除无用的依赖注入
  3. 内部封装的 Dubbo 组件存在很多优化点,如:
    • Dubbo Provider 在本地没必要对外暴露服务
    • 懒加载创建 Reference 代理

尽管不同项目的耗时原因各有差异,但只要能够确定具体的耗时点,问题就不会太棘手。本文介绍了一个简单的工具,用于获取 Spring Bean 初始化的时间,借此能够准确定位具体的 Bean,从而有针对性地进行优化。通过优化这些耗时操作,就能够有效提升整个项目的启动速度,显著增强研发工作的效率。

References

  • https://github.com/microsphere-projects/microsphere-spring
  • https://github.com/raphw/byte-buddy/issues/748

欢迎关注公众号:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1128816.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

“动捕设备+飞兔渲染软件”,激发数字人短视频营销新动力

随着短视频行业持续破发&#xff0c;短视频成为目前吸引流量最快的方式之一。同时&#xff0c;元宇宙催生众多虚拟主播、虚拟偶像、虚拟IP等&#xff0c;以数字人形象结合短视频形式&#xff0c;在社交平台上频频出圈。如虚拟美妆达人“柳夜熙”一条视频涨粉150w&#xff0c;但…

[卷积神经网络]FasterNet论文解析

一、概述 FasterNet是CVPR2023的文章&#xff0c;通过使用全新的部分卷积PConv&#xff0c;更高效的提取空间信息&#xff0c;同时削减冗余计算和内存访问&#xff0c;效果非常明显。相较于DWConv&#xff0c;PConv的速度更快且精度也非常高&#xff0c;识别精度基本等同于大型…

【计算机网络(1)】计算机网络体系结构1:计算机网络概述

文章目录 概念 & 功能 & 发展计算机网络的概念计算机网络的功能计算机网络的发展网络的本质 组成 & 分类计算机网络的组成计算机网络的分类 概念 & 功能 & 发展 计算机网络的概念 1. 网络 网一样的东西或网状系统。其中&#xff08;有线电视网络、电信网…

亲测解决no module named ‘PyQt5.QtCore‘

如果是在windows上遇到这个问题&#xff0c;升级一下VS Studio即可。 运行坏境和问题 Win10 Anaconda 解决方法 升級vs studio from 2022 preview to 2022。

springboot配置注入增强(四)使用框架实现自定义数据源和自定义属性解析

1.代码 框架代码已经上传到gitee中 代码链接&#xff1a;https://gitee.com/summer-cat001/config-enhance jar包&#xff1a;https://gitee.com/summer-cat001/config-enhance/tree/master/build/libs 2.使用方式 2.1 引入jar包 引入本地jar包或者把jar包上传到自己的ma…

年薪20w+,做测试的第4年,从手工测试到自动化测试每一步都是艰难的~

自己已经做了好几年的手工测试了&#xff0c;越来越觉得如果一直在手工测试的道路上前进&#xff0c;并不会有很大的发展&#xff0c;所以通过自己的努力&#xff0c;已经成功的转入自动化测试的方向&#xff0c;那么想快速的转入自动化方向&#xff0c;我们应该怎么做呢&#…

Linux的命令基本格式

因为对服务器来讲&#xff0c;图形界面会占用更多的系统资源&#xff0c;而且会安装更多的服务、开放更多的端口&#xff0c;这对服务器的稳定性和安全性都有负面影响。其实&#xff0c;服务器是一个连显示器都没有的家伙&#xff0c;要图形界面干十么&#xff1f;说到这里&…

python—openpyxl操作excel详解

前言 openpyxl属于第三方模块&#xff0c;在python中用来处理excel文件。 可以对excel进行的操作有&#xff1a;读写、修改、调整样式及插入图片等。 但只能用来处理【 .xlsx】 后缀的excel文件。 使用前需要先安装&#xff0c;安装方法&#xff1a; pip install openpyxl…

基于二维小波变换的散斑相位奇异构造算法matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 图(1)表示散斑原图像&#xff0c;(2)表示对(1)图像进行x轴方向的极化分析的小波相位图&#xff0c;呈周期的水平条纹&#xff0c;(3)表示对(1)图像…

大数据Doris(十二):扩容缩容

文章目录 扩容缩容 一、FE 扩容和缩容 1、增加 FE 节点 2、 删除 FE 节点

非接触式外径测量仪 光电在线检测

非接触式的检测方式可以在不损伤产品表面的情况下&#xff0c;进行高精度的检测&#xff0c;它能对一些高温、熔融等不易测量的轧材检测&#xff0c;适用面更广。光电非接触式的外径测量仪同样是非接触式的检测方式&#xff0c;完成了线缆电缆、橡胶、塑料等产品的高精度检测。…

YOLOv7改进:新颖的上下文解耦头TSCODE,即插即用,各个数据集下实现暴力涨点

💡💡💡本文属于原创独家改进:上下文解耦头TSCODE,进行深、浅层的特征融合,最后再分别输入到头部进行相应的解码输出,实现暴力暴力涨点 上下文解耦头TSCODE| 亲测在多个数据集实现暴力涨点,对遮挡场景、小目标场景提升也明显; 收录: YOLOv7高阶自研专栏介绍: …

✔ ★【备战实习(面经+项目+算法)】 10.22学习时间表(总计学习时间:4.5h)(算法刷题:7道)

✔ ★【备战实习&#xff08;面经项目算法&#xff09;】 坚持完成每天必做如何找到好工作1. 科学的学习方法&#xff08;专注&#xff01;效率&#xff01;记忆&#xff01;心流&#xff01;&#xff09;2. 每天认真完成必做项&#xff0c;踏实学习技术 认真完成每天必做&…

rust学习——操作字符串、字符串转义、操作UTF8-字符串 (操作中文字符串)

文章目录 操作字符串追加 (Push)插入 (Insert)替换 (Replace)1、replace2、replacen3、replace_range 删除 (Delete)1、 pop —— 删除并返回字符串的最后一个字符2、 remove —— 删除并返回字符串中指定位置的字符3、truncate —— 删除字符串中从指定位置开始到结尾的全部字…

CentOS7单磁盘挂载一个目录

1、查看磁盘是否存在 fdisk -l 2、格式化磁盘 mkfs -t ext4 /dev/vdb 3、查看磁盘分区的UUID blkid /dev/vdb 4、创建挂载目录 mkdir -p /data 5、磁盘挂载 mount /dev/vdb /data 6、设置开机启动自动挂载 需要让系统开机自动挂载&#xff0c;需要将挂载信息写入到/etc/f…

安装PyCharm必看,手把手教你安装pycharm

目录 一&#xff0c;简介 二&#xff0c;安装包 一&#xff0c;简介 Jetbrains家族和Pycharm版本划分&#xff1a; pycharm是Jetbrains家族中的一个明星产品&#xff0c;Jetbrains开发了许多好用的编辑器&#xff0c;包括Java编辑器&#xff08;IntelliJ IDEA&#xff09;、…

超实用!你不得不知道的7款项目管理神器

大家好&#xff0c;我是老原。 一个项目无论大小&#xff0c;都需要一款高效且实用的项目管理工具&#xff0c;对项目流程进行把控、及时共享工作文档&#xff0c;从而让工作变得更有效率。 大家在网上搜到的就有很多&#xff0c;譬如project、禅道、甘特图等这些都是使用率超…

“深入探讨Java JUC中的ReentrantLock锁:实现多线程同步与并发控制“

简介 1、从Java5开始&#xff0c;Java提供了一种功能更强大的线程同步机制——通过显式定义同步锁对象来实现同步&#xff0c;在这种机制下&#xff0c;同步锁由Lock对象充当。 2、Lock 提供了比synchronized方法和synchronized代码块更广泛的锁定操作&#xff0c;Lock允许实…

YOLOv8优化:独家创新(Partial_C_Detect)检测头结构创新,实现涨点 | 检测头新颖创新系列

💡💡💡本文独家改进:独家创新(Partial_C_Detect)检测头结构创新,适合科研创新度十足,强烈推荐 Partial_C_Detect | 亲测在多个数据集能够实现大幅涨点 💡💡💡Yolov8魔术师,独家首发创新(原创),适用于Yolov5、Yolov7、Yolov8等各个Yolo系列,专栏文章提…

ES 8.x 向量检索性能测试 把向量检索性能提升100倍!

向量检索不仅在的跨模态检索场景中应用广泛&#xff0c;随着chat gpt的或者&#xff0c;利用es的向量检索&#xff0c;在Ai领域发挥着越来越大的作用。 本文&#xff0c;主要测试es的向量检索性能。我从8.x就开始关注ES的向量检索了。当前ES已经发布到 8.10 版本。以下是官方文…