使用 Arthas 排查开源 Excel 组件问题

本文涉及的产品
函数计算FC,每月15万CU 3个月
应用实时监控服务ARMS - 应用监控,每月50GB免费额度
可观测监控 Prometheus 版,每月50GB免费额度
简介: 有了实际的使用之后,不免会想到,Arthas 是如何做到在程序运行时,动态监测我们的代码的呢?带着这样的问题,我们一起来看下 Java Agent 技术实现原理。

image.png

背景介绍

项目中有使用到 com.github.dreamroute excel-helper 这个工具来辅助 Excel 文件的解析,出错时的代码是这样写的:如下所示(非源代码)

     try {            excelDTOS = ExcelHelper.importFromFile(ExcelType.XLSX, file, ExcelDTO.class);        } catch (Exception e) {            log.error("ExcelHelper importFromFile exception msg {}", e.getMessage());        }

因为打印异常信息时,使用了 e.getMessage() 方法,没有将异常信息打印出来。而且本地复现也没有复现出来。所以只能考虑使用 arthas 来协助排查这个问题了。

排查过程

1、线上服务器安装 Arthas。
https://arthas.aliyun.com/doc/install-detail.html

2、使用 watch 命令监控指定方法,打印出异常的堆栈信息,命令如下:

watch com.github.dreamroute.excel.helper.ExcelHelper importFromFile '{params,throwExp}' -e -x 3

再次调用方法,捕获到异常栈信息如下:
image.png

已经捕获到异常,并打印出堆栈信息。

3、根据对应的堆栈信息,定位到具体的代码,如下:

image.png

代码很简单,从代码中可以很清晰的看到如果没有从 headerInfoMap 中没有获取到指定的 headerInfo ,就会抛这个异常。没有找到只有两种情况:

  • headerInfoMap 中保存的信息不对。
  • cell 中的 columnIndex 超出的正常的范围导致没有获取到对应 HeaderInfo 。

对于第二种情况,首先去校验了一下上传的 Excel 文件是否有问题,本地测试了一下 Excel 文件,没有任何问题。本地测试也是成功的,所以主观判断,第二种情况的可能性不大。

所以说主要检查第一种情况是否发生,这个时候可以再去看一下该方法的第一行代码

MapheaderInfoMap = processHeaderInfo(rows,cls);

可以看到headerInfoMap是通过processHeaderInfo中获取的。找到processHeaderInfo 的代码,如下所示。

public static MapproceeHeaderInfo(Iteratorrows, Class cls) {
    if (rows.hasNext()) {
        Row header = rows.next();
        return CacheFactory.findHeaderInfo(cls, header);
    }
    return new HashMap<>(0);
}
public static MapfindHeaderInfo(Class cls, Row header) {
    MapheaderInfo = HEADER_INFO.get(cls);
    if (MapUtils.isEmpty(headerInfo)) {
        headerInfo = ClassAssistant.getHeaderInfo(cls, header);
        HEADER_INFO.put(cls, headerInfo);
    }
    return headerInfo;
}
public static MapgetHeaderInfo(Class cls, Row header) {
    IteratorcellIterator = header.cellIterator();
    Listfields = ClassAssistant.getAllFields(cls);
    MapheaderInfo = new HashMap<>(fields.size());
    while (cellIterator.hasNext()) {
        org.apache.poi.ss.usermodel.Cell cell = cellIterator.next();
        String headerName = cell.getStringCellValue();
        for (Field field : fields) {
            Column col = field.getAnnotation(Column.class);
            String name = col.name();
            if (Objects.equals(headerName, name)) {
                HeaderInfo hi = new HeaderInfo(col.cellType(), field);
                headerInfo.put(cell.getColumnIndex(), hi);
                break;
            }
        }
    }

    return headerInfo;
}

主要通过 CacheFactory 类的 findHeaderInfo 来生成,在 findHeaderInfo 方法中,通过一个被 static final 修饰的 HEADER_INFO 变量来做缓存,被调用时先去HEADER_INFO 中查,如果有则直接返回,没有则重新创建(也就说明相同的 Excel 文件,仅初始化一次 HeaderInfo )。创建的步骤在 ClassAssistant.getHeaderInfo() 方法中。

简单的看一下 HeaderInfo 的生成过程,根据 Excel 文件的第一行中的各个 Cell 值与自定义实体类的注解比较,如果名字相同,就存为一个键值对( HeaderInfo 的数据结构为 HashMap )。

4、这个时候需要再确认一下 HEADER_INFO 中保存的 ExcelDTO.class 相关的 HeaderInfo 是怎样的。通过 ognl 命令或者 getstatic 命令来查看。这里使用 ognl 命令。

ognl '#value=new com.tom.dto.ExcelDTO(),#valueMap=@com.github.dreamroute.excel.helper.cache.CacheFactory@HEADER_INFO,#valueMap.get(#value.getClass()).entrySet().iterator.{#this.value.name}'

结果如下:正常情况下这个 Excel 文件有 6 列信息,为什么只产生了 4 个键值对呢?如果 HEADER_INFO 中保存了错的,从上面的逻辑来看,后面上传的正确的 Excel 文件在解析时都会抛错。

image.png

5、询问了当时发现这个问题的同事,得知他第一次上传的 Excel 文件是有问题的,后面想改正,再上传时便出现了问题。到这里问题也算是找到了。

Arthas 原理探究

有了实际的使用之后,不免会想到,Arthas 是如何做到在程序运行时,动态监测我们的代码的呢?带着这样的问题,我们一起来看下 Java Agent 技术实现原理。

Java Agent 技术

Agent 是一个运行在目标 JVM 的特定程序,它的职责是负责从目标 JVM 中获取数据,然后将数据传递给外部进程。加载 Agent 的时机可以是目标 JVM 启动之时,也可以是在目标 JVM 运行时进行加载,而在目标 JVM 运行时进行 Agent 加载具备动态性。

基础概念

  • JVMTI(JVM Tool Interface):是 JVM 暴露出来的一些供用户扩展的接口集合,JVMTI 是基于事件驱动的,JVM 每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者去扩展自己的逻辑。
  • JVMTIAgent(JVM Tool Interface):是一个动态库,利用 JVMTI 暴露出来的一些接口帮助我们在程序启动时或程序运行时 JVM Attach 机制,将 Agent 加载到目标 JVM 中。
  • JPLISAgent(Java Programming Language Instrumentation Services Agent):它的作用是初始化所有通过 Java Instrumentation API 编写的 Agent,并且也承担着通过 JVMTI 实现 Java Instrumentation 中暴露 API 的责任。
  • VirtualMachine :提供了Attach 动作和 Detach 动作,允许我们通过 attach 方法,远程连接到 JVM 上,然后通过 loadAgent 方法向 JVM 注册一个代理程序 agent ,在该 agent 的代理程序中会得到一个 Instrumentation 实例,该实例可以在 class 加载前改变 class 的字节码,也可以在 class 加载后重新加载。
  • Instrumentation:可以在 class 加载前改变 class 的字节码(premain),也可以在 class 加载后重新加载(agentmain)。

执行过程

image.png

动手写一个 Demo

通过 javassist,在运行时更改指定方法的代码,在方法之前后添加自定义逻辑。

1、定义 Agent 类。当前 Java 提供了两种方式可以将代码代码注入到 JVM 中,这里我们的 Demo 选择使用 agentmain 方法来实现。

premain:在启动时通过 javaagent 命令,将代理注入到指定的 JVM 中。
agentmain:运行时通过 attach 工具激活指定代理。

/**
 * AgentMain
 *
 * @author tomxin
 */
public class AgentMain {

    public static void agentmain(String agentArgs, Instrumentation instrumentation) throws UnmodifiableClassException, ClassNotFoundException {
        instrumentation.addTransformer(new InterceptorTransformer(agentArgs), true);
        Class clazz = Class.forName(agentArgs.split(",")[1]);
        instrumentation.retransformClasses(clazz);
    }
}

/**
 * InterceptorTransformer
 *
 * @author tomxin
 */
public class InterceptorTransformer implements ClassFileTransformer {

    private String agentArgs;

    public InterceptorTransformer(String agentArgs) {
        this.agentArgs = agentArgs;
    }

    @Override
    public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        //javassist的包名是用点分割的,需要转换下
        if (className != null && className.indexOf("/") != -1) {
            className = className.replaceAll("/", ".");
        }
        try {
            //通过包名获取类文件
            CtClass cc = ClassPool.getDefault().get(className);
            //获得指定方法名的方法
            CtMethod m = cc.getDeclaredMethod(agentArgs.split(",")[2]);
            //在方法执行前插入代码
            m.insertBefore("{ System.out.println(\"=========开始执行=========\"); }");
            m.insertAfter("{ System.out.println(\"=========结束执行=========\"); }");
            return cc.toBytecode();
        } catch (Exception e) {

        }
        return null;
    }
}

2、使用 Maven 配置 MANIFEST.MF 文件,该文件能够指定 Jar 包的 main 方法。

<build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>2.3.1</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Agent-Class>com.tom.mdc.AgentMain</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

3、定义 Attach 方法,通过 VirtualMachine.attach(#{pid}) 来指定要代理的类。

import com.sun.tools.attach.VirtualMachine;

import java.io.IOException;

/**
 * AttachMain
 *
 * @author tomxin
 */
public class AttachMain {
    public static void main(String[] args) {
        VirtualMachine virtualMachine = null;
        try {
            virtualMachine = VirtualMachine.attach(args[0]);
            // 将打包好的Jar包,添加到指定的JVM进程中。
            virtualMachine.loadAgent("target/agent-demo-1.0-SNAPSHOT.jar",String.join(",", args));
        } catch (Exception e) {
            if (virtualMachine != null) {
                try {
                    virtualMachine.detach();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
}

4、定义测试的方法

package com.tom.mdc;
import java.lang.management.ManagementFactory;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * PrintParamTarget
 *
 * @author toxmxin
 */
public class PrintParamTarget {

    public static void main(String[] args) {
        // 打印当前进程ID
        System.out.println(ManagementFactory.getRuntimeMXBean().getName());
        Random random = new Random();
        while (true) {
            int sleepTime = 5 + random.nextInt(5);
            running(sleepTime);
        }
    }

    private static void running(int sleepTime) {
        try {
            TimeUnit.SECONDS.sleep(sleepTime);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("running sleep time " + sleepTime);
    }
}
相关文章
|
6月前
|
easyexcel Java 测试技术
读取Excel还用POI?试试这款开源工具EasyExcel
读取Excel还用POI?试试这款开源工具EasyExcel
172 0
|
Arthas 监控 Java
开源Java诊断工具Arthas:开篇之watch实战
还在为排查Java程序线上问题头痛吗,看我们用阿里开源的诊断神器 Arthas 来帮您
506 1
|
Arthas 测试技术
Arthas排查生产环境CPU飚高问题
Arthas排查生产环境CPU飚高问题
158 0
Arthas排查生产环境CPU飚高问题
|
Arthas 监控 Java
Jvm性能调优+监控工具Arthas【阿里开源】
Jvm性能调优+监控工具Arthas【阿里开源】
990 0
VS2005(excel2007)利用Automation(OLD Automation)方法。将Excel当成组件服务器的编译错误 我的系统盘和office都装在d盘。 自动生成的import有问题
VS2005(excel2007)利用Automation(OLD Automation)方法。将Excel当成组件服务器的编译错误 我的系统盘和office都装在d盘。 自动生成的import有问题
|
前端开发 C#
C# 基于NPOI+Office COM组件 实现20行代码在线预览文档(word,excel,pdf,txt,png)
C# 基于NPOI+Office COM组件 实现20行代码在线预览文档(word,excel,pdf,txt,png)
|
4月前
|
Web App开发 前端开发 安全
2024年新一代WebOffice内嵌网页组件,Web网页在线编辑Word/Excel/PPT
WebOffice控件面临兼容性、用户体验和维护难题。随着浏览器更新,依赖插件的技术不再适用,如Chrome不再支持NPAPI和PPAPI。产品普遍不支持多版本Office并存,定制能力弱,升级复杂。猿大师办公助手提供了解决方案,它兼容多种浏览器,包括最新版和国产浏览器,不依赖插件,支持文档对比,具有丰富的功能和接口,兼容多种Office版本,允许源码级定制,提供终身技术支持,并实现静默在线升级。适用于多种行业和操作系统。
224 6
|
4月前
|
存储 对象存储 Python
`openpyxl`是一个用于读写Excel 2010 xlsx/xlsm/xltx/xltm文件的Python库。它不需要Microsoft Excel,也不需要.NET或COM组件。
`openpyxl`是一个用于读写Excel 2010 xlsx/xlsm/xltx/xltm文件的Python库。它不需要Microsoft Excel,也不需要.NET或COM组件。
|
6月前
|
JavaScript
【sgExcelGrid】自定义组件:简单模拟Excel表格拖拽、选中单元格、横行、纵列、拖拽圈选等操作
【sgExcelGrid】自定义组件:简单模拟Excel表格拖拽、选中单元格、横行、纵列、拖拽圈选等操作
【sgExcelGrid】自定义组件:简单模拟Excel表格拖拽、选中单元格、横行、纵列、拖拽圈选等操作
|
6月前
|
人工智能 自然语言处理 前端开发
Python 潮流周刊#17:Excel 终于支持 Python 了、Meta 重磅开源新项目、Mojo 新得 1 亿美元融资
Python 潮流周刊#17:Excel 终于支持 Python 了、Meta 重磅开源新项目、Mojo 新得 1 亿美元融资
75 1