设计模式之代理模式

概念

代理模式是最常用的设计模式之一,也是未来我们将要学习的 Spring AOP 的理论基础。

"代理" 是一个比较通用的词,作为一个软件设计模式,它在《设计模式》一书中被提出。编程领域中的代理的基本概念和日常生活中的概念是类似的。

代理背后一般至少有一个实际对象,代理的外部功能和实际对象一般是一样的,用户与代理打交道,不直接接触实际对象。

虽然外部功能和实际对象一样,但代理有它存在的价值,比如:

1.按需延迟加载,创建代理时并不真正创建实际对象,而只是保存实际对象的地址,在需要时再加载或创建。

  1. 执行权限检查,代理检查权限后,再调用实际对象。

  2. 屏蔽网络差异和复杂性,代理在本地,而实际对象在其它服务器上,调用本地代理时,本地代理请求其他服务器。

代理模式能让我们在不改变目标类源代码的情况下,让它的代码功能能"多"出来几行,这种看似矛盾的要求,就是代理模式最大的价值所在。

代理模式是我们必须要掌握的几种设计模式之一。

设计模式中的适配器模式和装饰器模式与代理模式有点类似,它们的背后都有一个别的实际对象,都是通过组合的方式指向该对象。不同之处在于:

  • 适配器是提供了一个不一样的新接口,装饰器是对原接口起到了"装饰"作用,可能是增加了新接口、修改了原有的行为等;
  • 而代理一般不改变接口。

静态代理

静态代理是理解动态代理的前提和基础,是缓和动态代理的学习曲线的有利工具。通过静态代理,我们能实现如下看似矛盾的需求:在不能改变目标类源码的情况下,我们需要为目标类"新增"代码。

  1. 静态代理的接口实现方案

接口和被代理类的实现

java 复制代码
public interface Animal {
    void eat();
}
public class Cat implements Animal {
    public void eat() {
        System.out.println("猫吃鱼");
    }
}

代理类的实现

java 复制代码
public class CatProxy implements Animal {
     private Cat target;
     
     public CatProxy() {
           this.target = new Cat();
     }

     @Override
    public void eat() {
        System.out.println("执行前调用");
        target.eat();
        System.out.println("执行后调用");
    }
}

从上述的 UML 图可以看到,JDK 动态代理有一个限制:它要求被代理对象必须有一个接口 。

在整个方案中,如果 Animal 接口并不存在,那么就无法使用这个方案就无从实现。

  1. 静态代理的继承实现方案

被代理类的实现

java 复制代码
public class Cat {
    public void eat() {
        System.out.println("猫吃鱼");
    }
}

代理类的实现

java 复制代码
public class CatProxy extends Cat {
    public CatProxy() {
    }

    @Override
    public void eat() {
        System.out.println("执行前调用");
        super.eat();
        System.out.println("执行后调用");
    }
}

代理的继承实现方案对 Cat 类有无接口不作要求。不过继承也带来一个代价,那就是如果 Cat 类中有final 方法,那么 CatProxy 无法对 final 方法进行增强。

代理模式是最常用、最重要的几种设计模式之一,通过代理模式,你能够实现看似不动原方法的前提下,为原方法新增代码的"神奇"功能。

Java SDK 动态代理

代理模式是 AOP 思想的必备概念,JDK 动态代理方案是实现代理模式的二种方式之一。

动态代理是一种强大的功能,它可以在运行时动态创建一个类,实现一个或多个接口,可以在不修改原有类的基础上动态地为通过该类获取的对象添加方法、修改行为。

动态代理有 2 种实现方式:

实现方式

1 一种是 Java SDK 提供的

2 一种是第三方库 (如 cglib) 提供的

在动态代理中,代理类是动态生成的。

接口和被代理类的实现

java 复制代码
public interface Animal {
       void eat();
}

public class Cat implements Animal {
    public void eat() {
        System.out.println("猫吃鱼");
    }
}

这里和静态代理中的 Animal 接口以及 Cat 类是一样的。

调用处理类

java 复制代码
public class SimpleInvocationHandler implements InvocationHandler {
    private Object realObj;

    public SimpleInvocationHandler(Object realObj) {
        this.realObj = realObj;
    }

    @Override
    public Object invoke(Object proxy,
    Method method,

    Object[] args) throws Throwable {
        System.out.println("entering " + method.getName());
        Object result = method.invoke(realObj, args);
        System.out.println("leaving " + method.getName());
        return result;
    }
}

在 SimpleInvocationHandler 的 invoke 实现中,我们调用了 method 的 invoke 方法,传递了实际对象realObj 作为参数,达到了调用实际对象对应方法的目的。

需要注意的是:

不能将 proxy 作为参数传递给 method.invoke 方法。

比如:

Object result = method.invoke(proxy, args);

上面的语句会出现死循环,因为 proxy 表示当前代理对象,这又会调用到 SimpleInvocationHandler 的invoke 方法。

生成动态代理对象

java 复制代码
Animal cat = new Cat();

Animal proxyCat = (Animal) Proxy.newProxyInstance(
                            Animal.class.getClassLoader(),
                            new Class<?>[]{Animal.class},
                            new SimpleInvocationHandler(cat)
                            );

proxyCat.eat();

代理对象的创建使用了 java.lang.reflect 包中的 Proxy 类的静态方法 newProxyInstance 来创建。这个方法的声明如下:

java 复制代码
public static Object newProxyInstance(
            ClassLoader loader,
            Class<?>[] interfaces,
            InvocationHandler h)

它有 3 个参数,具体如下:

基本原理解释

在 main 方法中加入如下一行代码:

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

javac 在编译项目时,会将 JDK 生成的 Cat 的动态代理类导出到一个 $Proxy0 的类文件中,它的内容如下:

java 复制代码
public final class $Proxy0 extends Proxy implements Animal {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;

public $Proxy0(InvocationHandler var1) throws {
    super(var1);
}

public final boolean equals(Object var1) throws {
    try {
        return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
    } catch (RuntimeException | Error var3) {
        throw var3;
    } catch (Throwable var4) {
        throw new UndeclaredThrowableException(var4);
    }
}

public final void eat() throws {
    try {
        super.h.invoke(this, m3, (Object[])null);
    } catch (RuntimeException | Error var2) {
        throw var2;
    } catch (Throwable var3) {
        throw new UndeclaredThrowableException(var3);
    }
}

public final String toString() throws {
    try {
        return (String)super.h.invoke(this, m2, (Object[])null);
    } catch (RuntimeException | Error var2) {
        throw var2;
    } catch (Throwable var3) {
        throw new UndeclaredThrowableException(var3);
    }
}

public final int hashCode() throws {
    try {
        return (Integer)super.h.invoke(this, m0, (Object[])null);
    } catch (RuntimeException | Error var2) {
        throw var2;
    } catch (Throwable var3) {
        throw new UndeclaredThrowableException(var3);
    }
}

static {
    try {
        m1 = Class.forName("java.lang.Object").getMethod("equals",
        Class.forName("java.lang.Object"));
        m3 = Class.forName("com.woniu.example.Animal").getMethod("eat");
        m2 = Class.forName("java.lang.Object").getMethod("toString");
        m0 = Class.forName("java.lang.Object").getMethod("hashCode");
    } catch (NoSuchMethodException var2) {
        throw new NoSuchMethodError(var2.getMessage());
    } catch (ClassNotFoundException var3) {
        throw new NoClassDefFoundError(var3.getMessage());
    }
}
}

$Proxy0 的父类是 Proxy,它有一个构造方法,接受一个 InvocationHandler 类型的参数,保存为实例变量 h ( h 定义在父类 Proxy 中 ) 。

Proxy0 实现了接口 Animal ,对于每个方法,如 eat 方法,它调用 InvocationHandler 的 invoke 方法;对于 Object 中的方法,如 hashCode、equals 和 toString,Proxy0 同样转给 InvocationHandler。

可以看出,Proxy0 类的主要与接口数组有关,给定这个接口数组,它动态创建了每个接口的实现代码,而具体的实现就是:转发给 InvocationHandler 。Proxy0 对象与被代理对象的关系以及对它的调用由InvocationHandler 的实现负责。

CGLib 动态代理

Java SDK 动态代理的局限在于:它只能为接口创建代理,返回的代理对象也只能转换到某个接口类型,如果一个类没有接口,或者希望代理非接口中定义的方法,那就没有办法了。

有一个第三方的类库:cglib (https://github.com/cglib/cglib) ,可以做到这一点,Spring、Hibernate 等框架都在使用该类库。

  1. 接口和被代理类
java 复制代码
public interface Animal {
    void eat();
}

public class Cat implements Animal {
    public void eat() {
        System.out.println("猫吃鱼");
    }
}

这里和静态代理中的 Animal 接口以及 Cat 类是一样的。

  1. 实现代理逻辑
java 复制代码
class SimpleInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object object,
                            Method method,
                            Object[] args,
                            MethodProxy proxy) throws Throwable {
            System.out.println("before " + method.getName());
            Object result = proxy.invokeSuper(object, args);
            System.out.println("after" + method.getName());
            return result;
        }
}

在这里我们实现了 MethodInterceptor 接口,它与 Java SDK 的 InvocationHandler 有点类似,方法名称变成了 intercept ,多了一个 MethodProxy 类型的参数。

与 JDK 动态代理方案中的 InvocationHandler 不同,这里的 SimpleInterceptor 中没有被代理的对象,它通过 MethodProxy#invokeSuper 方法调用被代理类的方法。

  1. 获得代理对象,并验证
java 复制代码
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Cat.class);
enhancer.setCallback(new SimpleInterceptor());
Cat cat = (Cat) enhancer.create();
cat.eat();

在上面的例子中:

Cat 是被代理的类,它可以没有接口。

getProxy 方法能够为一个类生成代理对象,这个代理对象可以安全地转换为被代理类型,它使用了cglib 的 Enhancer 类。

Enhancer 类的 getSuperclass 方法用来设置被代理的类,setCallback 方法用来设置被代理类的方法( 必须是 public 且非 final) 被调用时执行的代理逻辑。

SimpleInterceptor 实现了 MethodInterceptor 接口,用来封装代理逻辑。

cglib 是实现动态代理的第二种方案,它没有 JDK 动态代理的缺点,它不要求被代理类必须是某接口的实现类。

但是它的缺点是它不能代理 final 类,因为 final 类不能有子类。

使用代理完成统一的事务控制对事务进行管理,是代理模式的经典使用场景之一。

  1. 展示没有使用代理的原始事务控制版本

原始事务控制版本:

java 复制代码
/**
* SingletonBeanFactory 是自定义的用于创建单例对象的工具类。
* JdbcUtils 是自定义的用于数据库操作的工具类,简化代码。
*/
public class BankService {
    private AccountDao accountDao = SingletonBeanFactory.getAccountDao();
    dprivate HikariDataSource dataSource = SingletonBeanFactory.getHikariDataSource();

    public void transfer(Long fromAccountId, Long toAccount, Long money) {
        Connection connection = null;
        try {
            connection = dataSource.getConnection();
            connection.setAutoCommit(false);
            accountDao.updateAccount(connection, fromAccountId, -money);
            accountDao.updateAccount(connection, fromAccountId, money);
            connection.commit();
        } catch (SQLException | NullPointerException e) {
            e.printStackTrace();
           JdbcUtils.connectionRollback(connection);
        } finally {
            JdbcUtils.closeConnection(connection);
        }
    }
}
  1. 通过代理改进 Service,实现 Service 无事务控制版本

通过 Cglib 实现 Service 的代理对象:

java 复制代码
import  MethodInterceptor;
import  MethodProxy;

public class TransactionInterceptor implements MethodInterceptor {
    private HikariDataSource dataSource = SingletonBeanFactory.getHikariDataSource();
    
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy
                                        methodProxy) throws Throwable {

        Connection connection = dataSource.getConnection();
        connection.setAutoCommit(false);
        ConnectionContainer.setConnectionInThreadLocal(connection);
        try {
            Object result = methodProxy.invokeSuper(o, objects);
            return result;
        } catch (RuntimeException e) {
            e.printStackTrace();
            JdbcUtils.connectionRollback(connection);
        } finally {
            JdbcUtils.closeConnection(connection);
        }
            ConnectionContainer.clearConnection();
            throw new NullPointerException();
        }
}

改造 SingletonBeanFactory 的 getBankService 方法,返回的并非 BankService 对象,而是它的代理对象:

java 复制代码
private static volatile BankService bankService;

    public static BankService getBankService() {
        if (bankService == null) {
            synchronized (BankService.class) {
                if (bankService == null) {
                    bankService = new BankService();
                    Enhancer enhancer = new Enhancer();
                    enhancer.setSuperclass(BankService.class);
                    enhancer.setCallback(new TransactionInterceptor());
                    bankService = (BankService) enhancer.create();
                 }
            }
        }
        return bankService;
}

这样,BankService 中的与事务有关的逻辑就被挪到了代理对象的方法中,并且,Dao 方法也不需要显示传递 Connection 对象了:

java 复制代码
public class BankService {
private AccountDao accountDao = SingletonBeanFactory.getAccountDao();
        public void transfer(Long fromAccountId, Long toAccount, Long money) {
                    accountDao.updateAccount(fromAccountId, -money);
                    accountDao.updateAccount(fromAccountId, money);
        }
}

小结

  1. 为了实现位于同一个事务中,被 Service 调用的多个 Dao 必须使用同一个 Connection 对象。

  2. 实现多个 Dao 使用同一个 Connection 对象的方式有 2 种:

  • Service 中获得 Connection 对象,传入它所调用的多个 Dao 中。

  • 通过 ThreadLocal<Connection> 间接传参。

  1. 通过 ThreadLocal<Connection>,我们可以将 Service 中与Connection 有关的代码挪到 Service 的代理类中,从而实现统一事务控制。
相关推荐
V+zmm1013418 分钟前
基于微信小程序的乡村政务服务系统springboot+论文源码调试讲解
java·微信小程序·小程序·毕业设计·ssm
ProcessOn官方账号22 分钟前
如何绘制网络拓扑图?附详细分类解说和用户案例!
网络·职场和发展·流程图·拓扑学
Oneforlove_twoforjob43 分钟前
【Java基础面试题025】什么是Java的Integer缓存池?
java·开发语言·缓存
xmh-sxh-13141 小时前
常用的缓存技术都有哪些
java
Ven%1 小时前
如何在防火墙上指定ip访问服务器上任何端口呢
linux·服务器·网络·深度学习·tcp/ip
AiFlutter1 小时前
Flutter-底部分享弹窗(showModalBottomSheet)
java·前端·flutter
神的孩子都在歌唱1 小时前
TCP/IP 模型中,网络层对 IP 地址的分配与路由选择
网络·tcp/ip·智能路由器
阿雄不会写代码1 小时前
ubuntu安装nginx
linux·服务器·网络
J不A秃V头A2 小时前
IntelliJ IDEA中设置激活的profile
java·intellij-idea
starstarzz2 小时前
计算机网络实验四:Cisco交换机配置VLAN
网络·计算机网络·智能路由器·vlan·虚拟局域网