当前位置:   article > 正文

了解了反射,我们来捋一下Java的代理模式_java代理模式需要先了解反射吗

java代理模式需要先了解反射吗

Java反射机制一文中提到过Spring IOC 和AOP都运用到了动态代理机制,也相当于应用了反射,那么到底什么是代理,什么是动态代理?动态代理又是如何运用反射的?
在这里插入图片描述

常规编码方式

在了解代理之前,我们常规使用接口的方式其实是:所有interface变量都是通过向上转型指向某个子类实例的
1.编写一个接口

public interface SmsService {
    String send(String message);
}
  • 1
  • 2
  • 3

2.编写一个类实现这个接口(实现子类)

public class SmsServicseImpl implements SmsService {
    public String send(String message) {
        System.out.println("send message:" + message);
        return message;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

3.最后通过创建这个实现类对象,通过向上转型调用接口

SmsService s = new SmsServicseImpl();
s.send("Java");
  • 1
  • 2

而代理模式和我们接触的常规模式可以说完全不一样

代理模式的概念

简单来说,代理模式就是 使用代理对象来代替对真实对象的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。
代理模式有三个主要组成部分

  • 委托类RealSubject——也就是被代理类,用来实现真正的业务功能
  • 代理类Proxy——将自身的请求用RealSubject的功能来实现,自己并不真正的去实现功能
  • Subject——委托类和代理类都应该实现的接口

在这里插入图片描述
通常来说,代理模式作用就是扩展目标对象的功能,比如说在执行这个方法前后你可以自己增加一些额外操作,但是你并需要对目标代码进行改动,而是在代理类Proxy里手动添加
那么现在大家也知道为什么我们要定义一个公共接口Subject,而且Proxy和Real Subject都要实现这个接口?
就是为了保持行为的一致性在访问者看来两者之间就没有区别。这样,通过代理类这个中间层,很好地隐藏和保护了委托类对象,能有效屏蔽外界对委托类对象的直接访问。同时,也可以在代理类上加上额外的操作,比如小红在说话之前会跳一段舞,外界就会觉得你在说话前会跳一段舞,所以,这就实现了委托类的功能增强。
代理模式有静态代理和动态代理两种

静态代理

什么是静态代理
我们首先看一下静态代理的实现步骤

1)定义一个接口(Subject)

2)创建一个委托类(Real Subject)实现这个接口

3)创建一个代理类(Proxy)同样实现这个接口

4)将委托类 Real Subject 注入进代理类 Proxy,在代理类的方法中调用 Real Subject 中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。

但是在日常开发中,如果使用静态代理,我们对委托类对象的方法增强都必须手动加强
,非常的不灵活(如果Subject添加了一个新的方法,委托类和代理类的代码也要修改代码)和麻烦(每个委托类都要单独写一个代理类),所以平常几乎看不到使用静态代理的场景。
从 JVM 层面来说, 静态代理在编译时就将接口、委托类、代理类这些都变成了一个个实际的 .class 文件
静态代理案例
1.编写一个含有发送短信方法的接口

public interface SmsService {
    String send(String message);
}
  • 1
  • 2
  • 3

2.编写委托类实现接口

public class SmsServiceImpl implements SmsService {
    public String send(String message) {
        System.out.println("send message:" + message);
        return message;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

3.编写代理类Proxy也要实现接口
4.然后将委托类注入代理类,在代理类的方法中调用 Real Subject 中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。

public class SmsProxy implements SmsService {

    // 将委托类注入进代理类
    private final SmsService smsService;

    public SmsProxy(SmsService smsService) {
        this.smsService = smsService;
    }

    @Override
    public String send(String message) {
        // 调用委托类方法之前,我们可以添加自己的操作
        System.out.println("before method send()");
        // 调用委托类方法
        smsService.send(message); 
        // 调用委托类方法之后,我们同样可以添加自己的操作
        System.out.println("after method send()");
        return null;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

那么我们编写完成后该如何调用呢?

public class Main {
    public static void main(String[] args) {
        SmsService smsService = new SmsServiceImpl();//向上转型
        SmsProxy smsProxy = new SmsProxy(smsService);//创建代理类实例
        //调用增强后的方法
        smsProxy.send("Java");
    }
}
//结果输出
before method send()
send message:java
after method send()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

从结果输出看,我们已经对委托类的send方法实现了增强,而且并没有改变它的代码
当然,从上述代码我们也能看出来,静态代理存在一定的弊端。假如说我们现在新增了一个委托类实现了 SmsService 接口,如果我们想要对这个委托类进行增强,就需要重新写一个代理类,然后注入这个新的委托类,非常不灵活。也就是说静态代理是一个委托了对应一个代理类,能不能将代理类做成一个通用的呢?为此,动态代理应用而生。

Java字节码生成框架

在复习动态代理之前,我们还需要了解一下.class字节码文件这个东西,动态代理机制和 Java 字节码生成框架息息相关。
在反射里我们有提到过类的类型Class,每个Class类对应一个.class文件,Class类包含一个类的全部信息,相对的.class字节码文件也包含了一个类的全部信息,字节码其实就是二进制文件,里面是只有JVM才能识别的机器码
解析过程这样的:JVM 读取 .class 字节码文件,取出二进制数据,加载到内存中,解析字节码文件内的信息,生成对应的 Class 类对象:
在这里插入图片描述
显然上述的过程是在编译期发生的,而我们也知道JVM是根据.class字节码文件(二进制格式)加载类的,如果我们在运行期遵循 Java 编译系统组织 .class 字节码文件的格式和结构,生成相应的二进制数据,然后再把这个二进制数据加载转换成对应的类。这样,我们**不就完成了在运行时动态的创建一个类。**这个思想其实也就是动态代理的思想。
在这里插入图片描述
在运行时期按照 JVM 规范对 .class 字节码文件的组织规则,生成对应的二进制数据。当前有很多开源框架可以完成这个功能,如
1.ASM
2.CGLIB
3.Javassit
其中CGLIB 是基于 ASM 的。 这里简单对比一下 ASM 和 Javassist:

1.Javassist 源代码级 API 比 ASM 中实际的字节码操作更容易使用
2.Javassist 在复杂的字节码级操作上提供了更高级别的抽象层。Javassist 源代码级 API 只需要很少的字节码知识,甚至不需要任何实际字节码知识,因此实现起来更容易、更快。
3.Javassist 使用反射机制,这使得它比 ASM 慢。

总的来说 ASM 比 Javassist 快得多,并且提供了更好的性能,但是 Javassist 相对来说更容易使用,两者各有千秋

我们以Javassit举个例子看一下这些框架在运行时生成.class字节码文件的能力
我们正常创建类的流程都是

package com.samples;

public class Programmer {
    public void code(){
        System.out.println("I'm a Programmer,Just Coding.....");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

那么我们来看一下Javassit是如何运行时生成和上面的Programmer类一样的类

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;

public class MyGenerator {
    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
          // 创建 Programmer 类        
        CtClass cc= pool.makeClass("com.samples.Programmer");
        // 定义方法
        CtMethod method = CtNewMethod.make("public void code(){}", cc);
        // 插入方法代码
        method.insertBefore("System.out.println(\"I'm a Programmer,Just Coding.....\");");
        cc.addMethod(method);
        // 保存生成的字节码
        cc.writeFile("d://temp");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

我们通过反编译工具打开Programmer.class文件
在这里插入图片描述
生成了一个一样的类!

什么是动态代理

我们了解了字节码生成框架之后,我们可以开始进行动态代理的学习了
我们先回顾一下静态代理的工作流程
在这里插入图片描述
可以看出静态代理其实就是在调用委托类method的前后增加了一些操作。委托类的不同,也就导致代理类的不同。(静态代理一个委托对应一个代理)
那么为了做一个通用性的代理类出来,我们把调用委托类方法的这个动作抽取出来,把它封装成一个通用性的处理类,于是就有了动态代理中的 InvocationHandler 角色(处理类)。
这样在Proxy代理类调用委托类方法的这个过程之间我们加多了一个处理类InvocationHandler的角色,这个角色主要是对代理类调用委托类方法的这个动作进行统一的调用,也就是说由 InvocationHandler 来统一处理代理类调用委托类方法这个操作。
(其实这里就和反射机制相似)
看下图:
在这里插入图片描述
从 JVM 角度来说,动态代理是在运行时动态生成 .class 字节码文件 ,并加载到 JVM 中的。这个我们在 Java 字节码生成框架中已经提到过。

虽然动态代理在我们日常开发中使用的相对较少,但是在框架中的几乎是必用的一门技术。学会了动态代理之后,对于我们理解和学习各种框架的原理也非常有帮助,Spring AOP、RPC 等框架的实现都依赖了动态代理。
就Java来说,实现动态代理的方式有多种
1.JDK动态代理
2.CGLIB动态代理
3.Javassit动态代理
4.。。。。。
下面来详细介绍上述三种动态代理

JDK动态代理机制

JDK动态代理使用步骤
1、定义一个接口Subject
2、定义一个实现接口的委托类RealSubject
3、定义一个处理类并实现InvocationHandler接口,重写其 invoke 方法(在 invoke 方法中利用反射机制调用委托类的方法,并自定义一些处理逻辑),并将委托类注入处理类
在这里插入图片描述
invoke方法有三个参数
1.Proxy(代理类)
2.method——还记得反射中讲到的 Method.invoke 吗?就是这个,我们可以通过它来调用委托类的方法(反射),这也是为什么要先了解反射再学习动态代理
3.args——传给委托类方法的参数列表
在这里插入图片描述
4、创建代理对象(Proxy):通过 Proxy.newProxyInstance() 创建委托类对象的代理对象(反射)
在这里插入图片描述
这个方法需要 3 个参数:
1.类加载器 ClassLoader
2.委托类实现的接口数组,至少需要传入一个接口进去(这一步其实就是Proxy通用的过程)
3.调用的 InvocationHandler 实例处理接口方法(也就是第 3 步我们创建的类的实例)
也就是说:我们在通过 Proxy 类的 newProxyInstance() 创建的代理对象在调用方法的时候,实际会调用到实现了 InvocationHandler 接口的处理类的 invoke() 方法,可以在 invoke() 方法中自定义处理逻辑,比如在方法执行前后做什么事情。
总结的说,就是动态代理相比静态代理,将扩展方法的过程变成了调用invoke方法
代码举例
1.定义Subject

public interface SmsService {
    String send(String message);
}
  • 1
  • 2
  • 3

2.定义RealSubject实现接口

public class SmsServiceImpl implements SmsService {
    public String send(String message) {
        System.out.println("send message:" + message);
        return message;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

3.创建一个创建一个处理类并实现 InvocationHandler 接口,重写其 invoke 方法(在 invoke 方法中利用反射机制调用委托类的方法,并自定义一些处理逻辑),并将委托类注入处理类

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class DebugInvocationHandler implements InvocationHandler {

    // 将委托类注入处理类(这里我们用 Object 代替,方便扩展)
    private final Object target;

    public DebugInvocationHandler(Object target) {
        this.target = target;
    }

    // 重写 invoke 方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
        //调用方法之前,我们可以添加自己的操作
        System.out.println("before method " + method.getName());
        Object result = method.invoke(target, args);
        //调用方法之后,我们同样可以添加自己的操作
        System.out.println("after method " + method.getName());
        return result;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

4.定义一个创建代理对象(Proxy)的工厂类:通过 Proxy.newProxyInstance() 创建委托类对象的代理对象

public class JdkProxyFactory {
    public static Object getProxy(Object target) {
        return Proxy.newProxyInstance
        (
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new DebugInvocationHandler(target)
        );
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

实际调用

SmsService smsService = (SmsService) JdkProxyFactory.getProxy(new SmsServiceImpl());
smsService.send("Java");
  • 1
  • 2

输出结果

before method send
send message:Java
after method send
  • 1
  • 2
  • 3

CGLIB动态代理

JDK 动态代理有一个最致命的问题是它只能代理实现了某个接口的委托类,并且代理类也只能代理接口中实现的方法,要是委托类中有自己私有的方法,而接口中没有的话,该方法不能进行代理调用。
那么为了解决上述的缺点,CGLIB出现了
CGLIB(Code Generation Library)是一个基于 ASM 的 Java 字节码生成框架,它允许我们在运行时对字节码进行修改和动态生成。原理就是通过字节码技术生成一个子类,并在子类中拦截父类方法的调用,织入额外的业务逻辑
关键词大家一定要注意——拦截
CGLIB 引入一个新的角色就是方法拦截器 MethodInterceptor。和 JDK 中的处理类 InvocationHandler 差不多,也是用来实现方法的统一调用的。看下图:
在这里插入图片描述
另外由于CGLIB是采取继承的方式,所以委托类RealSubject不可以被final修饰
很多知名的开源框架都使用到了 CGLIB, 例如 Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。
CGLIB动态代理实现步骤
1.创建一个委托类RealSubject
2.创建一个方法拦截器实现接口 MethodInterceptor,并重写 intercept 方法。intercept 用于拦截并增强委托类的方法(和 JDK 动态代理 InvocationHandler 中的 invoke 方法类似)
在这里插入图片描述
这个intercept方法有四个参数
1.Object var1:委托类对象
2.Method var2:被拦截的方法(委托类中需要增强的方法)
3.Object[] var3:方法入参
4.MethodProxy var4:用于调用委托类的原始方法(底层也是通过反射机制,不过不是 Method.invoke 了,而是使用 MethodProxy.invokeSuper 方法)
在这里插入图片描述
3、创建代理对象(Proxy):通过 Enhancer.create() 创建委托类对象的代理对象
在这里插入图片描述
也就是说:我们在通过 Enhancer 类的 create() 创建的代理对象在调用方法的时候,实际会调用到实现了 MethodInterceptor 接口的处理类的 intercept() 方法,可以在 intercept() 方法中自定义处理逻辑,比如在方法执行前后做什么事情。
(这一步其实和JDK动态代理的invoke方法逻辑是差不多的)
CGLIB 动态代理的核心是方法拦截器 MethodInterceptor 和 Enhancer,而 JDK 动态代理的核心是处理类 InvocationHandler 和 Proxy
代码举例
不同与JDK动态代理,由于CGLIB是一个框架,我们使用前要手动添加依赖

<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.3.0</version>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5

1.创建一个委托类

public class AliSmsService {
    public String send(String message) {
        System.out.println("send message:" + message);
        return message;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

2.创建拦截器实现接口MethodInterceptor,并重写intercept方法

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

public class DebugMethodInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        // 调用方法之前,我们可以添加自己的操作
        System.out.println("before method " + method.getName());
        // 通过反射调用委托类的方法
        Object object = methodProxy.invokeSuper(o, args);
        // 调用方法之后,我们同样可以添加自己的操作
        System.out.println("after method " + method.getName());
        return object;
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

3.创建Proxy工厂类,使用Enhancer.create()创建代理对象

import net.sf.cglib.proxy.Enhancer;

public class CglibProxyFactory {
    public static Object getProxy(Class<?> clazz) {
        // 创建动态代理增强类
        Enhancer enhancer = new Enhancer();
        // 设置类加载器
        enhancer.setClassLoader(clazz.getClassLoader());
        // 设置委托类(设置父类)
        enhancer.setSuperclass(clazz);
        // 设置方法拦截器
        enhancer.setCallback(new DebugMethodInterceptor());
        // 创建代理类
        return enhancer.create();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

从setSuperclass就可以看出为什么CGLIB是基于继承的

实际使用

AliSmsService aliSmsService = 
    (AliSmsService) CglibProxyFactory.getProxy(AliSmsService.class);
aliSmsService.send("Java");
  • 1
  • 2
  • 3

输出结果

before method send
send message:Java
after method send
  • 1
  • 2
  • 3

JDK动态代理和CGLIB的对比
1)JDK 动态代理是基于实现了接口的委托类,通过接口实现代理;而 CGLIB 动态代理是基于继承了委托类的子类,通过子类实现代理。
2)JDK 动态代理只能代理实现了接口的类,且只能增强接口中现有的方法;而 CGLIB 可以代理未实现任何接口的类。
3)就二者的效率来说,大部分情况都是 JDK 动态代理的效率更高,随着 JDK 版本的升级,这个优势更加明显。

常见的还有 Javassist 动态代理机制。和 CGLIB 一样,作为一个 Java 字节码生成框架,Javassist 天生就拥有在运行时动态创建一个类的能力,实现动态代理自然不在话下。 Dubbo 就是默认使用 Javassit 来进行动态代理的。

什么时候使用动态代理?

1)设计模式中有一个设计原则是开闭原则,即对修改关闭,对扩展开放,我们在工作中有时会接手很多前人的代码,里面代码逻辑让人摸不着头脑,就很难去下手修改代码,那么这时我们就可以通过代理对类进行增强。

2)我们在使用 RPC 框架的时候,框架本身并不能提前知道各个业务方要调用哪些接口的哪些方法 。那么这个时候,就可用通过动态代理的方式来建立一个中间人给客户端使用,也方便框架进行搭建逻辑,某种程度上也是客户端代码和框架松耦合的一种表现。

3)Spring 的 AOP 机制同样也是采用了动态代理。

静态代理和动态代理的区别

1、灵活性 :动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的
2、JVM 层面 :静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 .class 字节码文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。

总结

静态代理:
Subject:公共接口
Real Subject:委托类
Proxy:代理类

JDK 动态代理:
Subject:公共接口
Real Subject:委托类
Proxy:代理类
InvocationHandler:处理类,统一调用方法

CGLIB 动态代理
Subject:公共接口
Real Subject:委托类
Proxy:代理类
MethodInterceptor:方法拦截器,统一调用方法

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/煮酒与君饮/article/detail/767952
推荐阅读
相关标签
  

闽ICP备14008679号