您当前的位置: 首页 >  ar

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

阿里云开发者 发布时间:2021-07-01 11:25:39 ,浏览量:3

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

背景介绍

项目中有使用到 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

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

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

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

代码很简单,从代码中可以很清晰的看到如果没有从 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 文件在解析时都会抛错。

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)。

执行过程

动手写一个 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 方法。


        
            
                org.apache.maven.plugins
                maven-jar-plugin
                2.3.1
                
                    
                        
                            true
                        
                        
                            com.tom.mdc.AgentMain
                            true
                            true
                        
                    
                
            
        
    

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);
    }
}

原文链接:https://developer.aliyun.com/article/784923?

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。
关注
打赏
1688896170
查看更多评论

阿里云开发者

暂无认证

  • 3浏览

    0关注

    2335博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文
立即登录/注册

微信扫码登录

0.0489s