Java 代理模式

Java 代理模式

Posted by 金志宏 on March 21, 2019

Java 代理模式

1. 什么是代理模式?

代理模式:为其他对象提供一种代理以便控制对这个对象的访问。可以详细控制访问某个类(对象)的方法,在调用这个方法前作的前置处理(统一的流程代码放到代理中处理)。调用这个方法后做后置处理。

例1:明星和经纪人、租房中介、电视和遥控机等。

1.1 代理模式怎么实现

  1. 抽象对象角色(抽象类 或 接口) 声明目标类和代理类相同的接口,在任何可以使用目标对象的地方都可以使用代理对象。
  2. 定义实际的目标对象,被代理对象所代表。
  3. 代理对象角色,代理对象内部含有实际对象的引用,从而可以操作实际对象。代理对象和实际对象具有统一接口,以便可以替代实际对象。代理对象通常在客户端调用传递给实际对象之前或者之后,执行某些操作,而非单纯的将调用传递给实际对象。

1.2 代理模式分类

  1. 静态代理(静态定义代理类,我们自己静态定义的代理类。比如我们自己定义电视和遥控机类)
  2. 动态代理(通过程序动态生成代理类,该代理类不是我们自己定义的。而是由程序自动生成)
  • JDK自带动态代理
  • CGLIB(重点介绍)
  • javaassist 字节码操作实现
  • ASM(底层使用命令)

2. 静态代理

我们以 电器、电视机、遥控器 举例

Electric 为一个电器的抽象类,其中有off 关闭和 on 开启电器的抽象方法

IVolume 是一个调节声音的接口

Television 是电视机的实现类,他继承了电器抽象类,由于电视机是可以调节音量的,所以又继承了音量接口

eutkgx.png

RemoteControl 遥控器 代理电视机对象,在构造方法里初始化了要代理的对象(电视机) 然后通过继承电器和实现音量接口,来调用遥控电视的方法,从而达到操作电视的目的。

eutAv6.png

2.1 代码实现

//电器抽象类
public abstract class Electric {
		 //开启电器
     abstract void off();
		 //关闭电器
     abstract void on();
}
//音量调节接口
public interface IVolume {
		
    void volumeUp();
    
    void volumeDown();
}
//电视机实现类
public class Television extends Electric implements IVolume {
    
    public Television() {
    }


    @Override
    void off() {
        System.out.println(" Television is off");
    }

    @Override
    void on() {
        System.out.println(" Television is on");
    }

    @Override
    public void volumeUp() {
        System.out.println(" Television volume up");
    }

    @Override
    public void volumeDown() {
        System.out.println(" Television volume down");
    }
}
//遥控器代理
public class RemoteControl extends Electric implements IVolume {

    //代理电视机
    private Television television;

    //构造器初始化
    public RemoteControl(Television television) {
        this.television = television;
    }

    @Override
    void off() {
        television.off();
    }

    @Override
    void on() {
        television.on();
    }

    @Override
    public void volumeUp() {
        television.volumeUp();
    }

    @Override
    public void volumeDown() {
        television.volumeDown();
    }
}
//测试
public static void main(String[] args) {
        RemoteControl remoteControl = new RemoteControl(new Television());
        remoteControl.on();
        remoteControl.off();
        remoteControl.volumeUp();
        remoteControl.volumeDown();
    }

//输出:
 Television is on
 Television is off
 Television volume up
 Television volume down

从以上例子可以看出,代理对象 “遥控器” 将客户端的调用委派给了实际对象“电视机”,其实在调用对象实际方法之前还可以进行一些额外的操作。

2.2 使用场景

如果已有的方法在使用的时候需要对原有的方法进行改进,此时有两种办法:

  1. 修改原有的方法来适应。显然这违反了“对扩展开放,对修改关闭”的原则。
  2. 采用一个代理类调用原来的方法,且对产生的结果进行控制。这就是代理模式了

使用代理模式可以将功能划分的更加清晰,有助于后期的维护,以上所说的这种代理模式称为静态代理。

3. 动态代理

3.1 动态代理(JDK)

以经纪人和明星举例,上代码:

//明星接口
public interface Star {
		//唱歌
    void sing(String name);
		//跳舞
    void dance(String name);
}
//接口实现
public class LiuDeHua implements Star {

    @Override
    public void sing(String name) {
        System.out.println("刘德华开始唱: " + name);
    }

    @Override
    public void dance(String name) {
        System.out.println("刘德华开始跳: " + name);
    }
}
//工厂模式的代理
public class ProxyFactory implements InvocationHandler {

    private Object target;

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

    public Object getProxyInstance(){
        Object object = Proxy.
                newProxyInstance(
                        target.getClass().getClassLoader(),
                        target.getClass().getInterfaces(),
                        this);
        return object;
    }

  	//重写invoke
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      //代理前执行方法
        if(method.getName().equals("sing")){
            System.out.println("支付唱歌出场费10万");
        }else if (method.getName().equals("dance")) {
            System.out.println("支付跳舞出场费20万");
        }
        Object o = method.invoke(target,args);
        return o;
    }
}
//测试
public class ProxyTest {
    public static void main(String[] args) {
        Star star = (Star) new ProxyFactory(new LiuDeHua()).getProxyInstance();
        star.sing("忘情水");
        star.dance("江南style");
    }
}
//输出
支付唱歌出场费10
刘德华开始唱: 忘情水
支付跳舞出场费20
刘德华开始跳: 江南style

JDK 动态代理总结:

动态代理的接口和实现类和静态代理一样,主要是看代理类的实现: 每个动态代理类都必须要实现一个 InvocationHandler接口,且每个代理类实例都关联到了一个handler,当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由InvocationHandler这个接口的 invoke 方法来进行调用。

我们一般调用方法都是采用a.method这种方式,代码会直接走到method方法中,而使用了动态代理类,我们需要通过InvocationHandler中的invoke来调用该方法,这样做有什么好处呢,最浅显的理解,就是在invoke中,除了通过method.invoke(object, args)来调用该方法以外,还可以做其他操作,如以上代码所示。经纪人在明星唱歌和跳舞前收取了一定的费用。在写好代理类后,需要创建一个代理类的实例,来调用方法,

创建方式为:Proxy.newProxyInstance,他接受三个参数

  1. ClassLoader loader:指定当前目标对象使用的类加载器,也就是委托类,获取加载器的方法是固定的
  2. Class<?>[] interfaces:指定目标对象实现的接口的类型,也就是委托类的接口,使用泛型方式确认类型
  3. InvocationHandler:指定动态处理器,也就是制定代理类,执行目标对象的方法时,会触发事件处理器的方法。

JDK 动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样,重实现接口的每个方法,每一个方法都需要进行中转。而且动态代理的应用使我们的类职责更加单一,复用性更强;但是:动态代理只能代理实现了接口的类,没有实现接口的类不能实现 JDK 动态代理。这时我们就需要用到Cglib代理。

3.2 Cglib 动态代理

Cglib 采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。但因为采用的是继承,所以不能对final修饰的类进行代理。JDK动态代理与 Cglib 动态代理均是实现Spring AOP的基础。Cglib 不是 java 自带的API,我们要使用 Cglib 代理必须引入 Cglib 的jar包。

我们以用户 登录小说APP看书举例,上代码:

public class User {

    //用户登录session
    public static Map userSession = new HashMap();

    private String name ;

    public User(String name) {
        this.name = name;
    }

    public User() {
    }
    
    public void login(){
        System.out.println("用户 [" + name + "] 登录了");
    }

    public void read(){
        System.out.println("用户 [" + name + "] 在看书");
    }

    public void logout(){
        System.out.println("用户 [" + name + "] 退出登录");
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}
public class UserProxyimplements MethodInterceptor {

    @Override
    public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {

        User user = (User)object;

        if(method.getName().equals("read")){
            //判断是否登录
            if(User.userSession.get(user.getName()) == null){
                System.out.println("用户 [" + user.getName() + "] 未登录,无法看书");
                return null;
            }
        }
        Object result = methodProxy.invokeSuper(object,args);
        if(method.getName().equals("login")){
            //登录后设置session
            User.userSession.put(user.getName(),"session");
        }else if(method.getName().equals("logout")){
            //清空session
            User.userSession.remove(user.getName());
        }

        return result;
    }
}
public class ProxyTest {

    public static void main(String[] args) {
        
        User proxuUser = (User)Enhancer.create(User.class,new UserProxy());
        proxuUser.setName("张三");

        proxuUser.read();
        proxuUser.login();
        proxuUser.read();
        proxuUser.logout();
        proxuUser.read();
    }
}

//输出
用户 [张三] 未登录,无法看书
用户 [张三] 登录了
用户 [张三] 在看书
用户 [张三] 退出登录
用户 [张三] 未登录,无法看书

代理类定义了一个拦截器,在调用目标方法之前,Cglib 回调MethodInterceptor接口方法拦截,来实现自己的业务逻辑,类似于JDK中的InvocationHandler接口。也就是通过intercept 调用methodProxy.invokeSuper来调用委托类的方法,而在intercept中可以做其他工作。

public Object intercept(Object object , Method method , Object[] args, MethodProxy methodProxy)

  1. object:为Cglib动态生成的代理实例
  2. method:为上文中实体类所调用的被代理的方法调用
  3. args:为method参数数值列表
  4. methodProxy:为生成代理类对方法的代理引用
  5. 返回:从代理实例方法调用返回的值
  6. 其中,methodProxy.invokeSuper(object,args):调用代理类实例上的proxy方法的父类方法

4. 三种代理方式总结

Cglib 创建的动态代理对象比JDK创建的动态代理对象的性能更高,但是Cglib 创建代理对象时所花费的时间却比JDK多得多。所以对于单例的对象,因为无需频繁创建对象,用 Cglib 合适,反之使用 JDK 方式要更为合适一些。同时由于 Cglib 由于是采用动态创建子类的方法,对于 final 修饰的方法无法进行代理。

代理方式 实现 优点 缺点
静态代理 代理类与委托类实现同一接口,并且在代理类中需要硬编码接口 实现简单,易理解 代理类需要硬编码接口,在实际应用中可能会导致重复编码,浪费存储空间并且效率很低
JDK 动态代理 代理类与委托类实现同一接口,主要是通过代理类实现InvocationHandler并重写invoke方法来进行动态代理的,在invoke方法中将对方法进行增强处理 不需要硬编码接口,代码复用率高 只能够代理实现了接口的委托类
Cglib 动态代理 代理类将委托类作为自己的父类并为其中的非final委托方法创建两个方法,一个是与委托方法签名相同的方法,它在方法中会通过super调用委托方法;另一个是代理类独有的方法。在代理方法中,它会判断是否存在实现了MethodInterceptor接口的对象,若存在则将调用intercept方法对委托方法进行代理 可以在运行时对类或者是接口进行增强操作,且委托类无需实现接口 不能对final类以及final方法进行代理

[JAVA设计模式之代理模式][https://baijiahao.baidu.com/sid=1627813818268632512&wfr=spider&for=pc]

[代理模式Proxy][https://www.cnblogs.com/meet/p/5116464.html]

[代理模式使用场景][https://blog.csdn.net/u012133048/article/details/83585976]