说在前面的话:本人在学习JDK动态代理时,以案例的方式学习了JDK动态代理底层实现,总结了个人的实现步骤,由于本人水平有限,如有不当之处,望指出。
JDK动态代理底层的底层类似于我们手写HelloWorld的过程,不过JDK是在运行期生成Java文件并编译加载
动态生成代理类的.java -> 动态编译为.class文件 -> 将.class文件加载到JVM中
-> 使用该类创建对象
一、案例需求
首先描述案例需求,现在有Flyable接口,抽象方法为fly(time)方法,Bird类实现Flyable接口。 现需要生成代理对象,代理该Bird对象中Flyable接口的方法,记录鸟儿每次飞行的日期时分秒。
二、准备工作
1.写一个Flyable接口
public interface Flyable {
void fly(long time);
}
复制代码
没什么 具体的东西,就时一个飞的方法,参数为飞多久
2.写一个Bird实现接口
public class Bird implements Flyable {
@Override
public void fly(long time) {
try {
System.out.println("我是小鸟,我开始飞了");
Thread.sleep(time);
System.out.println("我是小鸟,我飞了" + time + "毫秒");
} catch (InterruptedException e) {
}
}
}
复制代码
鸟实现会飞的接口
三、开始实现代理
1.写一个执行接口
//执行方法
public interface InvocationHandler {
//第一个参数,代理对象,第二个参数,所执行的方法反射对象,第三个参数及以后,传入的参数
Object invoke(Object proxy, Method method, Object... args);
}
复制代码
此接口作用:我们可以定制代理对象的方法功能
动态代理执行的所有方法其实都是在里面执行了这一个方法,所以此接口尤为重要
2.写获取代理对象的类
==重头戏==
用来获取代理对象的类
public class MyProxy {
private static final String CLASS_BASE_PATH = "C:\\Users\\bai\\Desktop\\Java生成代码";
private static final String PACKAGE_NAME = "myproxy";
//获取代理对象
public static <T> T newProxyInstance(Class<T> clazz, InvocationHandler invocationHandler) {
String proxyClassName = clazz.getSimpleName() + "$MyProxy";
try {
//一、 生成java文件
generateProxyJavaFile(clazz, proxyClassName);
//二、 编译
compileJavaFile();
//三、 加载class文件到jvm中
ClassUtil.loadClass(new File(CLASS_BASE_PATH));
//四、 创建对象并返回
Class proxyClass = MyProxy.class.getClassLoader().loadClass(PACKAGE_NAME + "." + proxyClassName);
return (T) proxyClass.getConstructor(InvocationHandler.class).newInstance(invocationHandler);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
//生成代理类的java文件
private static void generateProxyJavaFile(Class clazz, String proxyClassName) throws IOException {
//...下面讲
}
//把java文件编译成.class文件
private static void compileJavaFile() throws FileNotFoundException{
//...下面讲
}
}
复制代码
分四步走,和写一个java的helloWord类似
1)创建Java文件
这里是使用Java代码生成Java文件,要借助一个开源工具包,Javapoet,maven仓库中有。
和手写java文件类似,类头,属性,构造器,方法等。
扫描二维码关注公众号,回复:
6872387 查看本文章
按Javapoet规则写,并不难,平时可以手写的,用它都可以实现
构造方法的地方有些麻烦,可以参照生成后的java文件看代码
//生成代理类的java文件
private static void generateProxyJavaFile(Class clazz, String proxyClassName) throws IOException {
//构造一个类,实现传入接口,addSuperinterface功能是添加一个实现接口
TypeSpec.Builder classBuilder = TypeSpec.classBuilder(proxyClassName).addSuperinterface(clazz);
//构建一个属性,用于保存执行对象
FieldSpec fieldSpec = FieldSpec.builder(InvocationHandler.class, "handler", Modifier.PRIVATE).build();
//添加到类中
classBuilder.addField(fieldSpec);
//构建一个构造器,初始化执行对象
MethodSpec constructor = MethodSpec.constructorBuilder()
//添加权限修饰符
.addModifiers(Modifier.PUBLIC)
//添加参数
.addParameter(InvocationHandler.class, "handler")
//方法体内容
.addStatement("this.handler = handler")
.build();
//把构造器添加到类中
classBuilder.addMethod(constructor);
//获取传入接口的所有公有方法(自己的,外部可以访问的方法,不包括继承的方法)
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
MethodSpec.Builder methodSpec = MethodSpec.methodBuilder(method.getName())
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.returns(method.getReturnType());
//生成handler.invoke()执行语句(实际执行方法),添加到方法体
StringBuilder invokeString = new StringBuilder("\tthis.handler.invoke(this, " + clazz.getName() + ".class.getMethod(\"" + method.getName() + "\",");
//存储执行方法参数列表
StringBuilder paramNames = new StringBuilder();
//这部分如果看不太懂,可以对照生成后的java文件
for (Parameter parameter : method.getParameters()) {
//添加外部方法参数列表
methodSpec.addParameter(parameter.getType(), parameter.getName());
//添加实际执行方法中
invokeString.append(parameter.getType() + ".class, ");
//存到执行方法参数列表中
paramNames.append(parameter.getName() + ",");
}
//把最后一个逗号替换为)
int lastCommaIndex = invokeString.lastIndexOf(",");
invokeString.replace(lastCommaIndex, invokeString.length(), "), ");
lastCommaIndex = paramNames.lastIndexOf(",");
paramNames.replace(lastCommaIndex, lastCommaIndex + 1, ")");
//把属性名追加到最后一个参数列表
invokeString.append(paramNames);
//添加方法体,执行InvocationHandler的invoke方法,并抓取异常
methodSpec.addCode("try{\n");
methodSpec.addStatement(invokeString.toString());
methodSpec.addCode("} catch (NoSuchMethodException e) {\n");
methodSpec.addCode("\te.printStackTrace();\n");
methodSpec.addCode("}\n");
//添加到类中
classBuilder.addMethod(methodSpec.build());
}
//生成java文件,第一个参数是包名
// String path = MyProxy.class.getResource("/").toString();
JavaFile javaFile = JavaFile.builder(PACKAGE_NAME, classBuilder.build()).build();
//把java文件写到执行路径下(默认会把包生成文件夹)
javaFile.writeTo(new File(CLASS_BASE_PATH));
}
复制代码
生成后的Java文件
package myproxy;
import java.lang.Override;
class Flyable$MyProxy implements Flyable {
private InvocationHandler handler;
public Flyable$MyProxy(InvocationHandler handler) {
this.handler = handler;
}
@Override
public void fly(long arg0) {
try{
this.handler.invoke(this, myproxy.Flyable.class.getMethod("fly",long.class), arg0);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
}
复制代码
2)编译为.class文件
Java提供的有类似Javac编译功能的工具,过程并不复杂,可以灵活设置编译期jvm的参数,如果为null则用当前环境的参数
//把java文件编译成.class文件
private static void compileJavaFile() throws FileNotFoundException {
//1.获取javac编译器
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//2.通过javac编译器获取一个编译java文件管理器
StandardJavaFileManager manager = compiler.getStandardFileManager(null, null, null);
//3.获取java文件对象
//-调用一个工具类,从指定路径下,递归获取所有指定后缀的文件
Set<File> javaFiles = FileUtil.getFilesForSuffix(new File(CLASS_BASE_PATH), ".java");
//-这里就一个java文件,就直接使用了
Iterator<File> iterator = javaFiles.iterator();
Iterable<? extends JavaFileObject> it = manager.getJavaFileObjects(iterator.next().getAbsoluteFile());
//4.编译
JavaCompiler.CompilationTask task = compiler.getTask(null, manager,
null, null, null, it);
task.call();
}
复制代码
3)加载.class文件
加载class文件到jvm中
public class ClassUtil {
//加载class文件
public static <T> void loadClass(File classFolder) throws Exception {
//使用url类加载器,把文件夹路径添加进去,就可以直接加载文件夹下的所有class文件
//相当于把这个路径设置为源码路径之一
//获取URLClassLoader的addURL方法
Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
boolean accessible = method.isAccessible();
try {
//如果方法没有权限访问,则设置可访问权限
if (accessible == false) {
method.setAccessible(true);
}
// 设置类加载器
URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
// 将当前类路径加入到类加载器中
method.invoke(classLoader, classFolder.toURI().toURL());
} finally {
//把方法的权限设置回去
method.setAccessible(accessible);
}
//获取文件夹中所有的.class文件
Set<File> files = FileUtil.getFilesForSuffix(classFolder, ".class");
for (File file : files) {
// 把文件名称转化为,和java.lang.String类似的全类名
String className = file.getAbsolutePath();
className = className.substring(classFolder.getAbsolutePath().length() + 1, className.length() - 6);
className = className.replace(File.separatorChar, '.');
// 加载Class类
Class.forName(className);
}
}
}
复制代码
三.测试代理对象
经过一系列努力,终于完成了动态代理的编写,进入到测试阶段
1.编写Invoke方法体
public class MyInvocationHandler implements InvocationHandler {
private Bird bird;
public MyInvocationHandler(Bird bird) {
this.bird = bird;
}
@Override //第一个参数为代理对象,第二个参数为方法对象,后面的参数为方法参数
public Object invoke(Object proxy, Method method, Object... args) {
String dateString = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(System.currentTimeMillis());
System.out.println(dateString + "小鸟起飞");
try {
Object invoke = method.invoke(bird, args);
return invoke;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
复制代码
2.测试代理对象
public class MyProxyTest {
@Test
public void testProxy() {
Flyable flyable = MyProxy.newProxyInstance(Flyable.class, new MyInvocationHandler(new Bird()));
flyable.fly(1000);
}
}
复制代码
控制台打印
2019-07-25 20:44:59
我是小鸟,我开始飞了
我是小鸟,我飞了1000毫秒
复制代码