greys在线诊断工具的主要实现

greys是一个使用java management tool进程注入javaagent实现在线系统的诊断一个工具。原github为(https://github.com/oldmanpushcart/greys-anatomy),其主要的功能在于系统不停机的情况下。可以查看系统中的线程信息,cpu使用情况,jmx信息,以及某个方法在运行时的调用栈,调用参数等。

一个典型的场景就是线上某个功能出bug,但是系统中并没有记录参数信息,这时候即可通过这个功能注入agent,临时地打印出这个调用方法的参数,以方便定位相应的问题。如无此工具,则只能改代码,然后重新上线。在这种情况下,可能出错的场景就不能再复现。(当然也有其它工具(如log monitor)作到在线系统参数记录开关的目的,这里不作描述)

本文主要描述greys是如何工作的,包括如何注入到在线系统,然后脚本client与注入后server端的交互,以及如何实现一个简单的参数拦截记录功能。从整个实现机制层面描述其工作原理。

继续阅读“greys在线诊断工具的主要实现”

tomcat如何访问jndi信息

在上一篇我们知道tomcat在启动时会解析context.xml并将相应的数据源信息解析到上下文中。那么接下来的工作就是如何去访问这个数据源信息了。我们访问数据源信息是通过以下代码进行访问的 

InitialContext initialContext = new InitialContext();
	DataSource dataSource = (DataSource) initialContext.lookup("java:comp/env/jdbc/xx");

那么我们就按照这个访问思路跟踪一下代码。我们看一下InitialContext的lookup实现,如下所示:

public Object lookup(String name) throws NamingException {
	return getURLOrDefaultInitCtx(name).lookup(name);
    }

这里会取得一个UrlContext或InitContext,究竟取得哪个取决于传递的参数信息,这里我们的方法是以java:开头,即它有一个scheme_id信息,按照官方对于传递的URL字符串规则,显示如下所示(该描述在javadoc中的InitialContext中):

解析 URL 字符串时生成此策略的一个异常,如下所述。
 在将 URL 字符串(一个 scheme_id:rest_of_name 形式的 String)作为名称参数传递给任一方法时,将定位处理该方案的一个 URL 上下文工厂,并
将它用于解析该 URL。如果没有找到这样的工厂,则使用由 "java.naming.factory.initial" 指定的初始上下文。类似地,当将第一个组件是 URL 字
符串的 CompositeName 对象作为名称参数传递给任一方法时,将定位一个 URL 上下文工厂并将它用于解析第一个名称组件

因此,它将尝试找一个URLContext,而在前文中我们提到在tomcat支持命名服务中,设置了一个Context.URL_PKG_PREFIXES参数为org.apache.naming,因此,会尝试寻找一个名叫
org.apache.naming.java.javaURLContextFactory的类,而恰好存在这个类。那么InitContext将转向这个类,再从这个类找回一个UrlContext实例,具体实现如下所示:

继续阅读“tomcat如何访问jndi信息”

JVM对于不同classLoader加载的对象之间default或protected字段的访问限制

    通常对于对象的default及protected访问限制,一般的说法即为只能由同包或子类才能访问。对于同包的类,是可以访问default及protected的字段的。但如下情况除外:
    如果一个由其它途径加载的类尝试访问同一个包中由其它加载器加载的类的受保护或默认级别字段时,将产生一个IllegalAccessError错误,即不允许访问指定的字段。
    即只有相同加载器之间才可以访问受保护字段。

验证程序如下所示:类Foo

public class Foo implements IFoo {
	protected int i = 2;
}

类Foo2将要访问Foo:

public class Foo2 {
	public static void redFoo() {
		Foo f = new Foo();
		System.out.println(f.i);
	}
}

    以下为执行验证的代码:

		MyClassLoader2 myClassLoader2 = new MyClassLoader2(T.class.getClassLoader());
		Class<?> clazz = myClassLoader2.loadClass("m_ylf.study.java.classLoad.Foo2");
//		Class<?> clazz2 = myClassLoader2.loadClass("m_ylf.study.java.classLoad.Foo");
		clazz.getDeclaredMethod("redFoo").invoke(null);

    在以上代码中,将由类加载器单独加载Foo2,而Foo仍由appClassLoader来加载。上面的程序即通过调用Foo2的redFoo方法来读取Foo类的受保护字段,并输出信息。执行结果如下所示:

Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
......
Caused by: java.lang.IllegalAccessError: tried to access field com.m_ylf.study.java.classLoad.Foo.i from class com.m_ylf.study.java.classLoad.Foo2
	at com.m_ylf.study.java.classLoad.Foo2.redFoo(Foo2.java:8)
	... 11 more

    即不能访问该字段,需要注意到的是,在以上的代码中,我们专门注释了使用myClassLoader加载Foo的代码。如果我们取消注释,即也叫myClassLoader来加载Foo,那么结果即能够正确的输出相对应的数字(具体输出结果就不再列出,可以单独测试)。
    需要注意的是,如果Foo中的字段i,本身就是public类型的,则该错误也不会发生。即这个约束只限制在default以及protected字段上。
    这也是为了将一个恶意的代码,加载到虚拟机,来尝试访问本来不能够被访问的信息,如标准java API中的受保护信息(标准API是由java 系统加载器来加载的)。
    以上的叙述取自《深入JVM虚拟机》,由笔者验证得出。

继续阅读“JVM对于不同classLoader加载的对象之间default或protected字段的访问限制”

解决”java.lang.UnsatisfiedLinkError: Native Library .dll already loaded in another classloader”的问题

    笔者在前段时间碰到这么一种情况,即在两个项目中使用了相同的applet,然后在applet中调用了dll操作(使用jni或jna),然后在客户端进行调用。实际的访问过程如下:
    首先访问项目A的一个界面,界面中调用了appletA,接着并没有关闭浏览器而直接访问项目B的界面,在界面中调用了appletB。
    appletA和appletB实际上是同一个applet,只不过这个applet使用在了两个项目中,并且两个项目均是直接进行访问。这时候在访问appletB的时候,就会出现一个错误:

xxx NOT loaded java.lang.UnsatisfiedLinkError : Native Library XXX.dll already loaded in another classloader

    如果访问从appletB到appletA,那么在访问appletA时也会出现同样的错误。
    因为,在一个标签页中,多个applet运行实际上是运行在同一个jvm上,只是加载applet时使用了不同的classLoader。因此,不管是appletA先运行还是appletB先运行,最终情况都是所依赖的dll都会被同一个jvm所加载,就会出现以上的错误了。

    在进行google之后,发现很多开发人员都碰到了同样的问题,有的是因为在同一个javaEE容器如(weblogic,jboss)中部署了两个都要访问同一个jni调用的项目,有的则是像笔者同样的经历。最后的结论即是,在一个jvm当中,是不允许加载一个dll两次的。因此,后面的jni调用时,尝试再次加载同一个dll,这时候即会报上面的错误了。因为该错误,相对应的java类肯定不能被初始化,因此相应的项目或者applet肯定启动不了了。

    解决这个问题其实很简单,将访问到jni的代码单独提取出来,并不直接让项目自身的classLoader加载,则是让其由systemLoader加载即可。一种方法就是将这部分代码,单独封装成一个jar,放到java的systemLoader可以加载的地方,如lib/ext目录下。然后,项目中仍然去调用此代码。由于访问dll的代码由systemLoader加载,因此,多个项目同时访问同一个dll时,即可避免再次加载了。因为,第二个项目在访问时,寻找到的类,已经被systemLoader加载过了,因此项目本身的classLoader就不会再去加载这个类了。

    对比,原来的appletA,appletB,修改过后就成了这样的结构:appletA,appletB,以及jniAccess.jar,其中jniAccess.jar放到jre的lib目录的ext目录下。这样,再次访问applet,就没有问题了。