欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

java项目源码哪里找(java初学者练手项目)

程序员文章站 2023-11-17 08:48:10
1. 前言为什么会接触javaagent呢?这起源于笔者最近在读dubbo的源码,dubbo有一个很有意思的功能——spi,它可以根据运行时的uri参数,自适应的调用特定的实现类。大致的原理其实也能猜...

1. 前言

为什么会接触javaagent呢?

这起源于笔者最近在读dubbo的源码,dubbo有一个很有意思的功能——spi,它可以根据运行时的uri参数,自适应的调用特定的实现类。大致的原理其实也能猜到,无非就是生成一个代理类,反射解析uri参数里的值,然后再调用对应的实现类。虽然大概可以猜到实现原理,但毕竟只是猜想,抱着科学严谨的精神,还是想看看dubbo的实现源码,此时就有了一个想法,能不能把dubbo生成的代理对象的class类dump下来,然后反编译看看它的源码呢?

理论上是完全可行的,阿里有一个很好用的开源工具arthas,它的jad命令就支持对jvm已经加载的类进行反编译查看源码,笔者把arthas项目源码down下来了,查看以后发现,需要用到javaagent技术。

2. javaagent规范

在jdk1.5以后,我们可以使用javaagent技术,以「零侵入」的方式对java程序做增强。例如阿里云的arms应用监控服务,就可以通过javaagent的方式接入一个探针,它会把应用的运行数据上报到阿里云,开发者可以在后台查看到应用的运行数据。这种方式,不需要我们对应用做任何改动,就可以轻松实现应用监控。

javaagent是一种规范,它分为两类:主程序运行前agent、主程序运行后agent。它可以在jvm加载class文件前,对字节码做修改,甚至允许修改已经加载过的class,这样我们就可以对应用做增强、以及实现代码热部署。

主程序运行前agent的步骤:

1、编写agent类,该类必须有静态方法premain()。

public class myagentclass {

	// jvm优先执行该方法
	public static void premain(string agentargs, instrumentation inst) {
		system.err.println("main before...");
	}

	public static void premain(string agentargs) {
		system.err.println("main before...");
	}
}

2、在resources/meta-inf目录下编写manifest.mf文件,指定premain-class,然后将程序打成jar包。

manifest-version: 1.0
can-redefine-classes: true
can-retransform-classes: true
premain-class: top.javap.agent.myagentclass
// 注意,这里必须空一行

使用maven构建程序时,也可使用如下配置。

<plugin>
  <groupid>org.apache.maven.plugins</groupid>
  <artifactid>maven-jar-plugin</artifactid>
  <configuration>
    <archive>
      <manifest>
        <addclasspath>true</addclasspath>
      </manifest>
      <manifestentries>
        <premain-class>top.javap.agent.myagentclass</premain-class>
        <can-retransform-classes>true</can-retransform-classes>
        <can-redefine-classes>true</can-redefine-classes>
      </manifestentries>
    </archive>
  </configuration>
</plugin>

3、启动目标程序时,指定jvm参数,如下:

java -javaagent:agent-1.0-snapshot.jar javaapp

主程序运行后agent的步骤:

这种是针对已经运行的jvm进程,我们可以通过attach机制,启动一个新的jvm进程发送指令给它执行。

1、编写agent类,该类必须有静态方法agentmain()。

public class myagentclass {

	public static void agentmain(string agentargs, instrumentation inst) {
		system.err.println("main after...");
	}
}

2、在resources/meta-inf目录下编写manifest.mf文件,指定premain-class,然后将程序打成jar包。

manifest-version: 1.0
can-redefine-classes: true
can-retransform-classes: true
agent-class: top.javap.agent.myagentclass
// 注意,这里必须空一行

3、编写attach程序,启动并attach到目标jvm进程。

public static void main(string[] args) throws exception {
    virtualmachine vm = virtualmachine.attach("8080");
    vm.loadagent("/dev/agent.jar");
}

3. 相关组件

3.1 instrumentation

编写的agentclass类必须有premain()方法,其中一个比较重要的参数就是instrumentation。它是javaagent技术用到的主要api,接口定义如下:

public interface instrumentation {
    /**
	 * 添加class文件转换器,底层采用数组存储
	 * jvm加载class文件前,需要依次经过转换
	 * @param transformer
	 * @param canretransform 是否允许转换
	 */
    void addtransformer(classfiletransformer transformer, boolean canretransform);
    void addtransformer(classfiletransformer transformer);

    // 删除class文件转换器
    boolean removetransformer(classfiletransformer transformer);
    boolean isretransformclassessupported();

    // 重新转换class
    void retransformclasses(class<?>... classes) throws unmodifiableclassexception;
    boolean isredefineclassessupported();

    // 重新定义class,热更新
    void redefineclasses(classdefinition... definitions)
        throws  classnotfoundexception, unmodifiableclassexception;
    boolean ismodifiableclass(class<?> theclass);
    @suppresswarnings("rawtypes")
    class[] getallloadedclasses();
    @suppresswarnings("rawtypes")
    class[] getinitiatedclasses(classloader loader);
    // 获取对象大小
    long getobjectsize(object objecttosize);
    void appendtobootstrapclassloadersearch(jarfile jarfile);
    void appendtosystemclassloadersearch(jarfile jarfile);
    boolean isnativemethodprefixsupported();
    void setnativemethodprefix(classfiletransformer transformer, string prefix);
}

重要的方法笔者已经写上注释了,本文会用到的方法主要是addtransformer()。它可以用来添加class转换器,jvm在加载class前,会先经过这些转换器进行加工。

3.2 classfiletransformer

class文件转换器,jvm加载某个class前,会先经过它转换,我们可以在这里去修改字节码以达到功能增强的目的。它只有一个方法transform():

public interface classfiletransformer{
    
    /**
	 * 转换class
	 * @param loader 类加载器
	 * @param classname 类名
	 * @param classbeingredefined 原始class
	 * @param protectiondomain 
	 * @param classfilebuffer class文件字节数组
	 */
	byte[] transform(  classloader loader,
                string classname,
                class<?>  classbeingredefined,
                protectiondomain protectiondomain,
                byte[]  classfilebuffer)
        throws illegalclassformatexception;
}

本文主要用到的就是classfilebuffer,有了class的字节数组,只要把它导出到磁盘,通过idea反编译就能看到源码了。

4. 实战

【需求】

支持将任意java对象的class文件导出到磁盘,通过反编译查看源码,包括动态生成的类。

【实现】

1、编写instrumentationholder,持有instrumentation实例,后续操作全靠它。

public class instrumentationholder {
	private static instrumentation instance;

	public static void init(instrumentation ins) {
		instance = ins;
	}

	public static instrumentation get() {
		if (instance == null) {
			throw new runtimeexception("检查 -javaagent 配置");
		}
		return instance;
	}
}

2、编写myagentclass,保存instrumentation实例。

public class myagentclass {

	public static void premain(string agentargs, instrumentation inst) {
		system.err.println("main before...");
		instrumentationholder.init(inst);
	}
}

3、编写classdumptransformer,获取class文件字节数组,导出到磁盘。

public class classdumptransformer implements classfiletransformer {
	private final file file;
	private final set<class<?>> classes = new hashset<>();

	public classdumptransformer(string path, class<?>... classes) {
		this.file = new file(path);
		this.classes.addall(arrays.aslist(classes));
	}

	@override
	public byte[] transform(classloader loader, string classname, class<?> classbeingredefined, protectiondomain protectiondomain, byte[] classfilebuffer) throws illegalclassformatexception {
		if (classes.contains(classbeingredefined)) {
			fileutil.writebytes(classfilebuffer, file);
		}
		return null;
	}
}

4、编写classutil工具类,支持导出class文件。

public class classutil {

	public static void classdump(class<?> c, string path) {
		classdumptransformer transformer = new classdumptransformer(path, c);
		instrumentation inst = instrumentationholder.get();
		inst.addtransformer(transformer, true);
		try {
			inst.retransformclasses(c);
		} catch (unmodifiableclassexception e) {
			e.printstacktrace();
		} finally {
			inst.removetransformer(transformer);
		}
	}
}

5、编写manifest.mf文件,构建jar包。

manifest-version: 1.0
can-redefine-classes: true
can-retransform-classes: true
premain-class: top.javap.agent.myagentclass

6、编写测试类,利用jdk动态代理生成代理类,然后将代理类的class文件导出。

public class agentdemo {
	public static void main(string[] args) throws exception {
		object instance = proxy.newproxyinstance(a.class.getclassloader(), new class[]{a.class}, new invocationhandler() {
			@override
			public object invoke(object proxy, method method, object[] args) throws throwable {
				return null;
			}
		});

		classutil.classdump(instance.getclass(),
				"/target/x.class");
	}

	public static interface a {
		void a();
	}
}

7、设置-javaagent参数并启动程序。

java -javaagent:agent.jar agentdemo

此时,target目录下就会生成x.class文件,通过idea打开即可看到jdk生成的代理类源码。

5. 总结

javaagent十分强大,通过它可以在jvm加载class文件前修改字节码,甚至修改jvm已经加载的class。基于此,我们可以「零侵入」的对应用程序做增强,服务实现热部署等等。

本文通过一个小示例,编写classfiletransformer实现类导出对象的class文件,反编译查看其源码。这对于asm操作字节码、jdk动态代理等动态生成类的场景下,而我们又想看对象的具体实现时,提供了帮助。