excel导出使用arthas动态追踪方法调用耗时后性能优化的过程

目录

一、背景

1、用户反应某个报表导出时等待很久也没有导出来,我F12查看导出接口在1.5min的时间请求被取消,后端服务器的网关(如Nginx、Apache)或后端应用本身可能设置了90秒的超时,用户一直等待也没有导出文件,所以需要优化导出逻辑加快导出时间。

二、发现问题

1、本地调试

1.1安装启动arthas

(1)、下载地址:https://arthas.aliyun.com/arthas-boot.jar

(2)、启动 java -jar arthas-boot.jar

(3)、选择目标应用id回车

(4)、执行tarce命令 -n 5 表示最多捕获5次方法调用,使用--skipJDKMethod false包含基础库调用耗时,还可以添加这个参数'#cost>500'表示只显示超过500毫秒的。

java 复制代码
trace 全类名 方法名  -n 5 --skipJDKMethod false

关于arthas的命令,可以安装idea插件arthas idea

选中方法名称鼠标右键,Arthas Command中可以选择相应的命令,可以复制出来,直接使用

1.2代码逻辑分析及初步优化

从代码看有三个主要逻辑,第一个使用远程调用的方式获取数据大概12万条,第二个是循环获取数据对某个字段进行国际化如下图,

第三个是把数据写入excel方法。

通过打断点发现,在第二个逻辑国际化方法中阻塞时间很久,所以决定先优化国际化的逻辑,放弃循环结果集,改为改成了sql关联国际化表的形式。

1.3使用arthas追踪耗时情况并进一步优化

解决完字段国际化后,重新看一下耗时情况。

从耗时情况分析远程获取数据方法占用主要时间,所以需要优化查询逻辑,经过查看sql执行计划extra 中useing filesort,没有在排序字段上创建索引导致创建索引后重新看耗时时间,获取数据缩短到50s。数据写入到excel耗时13s左右。总耗时63s左右小于90s本地可以正常导出。

2、服务器调试

本地调试好以后本以为问题就解决了,但是发布到生产服务器后,导出还是1.5min后取消了,没有什么效果,所以在生产服务器上进行了两个测试。
arthas下载命令:curl -O https://arthas.aliyun.com/arthas-boot.jar

然后启动和选择目标服务进入arthas终端

2.1、测试一使用arthas查看修改后的代码是否生效

在arthas终端中执行:

java 复制代码
jad 全类名

反编码查看代码是否修改后的逻辑,我发现代码已经是修改后的。

2.2、测试二使用arthas追踪耗时情况

执行trace 全类名 方法名 -n 5 --skipJDKMethod false 后在生产上执行导出

2.3、结论

通过上面结果可以发现,73.86%耗时的是把数据写入excel的方法。所以优化重点是数据写入excel。

三、解决问题

1、原来数据写入excel的方法

循环遍历所有的数据,如果数据是实体类对象则使用反射的方式获取对象中的属性值,这里有多少数据量就会使用几次反射,反射开销很大。

2、修改成使用fastexcel

FastExcel 使用了缓存,避免重复使用反射方法获取属性值,提高了性能。还有就是字段国际化,可以使用处理器进行处理(自定义了I18nCellWriteHandler处理器),所以sql中关联表国际化的逻辑去掉了。

这个是优化后的追踪方法调用耗时情况,数据写入excel速度显著提高耗时32秒左右,总耗时54秒左右。

ReflectionBeanMap 核心实现如下,initPropertyDescriptors 初始化属性描述符缓存,getValue时从缓存中获取PropertyDescriptor,反射调用 getter 方法。

java 复制代码
public class ReflectionBeanMap<T> implements BeanMap<T> {
    private final Class<T> beanClass;
    private final Map<String, PropertyDescriptor> propertyDescriptors;
    private final BeanMapConfig config;
    
    public ReflectionBeanMap(Class<T> beanClass) {
        this(beanClass, BeanMapConfig.defaultConfig());
    }
    
    public ReflectionBeanMap(Class<T> beanClass, BeanMapConfig config) {
        this.beanClass = beanClass;
        this.config = config;
        this.propertyDescriptors = initPropertyDescriptors(beanClass);
    }
    
    /**
     * 初始化属性描述符缓存
     */
    private Map<String, PropertyDescriptor> initPropertyDescriptors(Class<T> beanClass) {
        try {
            BeanInfo beanInfo = Introspector.getBeanInfo(beanClass, Object.class);
            PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
            
            Map<String, PropertyDescriptor> result = new LinkedHashMap<>();
            for (PropertyDescriptor descriptor : descriptors) {
                // 过滤掉没有 getter 方法的属性
                if (descriptor.getReadMethod() != null) {
                    result.put(descriptor.getName(), descriptor);
                }
            }
            return Collections.unmodifiableMap(result);
        } catch (IntrospectionException e) {
            throw new RuntimeException("Failed to introspect bean: " + beanClass.getName(), e);
        }
    }
    
    /**
     * 获取属性值 - 核心反射调用
     */
    @Override
    public Object getValue(T bean, String propertyName) {
        if (bean == null) {
            return null;
        }
        
        PropertyDescriptor descriptor = propertyDescriptors.get(propertyName);
        if (descriptor == null) {
            if (config.isIgnoreMissingProperties()) {
                return null;
            }
            throw new IllegalArgumentException(
                "Property '" + propertyName + "' not found in " + beanClass.getName());
        }
        
        Method readMethod = descriptor.getReadMethod();
        if (readMethod == null) {
            if (config.isIgnoreMissingGetters()) {
                return null;
            }
            throw new IllegalArgumentException(
                "No getter method for property '" + propertyName + "' in " + beanClass.getName());
        }
        
        try {
            // 反射调用 getter 方法
            return readMethod.invoke(bean);
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new RuntimeException(
                "Failed to get value for property '" + propertyName + "' from " + beanClass.getName(), e);
        }
    }
    
    /**
     * 设置属性值
     */
    @Override
    public void setValue(T bean, String propertyName, Object value) {
        if (bean == null) {
            return;
        }
        
        PropertyDescriptor descriptor = propertyDescriptors.get(propertyName);
        if (descriptor == null) {
            if (config.isIgnoreMissingProperties()) {
                return;
            }
            throw new IllegalArgumentException(
                "Property '" + propertyName + "' not found in " + beanClass.getName());
        }
        
        Method writeMethod = descriptor.getWriteMethod();
        if (writeMethod == null) {
            if (config.isIgnoreMissingSetters()) {
                return;
            }
            throw new IllegalArgumentException(
                "No setter method for property '" + propertyName + "' in " + beanClass.getName());
        }
        
        try {
            // 类型转换和反射调用 setter 方法
            Object convertedValue = convertValue(value, descriptor.getPropertyType());
            writeMethod.invoke(bean, convertedValue);
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new RuntimeException(
                "Failed to set value for property '" + propertyName + "' in " + beanClass.getName(), e);
        }
    }
    
    /**
     * 获取所有属性名
     */
    @Override
    public Set<String> getPropertyNames() {
        return propertyDescriptors.keySet();
    }
    
    /**
     * 获取属性类型
     */
    @Override
    public Class<?> getPropertyType(String propertyName) {
        PropertyDescriptor descriptor = propertyDescriptors.get(propertyName);
        return descriptor != null ? descriptor.getPropertyType() : null;
    }
    
    /**
     * 值类型转换
     */
    private Object convertValue(Object value, Class<?> targetType) {
        if (value == null) {
            return null;
        }
        
        // 如果类型匹配,直接返回
        if (targetType.isInstance(value)) {
            return value;
        }
        
        // 使用配置的转换器进行类型转换
        return config.getTypeConverter().convert(value, targetType);
    }
}

我写了一个便于理解的例子

java 复制代码
public class Test {
    public static void main(String[] args) throws Exception {
        List<BaseUser> list =new ArrayList<>();
        //添加模拟数据
        for(int i=0;i<=10000000;i++){
            BaseUser baseUser1=new BaseUser();
            baseUser1.setActiveFlag(i+"");
            list.add(baseUser1);
        }
        long startTime = System.currentTimeMillis();
           /**
         * @description 原来的反射方式 
         */
        for(BaseUser item:list){
            Class cl=item.getClass();
            Field nameField=cl.getDeclaredField("activeFlag");
            nameField.setAccessible(true);
            nameField.get(item);
        }
        //打印耗时时间
        long endTime = System.currentTimeMillis();
        long executionTime = endTime - startTime;
        System.out.println("原来的反射方法执行时间:" + (executionTime) + "毫秒");
        long startTime1 = System.currentTimeMillis();

         /**
         * @description 原来的反射方式 
         */
        Class cl =BaseUser.class;
        Method method= cl.getMethod("getActiveFlag");
        for(BaseUser item:list){
            method.invoke(item);
        }
        //打印耗时时间
        long endTime1 = System.currentTimeMillis();
        long executionTime1 = endTime1 - startTime1;
        System.out.println("修改后的反射方法执行时间:" + (executionTime1) + "毫秒");
    }
}

执行结果

java 复制代码
原来的反射方法执行时间:1041毫秒
修改后的反射方法执行时间:170毫秒

四、总结

1、本地优化了sql和结果集中字段国际化逻辑,在本地导出速度得到很大的提高,但是发版到生产环境后,数据写入excel逻辑是耗时的主要原因,通过arthas实际的监控了每个方法的耗时情况,方便线上问题解决。

2、对于本地和线上的差异,我猜测是线上和本地jvm版本与启动参数不一样导致差异。

相关推荐
多云几多3 小时前
Yudao单体项目 springboot Admin安全验证开启
java·spring boot·spring·springbootadmin
摇滚侠4 小时前
Spring Boot 3零基础教程,Spring Intializer,笔记05
spring boot·笔记·spring
兮动人5 小时前
Spring Bean耗时分析工具
java·后端·spring·bean耗时分析工具
MESSIR225 小时前
Spring IOC(控制反转)中常用注解
java·spring
我命由我123456 小时前
Excel - Excel 列出一列中所有不重复数据
经验分享·学习·职场和发展·word·powerpoint·excel·职场发展
Lucky GGBond9 小时前
Vue + Spring Boot 实现 Excel 导出实例
vue.js·spring boot·excel
笨笨狗吞噬者9 小时前
【uniapp】体验优化:开源工具集 uni-toolkit 发布
性能优化·微信小程序·uni-app
hello 早上好10 小时前
深入 Spring 条件化配置底层:从硬编码到通用注解的实现原理
java·后端·spring
亚林瓜子10 小时前
Spring中Date日期序列化与反序列化中格式设置
java·后端·spring·jackson·date