代理模式是一种设计模式
,能够使得在不修改源目标的前提下,额外扩展源目标的功能,。即通过访问源目标的代理类,再由代理类去访问源目标。
这样一来,要扩展功能,就无需修改源目标的代码了。只需要在代理类上增加就可以了。
代理类=增强代码+目标实现类。
其中静态代理由于比较简单,面试中也没啥问的,在代理模式一块,问的最多就是动态代理,而且动态代理也是spring aop
的核心思想,spring其他很多功能也是通过动态代理来实现的,比如拦截器,事务控制
,还有测试框架 mock、用户鉴权、日志、全局异常处理
等。
熟练掌握动态代理技术,能让你业务代码更加精简而优雅。如果你需要写一些中间件
的话,比如某些RPC
框架也是运用了动态代理,动态代理技术更是必不可少的技能包。
还有就是项目中统计某些方法的耗时、方法前后记录日志、方法调用次数、频繁操作人记录
等。
那此篇文章就带大家一窥动态代理的所有细节吧。
在说动态代理前,还是先说说静态代理。
所谓静态代理,就是通过声明一个明确的代理类来访问源对象。
我们有2个接口,Person和Animal。每个接口各有2个实现类,UML如下图:
每个实现类中代码都差不多一致,用Student来举例(其他类和这个几乎一模一样)
public class Student implements Person{
private String name;
public Student() {
}
public Student(String name) {
this.name = name;
}
@Override
public void wakeup() {
System.out.println(StrUtil.format("学生[{}]早晨醒来啦",name));
}
@Override
public void sleep() {
System.out.println(StrUtil.format("学生[{}]晚上睡觉啦",name));
}
}
假设我们现在要做一件事,就是在所有的实现类调用wakeup()
前增加一行输出早安~
,调用sleep()
前增加一行输出晚安~
。那我们只需要编写2个代理类PersonProxy
和AnimalProxy
:
PersonProxy:
public class PersonProxy implements Person {
private Person person;
public PersonProxy(Person person) {
this.person = person;
}
@Override
public void wakeup() {
System.out.println("早安~");
person.wakeup();
}
@Override
public void sleep() {
System.out.println("晚安~");
person.sleep();
}
}
AnimalProxy:
public class AnimalProxy implements Animal {
private Animal animal;
public AnimalProxy(Animal animal) {
this.animal = animal;
}
@Override
public void wakeup() {
System.out.println("早安~");
animal.wakeup();
}
@Override
public void sleep() {
System.out.println("晚安~");
animal.sleep();
}
}
最终执行代码为:
public static void main(String[] args) {
Person student = new Student("张三");
PersonProxy studentProxy = new PersonProxy(student);
studentProxy.wakeup();
studentProxy.sleep();
Person doctor = new Doctor("王教授");
PersonProxy doctorProxy = new PersonProxy(doctor);
doctorProxy.wakeup();
doctorProxy.sleep();
Animal dog = new Dog("旺旺");
AnimalProxy dogProxy = new AnimalProxy(dog);
dogProxy.wakeup();
dogProxy.sleep();
Animal cat = new Cat("咪咪");
AnimalProxy catProxy = new AnimalProxy(cat);
catProxy.wakeup();
catProxy.sleep();
}
输出:
早安~
学生[张三]早晨醒来啦
晚安~
学生[张三]晚上睡觉啦
早安~
医生[王教授]早晨醒来啦
晚安~
医生[王教授]晚上睡觉啦
早安~~
小狗[旺旺]早晨醒来啦
晚安~~
小狗[旺旺]晚上睡觉啦
早安~~
小猫[咪咪]早晨醒来啦
晚安~~
小猫[咪咪]晚上睡觉啦
结论:
静态代理的代码相信已经不用多说了,代码非常简单易懂,也是基于接口实现的。这里用了2个代理类,分别代理了Person
和Animal
接口。
这种模式虽然好理解,但是缺点也很明显:
在正式介绍动态代理前,我们先复习一下 java 中对象是如何创建的。
我们在项目中使用一行代码就可以简单创建一个对象,实际上经过的流程还是很复杂的。
// 创建对象
A a = new A();
上面的流程中提到了 Class 对象,有两个概念初学者很容易混淆:Class 对象 和 实例对象。
Class 对象简单来说就是 Class 类的实例,Class 类描述了所有的类;实例对象是通过 Class 对象创建出来的。
从上面的分析可以看出来,要想创建一个实例,最最关键的是获得 Class 对象。
有些同学可能有疑问了,我写代码的时候创建对象没有用到 Class 对象呀,那是因为 Java 语言底层帮你封装了细节。Java 语言给我们提供了new 这个关键字,new 实在太好用了,一行代码就可以创建一个对象。
动态代理,通俗点说就是:无需声明式的创建java代理类,而是在运行过程中生成"虚拟"的代理类,被ClassLoader加载。从而避免了静态代理那样需要声明大量的代理类。
上面说了这么多其实是在引入动态代理的概念,动态代理相对于静态代理最大的区别就是不需要事先写好代理类,一般在程序的运行过程中动态产生代理类对象。
JDK从1.3版本就开始支持动态代理类的创建。主要核心类只有2个:
java.lang.reflect.Proxy
和java.lang.reflect.InvocationHandler
。
通过Proxy创建代理对象。
通过实现InvocationHandler接口的代理类对象实现增强代码和源目标类方法。
还是前面那个例子,用JDK动态代理类去实现的代码如下:
创建一个JdkProxy类,用于统一代理:
//必须实现Invocationhandler接口,完成代理类要做的功能(1.调用目标方法 2.完成目标增强)
public class JdkProxy implements InvocationHandler {
// 源目标类
private Object bean;
//动态代理:目标对象是活动的,不是固定的,需要传入进来
//传入的是谁,就给谁创建代理
public JdkProxy(Object bean) {
this.bean = bean;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// ===完成目标增强 start ===
String methodName = method.getName();
if (methodName.equals("wakeup")){
System.out.println("早安~~~");
}else if(methodName.equals("sleep")){
System.out.println("晚安~~~");
}
// ===完成目标增强 end ===
// 调用目标方法
return method.invoke(bean, args);
}
}
执行代码:
public static void main(String[] args) {
// 1.创建目标对象
Person student = new Student("张三");
// 2.创建Invocationhandler对象
JdkProxy proxy = new JdkProxy(student);
// 3.创建(实例化)代理对象赋值给接口Person
Person student = (Person) Proxy.newProxyInstance(proxy.getClass().getClassLoader(), new Class[]{Person.class}, proxy);
// 4.通过动态生成的代理对象去执行增强代码逻辑和执行源目标类Student的wakeup、sleep方法逻辑
student.wakeup();
student.sleep();
proxy = new JdkProxy(new Doctor("王教授"));
Person doctor = (Person) Proxy.newProxyInstance(proxy.getClass().getClassLoader(), new Class[]{Person.class}, proxy);
doctor.wakeup();
doctor.sleep();
proxy = new JdkProxy(new Dog("旺旺"));
Animal dog = (Animal) Proxy.newProxyInstance(proxy.getClass().getClassLoader(), new Class[]{Animal.class}, proxy);
dog.wakeup();
dog.sleep();
proxy = new JdkProxy(new Cat("咪咪"));
Animal cat = (Animal) Proxy.newProxyInstance(proxy.getClass().getClassLoader(), new Class[]{Animal.class}, proxy);
cat.wakeup();
cat.sleep();
}
讲解:
可以看到,相对于静态代理类来说,无论有多少接口,这里只需要一个代理类。核心代码也很简单。唯一需要注意的点有以下2点:
JDK动态代理是需要声明接口的,创建一个动态代理类必须得给这个”虚拟“的类一个接口。可以看到,这时候经动态代理类创造之后的每个bean已经不是原来那个对象了,比如源目标类Student被代理,生成的不是Student对象,而是一个代理对象。
为什么这里JdkProxy
还需要构造传入原有的bean呢?因为处理完附加的功能外,需要执行原有bean的方法,以完成代理
的职责。
这里JdkProxy
最核心的方法就是
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
其中proxy为代理过之后的对象(并不是原对象),method为被代理的方法,args为方法的参数。
如果你不传原有的bean,直接用method.invoke(proxy, args)
的话,那么就会陷入一个死循环。
Spring在5.X之前默认的动态代理实现一直是jdk动态代理。但是从5.X开始,spring就开始默认使用Cglib来作为动态代理实现。并且springboot从2.X开始也转向了Cglib动态代理实现。
是什么导致了spring体系整体转投Cglib呢,jdk动态代理又有什么缺点呢?
那么我们现在就要来说下Cglib的动态代理。
Cglib是一个开源项目,它的底层是字节码处理框架ASM,Cglib提供了比jdk更为强大的动态代理。主要相比jdk动态代理的优势有:
那何谓通过子类来实现呢?
实现接口MethodInterceptor
还是前面那个例子,我们要实现相同的效果。代码如下
创建CglibProxy类,用于统一代理:
public class CglibProxy implements MethodInterceptor {
private Enhancer enhancer = new Enhancer();
private Object bean;
public CglibProxy(Object bean) {
this.bean = bean;
}
// enhancer根据字节码创建一个原bean,创建完毕之后会调用intercept方法,传入这个obj
public Object getProxy(){
//设置需要创建子类的类
enhancer.setSuperclass(bean.getClass());
enhancer.setCallback(this);
//通过字节码技术动态创建子类实例
return enhancer.create();
}
//实现MethodInterceptor接口方法 这个obj来自于getProxy()
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
String methodName = method.getName();
if (methodName.equals("wakeup")){
System.out.println("早安~~~");
}else if(methodName.equals("sleep")){
System.out.println("晚安~~~");
}
//调用原bean的方法
return method.invoke(bean,args);
}
}
执行代码:
public static void main(String[] args) {
CglibProxy proxy = new CglibProxy(new Student("张三"));
// 动态代理对象赋值给了子类
Student student = (Student) proxy.getProxy();
student.wakeup();
student.sleep();
proxy = new CglibProxy(new Doctor("王教授"));
Doctor doctor = (Doctor) proxy.getProxy();
doctor.wakeup();
doctor.sleep();
proxy = new CglibProxy(new Dog("旺旺"));
Dog dog = (Dog) proxy.getProxy();
dog.wakeup();
dog.sleep();
proxy = new CglibProxy(new Cat("咪咪"));
Cat cat = (Cat) proxy.getProxy();
cat.wakeup();
cat.sleep();
}
讲解:
在这里用Cglib作为代理,其思路和jdk动态代理差不多。也需要把原始bean构造传入。原因上面有说,这里不多赘述。
关键的代码在这里
//设置需要创建子类的类
enhancer.setSuperclass(bean.getClass());
enhancer.setCallback(this);
//通过字节码技术动态创建子类实例
return enhancer.create();
可以看到,Cglib"凭空"的创造了一个原bean的子类,并把Callback指向了this,也就是当前对象,也就是这个proxy对象。从而会调用intercept
方法。而在intercept
方法里,进行了附加功能的执行,最后还是调用了原始bean的相应方法。
在debug这个生成的代理对象时,我们也能看到,Cglib是凭空生成了原始bean的子类,比如Student。
Javassist是一个开源的分析、编辑和创建Java字节码的类库,可以直接编辑和生成Java生成的字节码。相对于bcel, asm等这些工具,开发者不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。
在日常使用中,javassit通常被用来动态修改字节码。它也能用来实现动态代理的功能。
话不多说,还是一样的例子,我用javassist动态代理来实现一遍
创建JavassitProxy,用作统一代理:
public class JavassitProxy {
private Object bean;
public JavassitProxy(Object bean) {
this.bean = bean;
}
public Object getProxy() throws IllegalAccessException, InstantiationException {
ProxyFactory f = new ProxyFactory();
f.setSuperclass(bean.getClass());
f.setFilter(m -> ListUtil.toList("wakeup","sleep").contains(m.getName()));
Class c = f.createClass();
MethodHandler mi = (self, method, proceed, args) -> {
String methodName = method.getName();
if (methodName.equals("wakeup")){
System.out.println("早安~~~");
}else if(methodName.equals("sleep")){
System.out.println("晚安~~~");
}
return method.invoke(bean, args);
};
Object proxy = c.newInstance();
((Proxy)proxy).setHandler(mi);
return proxy;
}
}
执行代码:
public static void main(String[] args) throws Exception{
JavassitProxy proxy = new JavassitProxy(new Student("张三"));
Student student = (Student) proxy.getProxy();
student.wakeup();
student.sleep();
proxy = new JavassitProxy(new Doctor("王教授"));
Doctor doctor = (Doctor) proxy.getProxy();
doctor.wakeup();
doctor.sleep();
proxy = new JavassitProxy(new Dog("旺旺"));
Dog dog = (Dog) proxy.getProxy();
dog.wakeup();
dog.sleep();
proxy = new JavassitProxy(new Cat("咪咪"));
Cat cat = (Cat) proxy.getProxy();
cat.wakeup();
cat.sleep();
}
讲解:
熟悉的配方,熟悉的味道,大致思路也是类似的。同样把原始bean构造传入。可以看到,javassist也是用”凭空“生成子类的方式类来解决,代码的最后也是调用了原始bean的目标方法完成代理。
javaassit比较有特点的是,可以对所需要代理的方法用filter来设定,里面可以像Criteria构造器那样进行构造。其他的代码,如果你仔细看了之前的代码演示,应该能很轻易看懂了。
ByteBuddy,字节码伙计,一听就很牛逼有不。
ByteBuddy也是一个大名鼎鼎的开源库,和Cglib一样,也是基于ASM实现。还有一个名气更大的库叫Mockito,相信不少人用过这玩意写过测试用例,其核心就是基于ByteBuddy来实现的,可以动态生成mock类,非常方便。另外ByteBuddy另外一个大的应用就是java agent,其主要作用就是在class被加载之前对其拦截,插入自己的代码。
ByteBuddy非常强大,是一个神器。可以应用在很多场景。但是这里,只介绍用ByteBuddy来做动态代理,关于其他使用方式,可能要专门写一篇来讲述,这里先给自己挖个坑。
来,还是熟悉的例子,熟悉的配方。用ByteBuddy我们再来实现一遍前面的例子
创建ByteBuddyProxy,做统一代理:
public class ByteBuddyProxy {
private Object bean;
public ByteBuddyProxy(Object bean) {
this.bean = bean;
}
public Object getProxy() throws Exception{
Object object = new ByteBuddy().subclass(bean.getClass())
.method(ElementMatchers.namedOneOf("wakeup","sleep"))
.intercept(InvocationHandlerAdapter.of(new AopInvocationHandler(bean)))
.make()
.load(ByteBuddyProxy.class.getClassLoader())
.getLoaded()
.newInstance();
return object;
}
public class AopInvocationHandler implements InvocationHandler {
private Object bean;
public AopInvocationHandler(Object bean) {
this.bean = bean;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (methodName.equals("wakeup")){
System.out.println("早安~~~");
}else if(methodName.equals("sleep")){
System.out.println("晚安~~~");
}
return method.invoke(bean, args);
}
}
}
执行代码:
public static void main(String[] args) throws Exception{
ByteBuddyProxy proxy = new ByteBuddyProxy(new Student("张三"));
Student student = (Student) proxy.getProxy();
student.wakeup();
student.sleep();
proxy = new ByteBuddyProxy(new Doctor("王教授"));
Doctor doctor = (Doctor) proxy.getProxy();
doctor.wakeup();
doctor.sleep();
proxy = new ByteBuddyProxy(new Dog("旺旺"));
Dog dog = (Dog) proxy.getProxy();
dog.wakeup();
dog.sleep();
proxy = new ByteBuddyProxy(new Cat("咪咪"));
Cat cat = (Cat) proxy.getProxy();
cat.wakeup();
cat.sleep();
}
讲解:
思路和之前还是一样,通过仔细观察代码,ByteBuddy也是采用了创造子类的方式来实现动态代理。
cglib 和 JDK 动态代理的区别?
JDK 动态代理:利用 InvocationHandler 加上反射机制生成一个代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理
cglib 动态代理:利用ASM框架,将目标对象类生成的class文件加载进来,通过修改其字节码生成代理子类
JDK动态代理采用接口代理的模式,代理对象只能赋值给接口,允许多个接口。
Cglib,Javassist,ByteBuddy这些都是采用了子类代理的模式,代理对象既可以赋值给接口,又可以复制给具体实现类。
Spring框架什么时候用 cglib 什么时候用 JDK 动态代理?
- 目标对象生成了接口默认用 JDK 动态代理
- 如果目标对象没有实现接口,必须采用cglib
- 当然如果目标对象使用了接口也可以强制使用cglib
性能:
Spring5.X,Springboot2.X只有都采用了Cglib作为动态代理的实现,那是不是cglib性能是最好的呢?
动态代理技术对于一个经常写开源或是中间件的人来说,是一个神器。这种特性提供了一种新的解决方式。从而使得代码更加优雅而简单。动态代理对于理解spring的核心思想也有着莫大的帮助。