JavaAgent寄生在目标进程中引起的ClassNotFoundException

javaagent,寄生,目标,进程,引起,classnotfoundexception · 浏览次数 : 115

小编点评

**问题原因:** 1. **类加载器找不到`org.apache.naming.java.javaURLContextFactory`类**。 2. **客户应用程序系统变量影响到 JavaAgent的类加载**。 **解决方案:** 1. **检查`myProps`变量的值是否已初始化。** 2. **如果`myProps`中存在`org.apache.naming.java.javaURLContextFactory`的键值,则该变量已初始化,无需再次设置。** 3. **如果`myProps`中不存在`org.apache.naming.java.javaURLContextFactory`的键值,则应该从应用程序配置文件中获取该值并将其设置到`myProps`中。** 4. **如果以上步骤无法解决问题,请检查应用程序的配置是否正确,以及是否在运行 JavaAgent时设置了正确的系统变量。**

正文

今天有解决方案部的小伙伴反映,我公司XWind产品在分析客户应用程序的潜在性能问题时,总是显现诊断任务异常,为了定位问题的根因,我们马上要求解决方案部的小伙伴提供XWind相关的日志,从日志中找到了如下报错信息:

  

可以看到Java经典的动态加载类错误,org.apache.naming.java.javaURLContextFactory类找不到。为了能更好的说明这个问题,有必要先介绍一下与问题相关的、XWind产品局部的技术架构,如下:

 

这里涉及到我们的JavaAgent和XPocket,我马上在本地的JavaAgent和XPocket项目源代码中排查了一下这个类,方法很简单,只是import一下即可。如果是我们项目本身需要依赖这个类,那么Maven项目肯定会将依赖的包都引入项目中,那问题很可能就是类加载器和相关包路径的问题导致了类找不到。但是这个类在两个项目中都找不到,那就说明这个类是目标客户应用程序中的类,我上网查了一下这个类,发现是Tomcat中的相关类,进一步确认了这不是我们项目中用到的类。

排查到这里,问题就变成了,为什么JMX突然要加载一个可能是客户应用中使用的类呢?

从上图的技术架构可以看到,JavaAgent是挂到客户应用进程上的,初步猜测是应用进程的某些配置影响到了JavaAgent。我根据抛出异常的调用栈信息在getInitialContext()方法中的672行打了断点,然后本地进行调试,代码并没有走到这个方法,那么肯定是客户机器上的配置影响到了JavaAgent代码的执行流程。getInitialContext()方法的实现如下:

看第672行代码,调用loadClass()方法加载className,毫无疑问,这里className变量的值肯定是org.apache.naming.java.javaURLContextFactory。那么这个className的值从哪里读取的呢?通过IDEA的Find Usages命令查找调用者,只有一个调用者。这个调用者的实现如下:

 protected Context getDefaultInitCtx() throws NamingException{
        if (!gotDefault) {
            defaultInitCtx = NamingManager.getInitialContext(myProps);
            gotDefault = true;
        }
        if (defaultInitCtx == null)
            throw new NoInitialContextException();

        return defaultInitCtx;
}

现在我们的任务就是追踪myProps变量了,这个变量的定义如下:

protected Hashtable<Object,Object> myProps = null;

同样通过IDEA的Find Usages命令查找使用情况,结果如下图所示。

 

在InitialContext类的addToEnvironment()方法中有唯一的put()方法,打断点后在本地调试,没有走put()方法。

那么就只可能在给myProps赋值时就已经含有了相应的值, 查看上图的init()方法,实现如下:

 

protected void init(Hashtable<?,?> environment)
        throws NamingException
{
        myProps = (Hashtable<Object,Object>)
                ResourceManager.getInitialEnvironment(environment);

        // ...
}

myProps是调用ResourceManager.getInitialEnvironment()方法获取的值,于是我又查看了这个被调用的方法的实现,如下:

public static Hashtable<?, ?> getInitialEnvironment(Hashtable<?, ?> env)
            throws NamingException
{
        String[] props = VersionHelper.PROPS;
        if (env == null) {
            env = new Hashtable<>(11);
        }

        String[] jndiSysProps = helper.getJndiProperties();
        for (int i = 0; i < props.length; i++) {
            Object val = env.get(props[i]);
            if (val == null) {
                val = (jndiSysProps != null)
                        ? jndiSysProps[i]
                        : helper.getJndiProperty(i);
            }
            if (val != null) {
                // 1、放入了额外的值
                ((Hashtable<String, Object>)env).put(props[i], val); 
            }
        }

        // ...

        // 2、将getApplicationResources()方法获取的值存储到env中
        mergeTables((Hashtable<Object, Object>)env, getApplicationResources()); 
        return env;
}

代码虽然有些长,但是逻辑很简单,就是汇总值,将key-value对放到最终的Hashtable中返回。如上代码有2处地方可能为最终的env放入了值。在看第一处调用put()方法时,放到的键的名称来自VersionHelper.PROPS,而值来自于getJndiProperties()方法,查看这个方法,实现代码如下:

 String[] getJndiProperties() {
        Properties sysProps = AccessController.doPrivileged(
            new PrivilegedAction<Properties>() {
                public Properties run() {
                    try {
                        return System.getProperties();
                    } catch (SecurityException e) {
                        return null;
                    }
                }
            }
        );
        if (sysProps == null) {
            return null;
        }
        String[] jProps = new String[PROPS.length];
        for (int i = 0; i < PROPS.length; i++) {
            jProps[i] = sysProps.getProperty(PROPS[i]);
        }
        return jProps;
}

从这里能看出,这里的值全部来自系统变量,而系统变量的key来自PROPS。PROPS的定义如下:

static final String[] PROPS = new String[]{
        javax.naming.Context.INITIAL_CONTEXT_FACTORY,
        // ...
};

其中javax.naming.Context.INITIAL_CONTEXT_FACTORY属性的值为java.naming.factory.initial。

联系了解决方案部的小伙伴,在目标客户机上执行命令jinfo -sysprop 客户应用进程pid,导出了所有的系统配置,其中有个系统变量的定义马上引起了我的注意,如下:

在本地启动应用挂上我们自己的JavaAgent后查看系统变量,没有发现这个变量,于是我用阿里的Arthas为本地系统配置了如上的系统变量,启动时报出了同样的错误,现在终于确定是客户应用程序的系统变量影响到我们的JavaAgent了。

到目前为止,引起问题的原因找到了,那么还有一个问题,为什么在目标客户机就找不到这个类呢?由于Tomcat自定义了类加载器来加载自己的类,而JavaAgent通常是由应用类加载器加载的,所以找不到也是理所当然了。

现在问题找到了,那么如何解决这个问题呢? 之前我们客户端连接JMX的代码如下:

String jmxURL = String.format("service:jmx:rmi:///jndi/rmi://localhost:%s/server", 1099);
JMXServiceURL url = new JMXServiceURL(jmxURL);

JMXConnector jmxConnector = JMXConnectorFactory.connect(url);

MBeanServerConnection msc = jmxConnector.getMBeanServerConnection();
if(!msc.isRegistered(new ObjectName("XPocket:name=xpocket"))){
    System.out.println("连接出错");
}

查找了相关的API,发现connect()还有一个重载方法,可用来传递自定义的变量,于是修改后的代码如下:

String jmxURL = String.format("service:jmx:rmi:///jndi/rmi://localhost:%s/server", 1099);
JMXServiceURL url = new JMXServiceURL(jmxURL);

Map<String,Object> m = new HashMap<>();
m.put("java.naming.factory.initial","com.sun.jndi.rmi.registry.RegistryContextFactory");
JMXConnector jmxConnector = JMXConnectorFactory.connect(url,m);

MBeanServerConnection msc = jmxConnector.getMBeanServerConnection();
if(!msc.isRegistered(new ObjectName("XPocket:name=xpocket"))){
    System.out.println("连接出错");
}  

这样就能使用我们自定义的java.naming.factory.initial变量值了,同时也不会影响到客户应用程序的配置。

其实经常会遇到ClassNotFoundException或MethodNotFoundException等这类问题,归根结底是因为动态类加载造成的,动态类加载在实现AOP、优化反射调用速度、实现动态代理等方面发挥了巨大的作用,这可能是它的一些小小的副作用吧。

 

本人最近准备出一个手写Hotspot VM的课程,超级硬核,从0开始写HotSpot VM,将HotSpot VM所有核心的实现全部走一遍,如感兴趣,速速入群。

群里可讨论虚拟机和Java性能剖析与故障诊断等话题,欢迎加入。

  

 

 

  

 

  

 

与JavaAgent寄生在目标进程中引起的ClassNotFoundException相似的内容:

JavaAgent寄生在目标进程中引起的ClassNotFoundException

今天有解决方案部的小伙伴反映,我公司XWind产品在分析客户应用程序的潜在性能问题时,总是显现诊断任务异常,为了定位问题的根因,我们马上要求解决方案部的小伙伴提供XWind相关的日志,从日志中找到了如下报错信息: 可以看到Java经典的动态加载类错误,org.apache.naming.java.j

skywalking

-javaagent:skywalking-agent.jar -Dskywalking.agent.service_name=master5202 -Dskywalking.collector.backend_service=127.0.0.1:11800 export JAVA_HOME=/sk

Sermant类隔离架构:解决JavaAgent场景类冲突的实践

Sermant是基于Java字节码增强技术的无代理服务网格,其利用Java字节码增强技术为宿主应用程序提供服务治理功能。

如何利用动态配置中心在JavaAgent中实现微服务的多样化治理

Sermant的动态配置模型不仅可以实现了微服务的动态治理,还可以提高配置的可维护性以及可读性,帮助用户更方便地进行微服务治理和运维操作。

记一次多个Java Agent同时使用的类增强冲突问题及分析

摘要:Java Agent技术常被用于加载class文件之前进行拦截并修改字节码,以实现对Java应用的无侵入式增强。 本文分享自华为云社区《记一次多个JavaAgent同时使用的类增强冲突问题及分析》,作者:Vansittart。 问题背景 Java Agent技术常被用于加载class文件之前进

云原生微服务治理技术朝无代理架构的演进之路

摘要:本文基于对微服务治理技术从SOA, 微服务框架,到云原生架构的历史发展总结,提出了一种新的基于Javaagent技术的新一代无代理架构的服务治理技术,并介绍了其相关的代表性开源项目Sermant。 本文分享自华为云社区《云原生微服务治理技术朝无代理架构的演进之路》,作者: 杨奕|华为云技术规划

Java Agent场景性能测试分析优化经验分享

摘要:本文将以Sermant的SpringBoot 注册插件的性能测试及优化过程为例,分享在Java Agent场景如何进行更好的性能测试优化及在Java Agent下需要着重注意的性能陷阱。 作者:栾文飞 高级软件工程师 一、背景介绍 Sermant是一个主打服务治理领域的Java Agent框架

[转帖]Spring Cloud 整合 SkyWalking

https://www.jianshu.com/p/e81e35dc6406 Java Agent 服务器探针 探针,用来收集和发送数据到归集器。参考官网给出的帮助 Setup java agent,我们需要使用官方提供的探针为我们达到监控的目的,按照实际情况我们需要实现三种部署方式 IDEA 部署

[转帖]perf-map-agent

https://github.com/jvm-profiling-tools/perf-map-agent A java agent to generate /tmp/perf-.map files for just-in-time(JIT)-compiled methods for us

[转帖]perf-map-agent

https://github.com/brendangregg/perf-map-agent A java agent to generate /tmp/perf-.map files for just-in-time(JIT)-compiled methods for use with