学一点点Java的代理模式

289 阅读9分钟

代理模式

代理(proxy)是一种常用的设计模式,其目的就是为其他对象提供一个代理来控制对某个对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理

优点一:可以隐藏委托类的实现。

就像房屋中介一样,或者说二手房东更为贴切。二手房东通常是房东的亲戚,经常直接以自己的名义出租,你可能连真正的房东都不知道。

优点二:可以实现客户与委托类间的解耦,在不修改委托类代码的情况下能够做一些额外的处理。

我们设计类有单一性原则,使每个类(方法)功能尽量单一,是啥做啥。但我们经常需要做日志和权限,你当然能每次都写在方法里,100个方法写100遍吗?所以日志、权限就可以充分展示了这个优点。

代理我们通常分为静态代理动态代理

静态代理

由程序员创建代理类或特定工具自动生成源代码再对其编译,在程序运行前代理类的.class文件就已经存在了。

实现

代理类需要和委托类使用相同的接口, 然后在代理类的方法内穿插执行委托类的方法,因为代理真正调用的还得是委托类的方法

统一接口:UserDao

public interface UserDao {
    // 添加用户
    int add(String user);
}

实现类UserDaoImpl(委托类)

public class UserDaoImpl implements UserDao {
    @Override
    public int add(String user) {
        System.out.println("添加用户" + user);
        return 1;
    }
}

UserDaoProxy(代理类)

public class UserDaoProxy implements UserDao {

    private UserDao userDao;

    public UserDaoProxy(UserDao userDao) {
        this.userDao = userDao;
    }

    @Override
    public int add(String user) {
        System.out.println("开启事务");
        int i = userDao.add(user);
        System.out.println("关闭事务");
        return i;
    }
}

测试

public static void main(String[] args) {
    UserDaoProxy proxy = new UserDaoProxy(new UserDaoImpl());
    proxy.add("money");
}

它可以在不修改目标对象的前提下扩展目标对象的功能,如果只有UserDaoImpl一种实现类还可以用工厂模式直接把new UserDaoImpl()隐藏掉,但缺点也很明显:

  • 委托类和代理类都要实现同样的接口,这个接口一旦加个方法,除了委托类连代理类也要进行实现,不易维护。
  • 一个代理只服务一种类型对象,也就是一个接口一个代理

Java中的静态代理代表就是创建线程

// 实现Runable接口的类
MyThread myThread = new MyThread();  // 相当于被代理类
// Thread本身也实现了Runable接口
Thread thread = new Thread(myThread); //  相当于代理类

thread.start(); // 启动线程;调用run()方法

动态代理

静态代理在大规模需要代理的情况下是很不抗揍的,所以引入了动态代理。在Java说到动态,那肯定和反射有关,所以至少得了解些常用API吧。

这里讲的是基于JDK的动态代理

不同:

  • 动态代理是在程序运行时根据需要动态创建代理类,也就是编译完并没有.class文件,是当需要的时候临时生成载入JVM。

  • 动态代理类不需要实现接口,但是委托类需要实现相应接口。

实现

要想创建一个代理类,需要使用到java.lang.reflect.Proxy类的newProxyInstance方法。

@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException {
        //方法体省略...
}

我们就是通过这个方法创建一个代理类,它有三个参数:

  • ClassLoader:类加载器,可以通过反射中getClass().getClassLoader()获得
  • Class<?>[]:一个Class类数组,每个元素代表委托类需要实现的接口。
  • InvocationHandler: 一个调用处理器。

所以我们只要搞定这个调用处理器就可以使用这个方法创建代理了。ctrl+左键点进去,发现InvocationHandler是一个接口,并且只有一个接口方法invoke

public interface InvocationHandler {
  /**
   * 
   * @param proxy newProxyInstance生成的代理类
   * @param method 被执行的方法
   * @param args  被执行方法的参数
   * @return
   * @throws Throwable
   */
  public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

既然是接口我们就实现它:

  1. 需要维护一个委托类,因为动态创建,并不知道到程序员要我们代理哪个类,使用Object
  2. 实现 InvocationHandler接口的invoke,在里面使用反射执行方法。
public class UserDaoHandler implements InvocationHandler {

    private Object target; // 委托类

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

    @Override
    public Object invoke(Object obj, Method method, Object[] args) throws Throwable {
        System.out.println("委托类:" + target.getClass().getName());
        System.out.println("代理类:" + obj.getClass().getName());
        System.out.println("调用方法:" + method.getName());
        System.out.println("传入参数:" + Arrays.toString(args));
        //---------------------代理---------------------------
        System.out.println("开启事务");
        // 调用target的当前调用方法,并填入参数args
        Object returnValue = method.invoke(target, args);
        System.out.println("关闭事务");
        return returnValue;
    }
}

好了,三个参数集齐了,try it

public static void main(String[] args) {
        // 委托类
        UserDao userDao = new UserDaoImpl();
        // 获取第一个参数:类加载器,如果为系统类可以直接写null的
        ClassLoader classLoader = userDao.getClass().getClassLoader();
        // 获取第二个参数:需要实现的接口,就刚才的UserDao接口
        Class[] needInterface = new Class[]{UserDao.class};
        // 获取第三个参数:刚才写的调用处理器
        InvocationHandler handler = new UserDaoHandler(userDao);
        // 动态生成代理
        UserDao proxy = (UserDao)Proxy.newProxyInstance(classLoader, needInterface, handler);
        proxy.add("Money");
}

结果:

分析

根据代码来看,我们不用自己写UserProxy了,但是写了一个看起来似乎有点类似的调用处理器UserDaoHandler。

首先我们看结果的代理类,很明显不是我们写的,它是由虚拟机中的proxy类帮我们创建的以&proxy开头的类名,其实他就代表了代理类UserProxy。

然后我们针对静态代理的缺点来看:

  1. 委托类和代理类都要实现同样的接口,多一个接口方法,代理类也得跟着修改。

    比如我们加一个void delete(String user)的方法,那静态代理的代理类UserProxy也要手动添加,而调用处理器我们并不做需要任何更改。

  1. 一个接口对应一个代理

    我们在写调用处理器的时候的委托类类型是Object,也就是为谁代理是取决于程序员传入的委托类。所以一个调用处理器就可以应对多个接口,除非你的前后操作内容需要更换。比如我加一个ProductDao和他的实现,调用处理器代码不变,修改main

    public static void main(String[] args) {
        ProductDao productDao = new ProductDaoImpl();
        // 甚至你可以将这两句封装到一个工厂
        InvocationHandler handler = new UserDaoHandler(productDao);
        ProductDao proxy = (ProductDao) Proxy.newProxyInstance(productDao.getClass().getClassLoader()
                , new Class[]{ProductDao.class}, handler);
        proxy.add("可口可乐");
    }
    

第三方动态代理cglib

刚才说基于JDK的动态代理需要实现一个或以上接口,如果不实现接口的话,那就得用cglib。其实它就是一个字节码生成和转换的库。直接上才艺

先添加依赖

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

其实总共需要一个cglib.jar还有一个asm.jar,但maven会传递下载,而我是自己导包结构少了asm.jar还报错了,在解析字节码和生成都需要用到它。

委托类:

public class PersonDao {
    int add(String person) {
        System.out.println("添加一个人:" + person);
        return 1;
    }
}

cglib类代理的基本思想就是给委托类生成一个子类(proxy),并添加一些前后操作,类似于添加一个调用处理器(InvocationHandler)。不过它不止一种,我们这里使用最常用的MethodInterceptor接口,它只有一个接口方法intercept和调用处理器实现的invoke有点类似。

/**
     *
     * @param obj 代理类
     * @param method 被执行的方法
     * @param args  参数
     * @param proxy 这个也有invoke函数用来使用方法,不知道具体是干啥的
     * @return
     * @throws Throwable
     */
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
            throws Throwable {

所以主要两步:

1.使用cglib的工具类生成委托类的子类

  1. 给他安排一个回调接口intercept的实现。

之前一直说工厂,那就整个工厂

public class ProxyFactory implements MethodInterceptor {

    private Object target; // 委托类

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

    public Object getProxyInstance() {
        // cglib的工具
        Enhancer enhancer = new Enhancer();
        // 设置继承委托类
        enhancer.setSuperclass(target.getClass());
        // 设置代理操作, 需要传入一个实现MethodInterceptor的类,本工厂继承了
        enhancer.setCallback(this);
        // 创建并返回这个代理类
        return enhancer.create();
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
            throws Throwable {
        System.out.println("委托类:" + target.getClass().getName());
        // 这里代理类参数名变为obj了
        System.out.println("代理类:" + object.getClass().getName());
        System.out.println("调用方法:" + method.getName());
        System.out.println("传入参数:" + Arrays.toString(args));
        System.out.println("开启事务");
        // 调用target的当前调用方法,并填入参数args
        Object returnValue = method.invoke(target, args);
        // 也可以用第四个参数调用方法
        //Object returnValue = proxy.invoke(target, args);
        System.out.println("关闭事务");
        return returnValue;
    }

}

测试:

public static void main(String[] args) {
        PersonDao personDao = new PersonDao();
        PersonDao proxy = (PersonDao) new ProxyFactory(personDao).getProxyInstance();
        proxy.add("Money");
        System.out.println(proxy.getClass().getName());
    }

结果:

总结

静态代理:

  1. 优缺点

实现简单,但只能为一种委托类进行代理,委托类类型太多,会产生过多代理类。如果接口方法增加,还需要改动代理类。

  1. 代理类编写步骤:

    1. 一个存储委托类的实例域
    2. 带委托类的构造器
    3. 实现和委托类一样的接口方法
    4. 在方法中穿插调用委托类的同方法
    5. new代理类使用

动态代理:

  1. 优缺点

    修复了静态代理的弊端,但委托类必须实现一个或多个接口,使用反射耗系统资源。

  2. 代理类编写步骤:完成Proxy.newProxyInstance的三个参数

    1. 获取第一个参数:类加载器,使用反射a.getClass().getClassLoader()

    2. 获取第二个参数:Class数组,委托类实现什么,我们需要用的是哪些,以A.class的形式创建成Class数组

    3. 写一个调用处理器(实现InvocationHandler接口)

      1. 一个存储委托类的实例域
      2. 带委托类的构造器
      3. 在实现的invoke方法中穿插调用method.invoke(target,args)执行委托类方法
    4. Proxy.newProxyInstance返回Object强转为要使用的接口(必须是在Class数组的)

cglib

  1. 优缺点

    不需要实现接口了,但是它原理是用委托类创建子类重写方法,所以委托类不能被final修饰。

  2. 实现步骤

    1. 创建一个继承MethodInterceptor的类,并实现接口方法intercept
    2. 创建cglib的工具类Enhancer的对象en
    3. 使用en.setSuperclass(Class)设置继承委托类
    4. 使用en.setCallback(Callback)设置代理操作的回调类, 如传入一个实现MethodInterceptor的类
    5. 使用en.create()返回代理类强制转换为委托类类型使用

错了不要打我,偷偷告诉我,谢谢。

参考:

cglib代理的使用:更详细的cglib用法

本文使用 mdnice 排版