引言
为什么要把动态代理和字节码生成技术一起说呢?因为动态代理的技术基石就是字节码生成技术,但是字节码生成技术能做的事情不止动态代理。
因为之前对Spring的使用不多,没有深入研究过源码,关于aop和动态代理等技术没有深入了解过,比如它到底是用来做什么的、又是怎么实现的。最近在工作中时不时会接触到这块概念,索性深入了解一下。
代理
什么是代理
英文似乎是Delegation,音译为代理。
其实就是本来程序提供了接口以及实现,但是在使用方调用的时候不是直接调用具体实现,而是有一个代理方,它被调用然后进行一些操作,并由它来调用真正的实现代码。
代理和组合是什么关系
上面提到的代理方 和真正实现 其实就是组合关系,这个是描述两个类之间的关系的,而代理 是用来描述两者之间的逻辑关系,他们只是用了组合这种方式来实现。
静态代理和动态代理
在网上搜索动态代理的资料,总是会有提到静态代理 ,但根据我的观察,这个只是为了说明代理形态,实际上动态代理是大家最终的选择,静态代理只是为了讲解而引入的。就像JVM的GC从没用过引用计数,但总是会提到这个思想,因为直观先想到的解决方法就是它。
下面是静态代理类的示例:
java
// 接口
public interface HelloService {
void sayHello(String name);
}
// 实现类
public class HelloServiceImpl implements HelloService {
@Override
public void sayHello(String name) {
System.out.println("Hello, " + name + "!");
}
}
// 静态代理类
public class HelloServiceProxy implements HelloService {
private HelloService target; // 持有被代理对象
public HelloServiceProxy(HelloService target) {
this.target = target;
}
@Override
public void sayHello(String name) {
System.out.println("【代理】方法执行前...");
target.sayHello(name); // 调用真实方法
System.out.println("【代理】方法执行后...");
}
}
// 测试
public class Main {
public static void main(String[] args) {
// 创建真实对象
HelloService real = new HelloServiceImpl();
// 创建代理对象,传入真实对象
HelloService proxy = new HelloServiceProxy(real);
// 通过代理调用
proxy.sayHello("World");
}
}
下面是动态代理的例子:
java
// 接口
public interface HelloService {
void sayHello(String name);
}
// 实现类
public class HelloServiceImpl implements HelloService {
@Override
public void sayHello(String name) {
System.out.println("Hello, " + name + "!");
}
}
// 实现 InvocationHandler
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class MyInvocationHandler implements InvocationHandler {
private Object target; // 被代理的真实对象
public MyInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("【动态代理】方法执行前...");
Object result = method.invoke(target, args); // 调用真实方法
System.out.println("【动态代理】方法执行后...");
return result;
}
}
// 测试
import java.lang.reflect.Proxy;
public class Main {
public static void main(String[] args) {
// 创建真实对象
HelloService real = new HelloServiceImpl();
// 创建动态代理对象
HelloService proxy = (HelloService) Proxy.newProxyInstance(
real.getClass().getClassLoader(), // 类加载器
real.getClass().getInterfaces(), // 代理的接口
new MyInvocationHandler(real) // 处理器
);
// 通过代理调用
proxy.sayHello("World");
}
}
动态代理代码解析
总共其实是两个部分
- 实现InvocationHandler接口,加入代理逻辑
- 使用Proxy.newProxyInstance创建动态代理对象
逻辑是确定的,不展开描述
动态代理的底层实现
这里基于问题跟进
-
动态代理到底是怎么让代理work起来的?
从静态代理来看,其实是一个代理类调用具体实现类。
但动态代理其实有三个类,需要新产生两个类,一个类A是切面类,实现要对被代理类的扩展操作;另一个类B 是动态生成的类,实现了指定的接口,同时继承Proxy类。
他们的调用关系如下:类B调用 类A,类A调用真正实现类。
"动态"的特点就体现在类B是运行时生成的,类A也不需要实现被代理类的接口。
-
真正实现类与切面类之间没有组合关系,切面类是如何调用真正实现类的方法的。
答案很简单:反射。动态代理类调用切面类的时候把方法句柄传过去了。
-
动态代理类是怎么生成的
是用了字节码生成技术,通过拼接字节码生成了一个动态类,我们经常在堆栈中看到的Proxy0就是。
HelloService proxy = (HelloService) Proxy.newProxyInstance(
real.getClass().getClassLoader(), // 类加载器
real.getClass().getInterfaces(), // 代理的接口
new MyInvocationHandler(real) // 处理器
);
方法调用里的参数也可以看到,需要指定用哪个类加载器加载动态生成的字节码;然后要指定接口,动态生成的字节码要实现这个接口;至于第三个就是切面对象,代理类会调用它。
切面是什么
我觉得切面其实就是在方法前后可以做一些事情,不是通过正常编程方式,而是在已有的代码里塞进去一些逻辑,所以叫做切面编程。
动态代理其实就是切面编程,InvocationHandler里实现的就是切面的逻辑。
但切面其实还更广泛,如果直接在已有的代码上通过字节码增强的方式加入逻辑,也是切面编程,但就不属于动态代理的范畴了,Aspectj就是切面编程的一个工具,这种又有代码的编译时静态织入和运行时动态织入两种形态。
基于动态代理,对RPC的理解
RPC框架其实就是用动态代理生成了业务接口的实现类,可以想象它底层做了什么。
- 动态代理类实现了业务接口,让调用方感觉像是在调用接口的具体实现类
- 内部做了request构造、远程调用、response解析等逻辑,让IO操作对调用方进行了屏蔽。
所以RPC框架其实就是对这种IO调用的一种统一解决方案,避免了每个调用方都要自己写IO连接、请求、返回处理等逻辑,而且用动态代理的好处是RPC框架是通用的,只要指定一个接口,RPC可以表现成对这个接口的实现。
CGLIB
除了上面JDK的Proxy,还有一种动态代理技术是CGLIB(code generation library)
它俩的区别是JDK的Proxy是基于接口生成动态类,CGLIB是继承父类实现动态类。Spring同时支持这两种。
字节码生成技术
上面的动态代理底层都是要动态生成代理类的,那这个代理类的生成都是要用字节码生成技术的。
这个技术有多种实现,JDK的Proxy是用的自行实现的字节码拼接逻辑,除此之外还有ASM、java的Instrumentation API、byteBuddy。
这几个不展开介绍了,暂不是很了解。
他们有很强的字节码操作功能,不只是生成,应该还有修改。
Arthas应该就用了Instrumentation API,所以这个技术的使用范围不止是动态代理,能做的事情很多。
关于java的编译 + 解释执行
感觉字节码生成技术是依托于java的这个特性的,java的编译结果是class,class是有固定结构的,那就存在生成、拼接、修改等灵活操作的空间。这些修改动作其实就是字节码生成。
然后生成出来的class最终被ClassLoader加载去解释执行,这么一想,其实这项技术也没有什么神秘的地方,只不过在平常业务开发中并不是那么常用罢了。