代理-JDK动态代理原理:$Proxy

发表于 2016-09-20 23:36 显示全部楼层 17 2526

本帖最后由 叶星飞 于 2016-9-20 23:44 编辑

接上一篇代理-动态代理(jdk自带),动态代理区别于静态代理,只有在程序运行的时候才动态拼接出代理类,那么问题来了,java是如何生成动态代理类的?动态代理生成类结构是怎样?这2个问题将在本篇跟下一篇回答。本篇先回答第二个问题:动态代理生成类结构是怎样?


修改一下上篇App中的代码:

//测试类
public class App {
	public static void main(String[] args) {
		StudentA studentA = new StudentA();
		StudentB studentB = new StudentB();
		
		GivingInvocationHandler handler = new GivingInvocationHandler(studentA, studentB);
		Giving proxy = handler.getProxyInstance();
		//添加一行代码:打印代理类proxy类型
		System.out.println(proxy.getClass());
		proxy.giveFlower();
		proxy.giveGift();
	}
}

输出:

blob.png

观察观察proxy对象的getClass类型,明显proxy不是我们代码生成,而是jvm根据一定规则,动态生成的类:$Proxy0。问题又来了,怎获取这个类?


操作前,先科普些知识

javaagent

   java 代理机制,注意此代理非彼代理,此处只需知道它能完成下列功能便可:在jvm启动后加载class文件之前实现拦截,可对字节码进行自定义修改。本篇中获取$Proxy代码就全靠他了


ClassFileTransformer

   一个提供此接口的实现以转换类文件的代理。转换在 JVM 定义类之前发生。 


具体操作:

1、编写一个java agent代理类,实现ClassFileTransformer接口,在main方法执行前拦截,并截取$Proxy 代码

2、反编译$Proxy代码(注意:截取到的代码是class文件)

3、分析$Proxy代码

import java.io.File;
import java.io.FileOutputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

/**
 * java agent 类
 * 实现ClassFileTransformer接口,在main方法执行前拦截,并截取$Proxy0 代码
 */
public class ProxyClassAgent implements ClassFileTransformer {
	//保存class文件目录路径
	private String dirPath;

	public ProxyClassAgent(String dirPath) {
		this.dirPath = dirPath;
	}

	/**
	 * ClassFileTransformer接口唯一方法,此方法的实现可以转换提供的类文件,并返回一个新的替换类文件。 
	 *
	 * @param loader - 定义要转换的类加载器;如果是引导加载器,则为 null
	 * @param className - 完全限定类内部形式的类名称和 The Java Virtual Machine Specification 中定义的接口名称。
	 * 			例如,"java/util/List"。
	 * @param classBeingRedefined - 如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null
	 * @param protectionDomain - 要定义或重定义的类的保护域
	 * @param classfileBuffer - 类文件格式的输入字节缓冲区(不得修改)
	 * 
	 */
	public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
			ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

		//因为这里拦截的是loader加载所有class文件,只能通过粗略过滤,无法精确过滤
		//这里使用className中包含"$Proxy"的类名,
		//原因:java 代理动态代理类,命名规则:$ProxyX  X 表示个数从0开始
		if (className.contains("$Proxy")) {
			//使用IO流将classfileBuffer保存到文件中
			//需要截取操作,原因:className 是以反斜杠命名的,而不是. 而且不带.class后缀 例如:java/util/List
			int lastIndexOf = className.lastIndexOf("/") + 1;
			String fileName = className.substring(lastIndexOf) + ".class";
			saveClassFile(dirPath, fileName, classfileBuffer);
			System.out.println("导出成功....");
		}
		//只需要获取$Proxy的字节码,不需要对其代码修改,故原样返回
		return classfileBuffer;
	}

	/**
	 * 使用IO流将classfileBuffer保存到文件中
	 * @param dirPath   文件目录
	 * @param fileName  文件名
	 * @param data      字节码数据
	 */
	private void saveClassFile(String dirPath, String fileName, byte[] data) {
		try {
			File file = new File(dirPath + fileName);
			if (!file.exists()) {
				file.createNewFile();
			}
			FileOutputStream fos = new FileOutputStream(file);
			fos.write(data);
			fos.close();
		} catch (Exception e) {
			System.out.println("exception occured while doing some file operation");
			e.printStackTrace();
		}
	}

	/**
	 * premain方法:在main方法之前前执行
	 * 
	 * 注意:
	 * 普通类成为 java agent 必须实现以下2个方法中的一个:
	 * 1、public static void premain(String agentArgs, Instrumentation inst) {}
	 * 2、public static void premain(String agentArgs) {}
	 * 
	 * @param agentArgs  保存class文件目录路径, 代码执行是需要特别指定,否则默认到根目录 /
	 * @param inst 提供检测 Java 编程语言代码所需的服务 的类。
	 * 		       当 JVM 以指示一个代理类的方式启动时,将传递给代理类的 premain 方法一个 Instrumentation 实例。 
	 */
	public static void premain(String agentArgs, Instrumentation inst) {

		if (agentArgs == null || "".equals(agentArgs.trim())) {
			agentArgs = "/";
		}
		inst.addTransformer(new ProxyClassAgent(agentArgs));
	}
}

好,到这,java agent类就搞掂了,接下来就是怎么让这个运行起来:

第一步:打包,将ProxyClassAgent 打成jar包

blob.png

注意点①

导出jar包只需要选出ProxyClassAgent即可,其他的类不要选

注意点②

选择jar存放的位置,jar的名字随便起,最好是英文名

注意点③

生成jar包后,需要修改jar中的MANIFEST.MF文件,故需要使用解压方式打开

注意点④

注意路径:proxyClassAgent.jar\META-INF\MANIFEST.MF

注意点⑤

在MANIFEST.MF文件中添加这句话 Premain-Class: com.langfei.proxy.jdk.demo1.ProxyClassAgent

注意:

1、Premain-Class: 后面有个一个空格,

2、后面带的类是我们编写的ProxyClassAgent 全限类名。


第二步:运行

在eclipse中运行:

blob.png

注意点①

将打包好jar放值某个位置,运行时候要引用,这里为了方便,我直接放在项目的lib包中

注意点②

找到测试App.java 右键Run As --> Run Configurations

注意点③

选择tab中Arguments

注意点④

在VM arguments 栏输入

-javaagent:lib/proxyClassAgent.jar=E:/aa/

-javaagent 这是命令,必须要写

lib/proxyClassAgent.jar 这是上面弄出来的jar

E:/aa/  这是class文件生成的文件目录路径

点运行后出现:

blob.png

如果在cmd 使用命令执行

blob.png

1、java运行命令

2、指定的jar包及生成class文件目录路径

3、要运行的java代码App 注意需要全限类名


第三步:反编译$Proxy0.class

这里使用XJad2.2反编译,整理后的源码

import com.langfei.proxy.jdk.demo1.Giving;
import java.lang.reflect.*;

//很明显,$Proxy0实现了Giving接口
public final class $Proxy0 extends Proxy implements Giving {

	private static Method m0;
	private static Method m1;
	private static Method m2;
	private static Method m3;  //Giving.giveFlower  这个方法的对象
	private static Method m4;  //Giving.giveGift 这个方法的对象
	
	static {
		try {
			m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
			m1 = Class.forName("java.lang.Object").getMethod("equals",
					new Class[] { Class.forName("java.lang.Object") });
			m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
			//通过全限类名直接反射生成Giving.giveFlower/giveGift 方法对象
			m3 = Class.forName("com.langfei.proxy.jdk.demo1.Giving").getMethod("giveFlower", new Class[0]);
			m4 = Class.forName("com.langfei.proxy.jdk.demo1.Giving").getMethod("giveGift", new Class[0]);
		} catch (NoSuchMethodException nosuchmethodexception) {
			throw new NoSuchMethodError(nosuchmethodexception.getMessage());
		} catch (ClassNotFoundException classnotfoundexception) {
			throw new NoClassDefFoundError(classnotfoundexception.getMessage());
		}
	}

	//构造方法,传入InvocationHandler 对象,这里对应 GivingInvocationHandler.getProxyInstance 方法中
	//return (T) Proxy.newProxyInstance(realObject.getClass().getClassLoader(),	realObject.getClass().getInterfaces(), this);
	//的this, 这里下一篇再详细介绍
	public $Proxy0(InvocationHandler invocationhandler) {
		super(invocationhandler);
	}
	
	//代理类的重写Giving接口的方法
	public final void giveFlower(){
		try {
			//通过传进来的invocationhandler 对象,执行代理方法:
			//这里实际调用GivingInvocationHandler.invoke 方法
			super.h.invoke(this, m3, null);
			return;
		} catch (Throwable e) {
			e.printStackTrace();
		}
	}

	public final void giveGift()
	{
		try {
			super.h.invoke(this, m4, null);
			return;
		} catch (Throwable e) {
			e.printStackTrace();
		}
	}
}

总结:

java动态代理相比静态代理实现的逻辑麻烦多了,但是,这些相对于我们来说透明的,jvm自动帮我们实现好了,我们直接拿来用就好,本篇文章,看看就好,仅当坎大山时的谈资即可。


好了,至此,本篇完。








回复 使用道具
举报
甜甜车

发表于 2017-02-09 12:09 显示全部楼层

无需回复的,我们保持沉默,如需回复的,就回21个字!

回复 支持 反对 使用道具
举报
甜甜车

发表于 2017-02-09 05:34 显示全部楼层

前排支持!!

回复 支持 反对 使用道具
举报
宁静致远

发表于 2017-02-08 18:53 显示全部楼层

回复 支持 反对 使用道具
举报
diming520

发表于 2017-02-08 17:20 显示全部楼层

回复 支持 反对 使用道具
举报
PPAP!

发表于 2017-02-08 16:38 显示全部楼层

观后纵想法再多,也不如一句回复实在!

回复 支持 反对 使用道具
举报
凌大胖纸

发表于 2017-02-08 13:47 显示全部楼层

楼主你好

回复 支持 反对 使用道具
举报
GHOST

发表于 2017-02-08 09:39 显示全部楼层

为了学币每天必水一下,望见谅

回复 支持 反对 使用道具
举报
今夜为你想

发表于 2017-02-08 08:17 显示全部楼层

好文章,必须帮顶!!!

回复 支持 反对 使用道具
举报
张比亚

发表于 2017-02-08 06:36 显示全部楼层

跟你讲个故事:六一那天我玩lol,我进游戏就打了一句61快乐,有3个人说了谢谢,我赶紧退出,我果断退出游戏。

回复 支持 反对 使用道具
举报
12下一页

发表新文章
叶星飞

叩丁狼骨干成员

0

学分

982

学币

1035

积分

叩丁狼骨干成员

Rank: 6Rank: 6

积分
1035
Ta的主页 发消息
精华帖排行榜

精彩推荐

  • 关注叩丁狼教育