Java设计模式及应用场景之《代理模式》

一、代理模式定义

Provide a surrogate or placeholder for another object to control access to it.
为其它对象提供一种代理以控制对这个对象的访问。

  代理模式是通过创建一个代理对象,用代理对象去代表真实的对象。当客户端去操作这个代理对象的时候,实际上功能最终还是会由真实的对象来完成,只不过是通过代理操作的,也就是客户端操作代理,代理操作真实对象。
  代理对象夹在客户端和真实对象中间,相当于一个中转,那么在中转的时候,我们就可以实现很多的花招,比如,权限控制事务记录日志等。

二、代理模式的结构和说明

1、通过聚合方式实现代理模式(代理对象和真实对象实现相同的接口,代理对象持有真实对象的实例)

在这里插入图片描述
2、通过继承方式实现代理模式(代理对象继承自真实对象)
在这里插入图片描述

聚合比继承更适合代理模式,如果需要叠加多种代理需求,使用聚合方式更加灵活。

  • Subject 目标接口
  • RealSubject 具体的目标对象
  • Proxy 代理对象

三、代理模式的分类

  1. 虚拟代理(Virtual Proxy)
    如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。
  2. 保护代理(Protect Proxy)
    控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限。
  3. 远程代理(Remote Proxy)
    为一个位于不同的地址空间的对象提供一个本地的代理对象,这个不同的地址空间可以是在同一台主机中,也可是在另一台主机中,远程代理又称为大使(Ambassador)。远程代理在Java中最典型的应用就是RMI技术。
  4. 缓冲代理(Cache Proxy)
    为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果。
  5. 智能引用代理(Smart Reference Proxy)
    当一个对象被引用时,提供一些额外的操作,例如将对象被调用的次数记录下来等。
  6. copy on write 代理(Copy on write Proxy)
    只有对象确实改变了,才会真的去拷贝(或克隆)这个对象。
  7. 防火墙代理(Firewall Proxy)
    保护对象不被恶意用户访问和操作。
  8. 同步代理(Synchronization Proxy)
    使多个用户可以同时访问目标对象而不会出现冲突。

四、代理模式示例

  让我们来模拟一个保护代理的场景。有一个订单系统,要求是,一旦订单被创建,只有订单的创建人才能修改订单的订购数量,其他人则不能修改。

OrderApi 订单对象的接口定义(目标接口)

/**
 * 订单对象的接口定义(目标接口)
 */
public interface OrderApi {

    /**
     * 修改订单数量
     * @param orderNum 订单数量
     * @param user 修改人
     */
    void changeOrderNum(int orderNum,String user);

}

Order 订单对象(具体的目标对象)

/**
 * 订单对象(具体的目标对象)
 */
@Data
public class Order implements OrderApi{

    /**
     * 订单订购的产品名称
     */
    private String productName;
    /**
     * 订单订购的数量
     */
    private int orderNum;
    /**
     * 创建订单的人员
     */
    private String orderUser;

    /**
     * 构造方法,传入构建需要的数据
     * @param productName 订单订购的产品名称
     * @param orderNum 订单订购的数量
     * @param orderUser 创建订单的人员
     */
    public Order(String productName,int orderNum,String orderUser){
        this.productName = productName;
        this.orderNum = orderNum;
        this.orderUser = orderUser;
    }

    @Override
    public void changeOrderNum(int orderNum, String user) {
        this.orderNum = orderNum;
    }

    @Override
    public String toString() {
        return "Order{" +
                "productName='" + productName + '\'' +
                ", orderNum=" + orderNum +
                ", orderUser='" + orderUser + '\'' +
                '}';
    }
}

OrderProxy 订单的代理对象

/**
 * 订单的代理对象
 */
public class OrderProxy implements OrderApi{

    /**
     * 持有被代理的具体的目标对象
     */
    private Order order;
    /**
     * 构造方法,传入被代理的具体的目标对象
     * @param realSubject 被代理的具体的目标对象
     */
    public OrderProxy(Order realSubject){
        this.order = realSubject;
    }

    @Override
    public void changeOrderNum(int orderNum, String user) {
        //控制访问权限,只有创建订单的人员才能够修改
        if(Objects.equals(user, order.getOrderUser())){
            order.changeOrderNum(orderNum,user);
        }else{
            System.out.println("对不起"+user+",您无权修改订单中的产品名称。");
        }
    }

    @Override
    public String toString() {
        return "OrderProxy{" +
                "order=" + order +
                '}';
    }
}

以上,保护代理已经设计好了,下边我们来模拟一下使用。

public class Client {

    public static void main(String[] args) {
        //张三先登录系统创建了一个订单
        OrderApi order = new OrderProxy(new Order("泡面",1,"张三"));
        //李四想要来修改,那就会报错
        order.changeOrderNum(3, "李四");
        //输出order
        System.out.println("李四修改后订单记录没有变化:"+order);
        //张三修改就不会有问题
        order.changeOrderNum(3, "张三");
        //再次输出order
        System.out.println("张三修改后,订单记录:"+order);
    }

}

执行main方法后,得到下边的输出:

对不起李四,您无权修改订单中的产品名称。
李四修改后订单记录没有变化:OrderProxy{order=Order{productName=‘泡面’, orderNum=1, orderUser=‘张三’}}
张三修改后,订单记录:OrderProxy{order=Order{productName=‘泡面’, orderNum=3, orderUser=‘张三’}}

五、动态代理

  像前边定义的这种,代理类在程序运行前就已经存在,代理类所实现的接口和所代理的方法都被固定,这种代理方式被称为静态代理
  静态代理会存在这样的缺点:如果Subject(目标接口)中方法发生变化,那么真实类和代理类都需要跟着变化;如果需要被代理的真实类有很多,就需要相应的创建很多个代理类。
  那么,有没有一种机制能够让系统在运行时动态创建代理类,并可以对多个真实类进行代理操作?有,这就是我们将要介绍的这种动态代理(Dynamic Proxy)。
  动态代理是一种较为高级的代理模式,它在事务管理、AOP(Aspect-OrientedProgramming,切面编程)等领域都发挥了重要的作用。

1、JDK动态代理

  从JDK 1.3开始,Java语言提供了对动态代理的支持,Java语言实现动态代理时需要用到位于java.lang.reflect包中的一些类,一般主要用到到以下两个类:

  1. InvocationHandler 接口
    InvocationHandler接口是代理处理程序类的实现接口,该接口作为代理实例的调用处理者的公共父类,每一个代理类的实例都可以提供一个相关的具体调用处理者(InvocationHandler接口的子类)。
    客户端在调用动态代理对象的方法时,调用请求会将请求自动转发给InvocationHandler对象的invoke()方法,由invoke()方法来实现对请求的统一处理。

  2. Proxy
    Proxy类提供了用于创建动态代理类和实例对象的方法。

JDK动态代理使用步骤
  1. 创建一个实现接口 InvocationHandler 的类,重写invoke方法。
  2. 通过 Proxy 的静态方法 newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 创建一个代理对象。
  3. 通过代理对象调用方法。
JDK动态代理示例

假设我们的订单系统需要实现一个日志功能,在用户做一些关键操作时,将操作信息记录到数据库中。

UserService 用户业务层的接口定义(目标接口)

/**
 * 用户业务层的接口定义
 */
public interface UserService {

    /**
     * 修改密码
     */
    void changePwd();
}

UserServiceImpl 用户业务层的具体处理类(真实的对象)

/**
 * 用户业务层的具体处理类
 */
public class UserServiceImpl implements UserService{

    @Override
    public void changePwd() {
        System.out.println("用户修改密码");
    }
}

以上,目标接口和实现类已经有了,接下来我们来定义一个处理类,声明一下代理中具体要执行的动作。

LogHandler 使用JDK动态代理时具体处理者

/**
 * 使用JDK动态代理时具体处理者
 */
public class LogHandler implements InvocationHandler {

    /**
     * 被代理的真实对象
     */
    private Object object;

    public LogHandler(Object object) {
        this.object = object;
    }

    /**
     * @param proxy 代理对象,Proxy.newProxyInstance方法的返回对象
     * @param method 调用的方法
     * @param args 方法中的参数
     * @return 调用方法的返回值
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println(String.format("%s.%s 方法被调用 -- 保存到数据库", object.getClass().getSimpleName(),method.getName()));
        return method.invoke(object, args);
    }
}

我们来看下怎么来使用JDK动态代理

public class Client {

    public static void main(String[] args) {
        // 创建一个用户业务的真实处理对象
        UserService userService = new UserServiceImpl();
        // 创建处理程序实现类(InvocationHandler)
        LogHandler logHandler = new LogHandler(userService);
        Class cls = userService.getClass();

        /**
         * 创建代理对象
         * 三个参数分别为:类加载器、目标接口、处理程序实现类(InvocationHandler)
         */
        UserService userServiceProxy = (UserService) Proxy.newProxyInstance(cls.getClassLoader(), cls.getInterfaces(), logHandler);
        // 执行修改密码方法
        userServiceProxy.changePwd();
    }
    
}

执行main方法后,得到下边的输出:

UserServiceImpl.changePwd 方法被调用 – 保存到数据库
用户修改密码

如果我们的目标接口中方法有变动,如新增一个方法:

UserService 用户业务层的接口定义(目标接口)

/**
 * 用户业务层的接口定义
 */
public interface UserService {

    /**
     * 修改密码
     */
    void changePwd();

    /**
     * 修改收货地址(新增方法)
     */
    void changeAddr();
}

UserServiceImpl 用户业务层的具体处理类(具体的目标对象)

/**
 * 用户业务层的具体处理类
 */
public class UserServiceImpl implements UserService{

    @Override
    public void changePwd() {
        System.out.println("用户修改密码");
    }

    @Override
    public void changeAddr() {
        System.out.println("用户修改收货地址");
    }
}

我们新增的方法加好了,我们再来看看动态代理能否对新方法有效

public class Client {

    public static void main(String[] args) {
        // 创建一个用户业务的真实处理对象
        UserService userService = new UserServiceImpl();
        // 创建处理程序实现类(InvocationHandler)
        LogHandler logHandler = new LogHandler(userService);
        Class cls = userService.getClass();

        /**
         * 创建代理对象
         * 三个参数分别为:类加载器、目标接口、处理程序实现类(InvocationHandler)
         */
        UserService userServiceProxy = (UserService) Proxy.newProxyInstance(cls.getClassLoader(), cls.getInterfaces(), logHandler);
        // 执行修改密码方法
        userServiceProxy.changePwd();
        // 执行修改收货地址方法
        userServiceProxy.changeAddr();

    }

}

执行main方法后,得到下边的输出:

UserServiceImpl.changePwd 方法被调用 – 保存到数据库
用户修改密码
UserServiceImpl.changeAddr 方法被调用 – 保存到数据库
用户修改收货地址

从输出中,我们可以看出,动态代理对新增的方法同样有效。

那么,如果我们需要代理的类非常多怎么办?来,安排上。

OrderService 订单业务层的接口定义(目标接口)

/**
 * 订单业务层的接口定义
 */
public interface OrderService {

    /**
     * 保存订单
     */
    void saveOrder();

}

OrderServiceImpl 订单业务层的具体处理类(具体的目标对象)

/**
 * 订单业务层的具体处理类
 */
public class OrderServiceImpl implements OrderService{

    @Override
    public void saveOrder() {
        System.out.println("保存订单信息");
    }
}

新的接口和具体实现类也加好了,我们再来看看动态代理能否有效

public class Client {

    public static void main(String[] args) {
        // 创建一个订单业务的真实处理对象
        OrderService orderService = new OrderServiceImpl();
        // 创建处理程序实现类(InvocationHandler)
        LogHandler logHandler = new LogHandler(orderService);
        Class cls = orderService.getClass();

        /**
         * 创建代理对象
         * 三个参数分别为:类加载器、目标接口、处理程序实现类(InvocationHandler)
         */
        OrderService orderServiceProxy = (OrderService) Proxy.newProxyInstance(cls.getClassLoader(), cls.getInterfaces(), logHandler);
        // 执行代理对象的保存订单方法
        orderServiceProxy.saveOrder();

    }
}

执行main方法后,得到下边的输出:

OrderServiceImpl.saveOrder 方法被调用 – 保存到数据库
保存订单信息

同样有效,这样一来,我们就不需要像静态代理那样创建那么多的代理类了。

JDK动态代理实现原理

  利用Java的反射技术(Java Reflection),在运行时创建一个实现给定目标接口的新类(也称“动态代理类”),将产生的新类进行编译生成class文件,将class字节码文件Load进内存,最后创建一个此动态代理类的对象。

JDK动态代理局限性

  使用JDK动态代理,要求委托类必须实现一个接口,但事实上并不是所有类都有接口,对于没有实现接口的类,便无法使用该方式实现动态代理。

2、CGLIB动态代理

  前面介绍了JDK动态代理的局限性,它要求被代理类必须实现一个接口。
  而CGLIB动态代理则没有此类强制性要求。CGLib动态代理是代理类去继承目标类,然后重写目标类的方法。既然是继承,则被代理类不能为final,并且 final 和static 修饰的方法也不会被拦截。
  使用CGLIB动态代理,需要引入cglib.jar这个Jar包,一般主要用到到以下两个类:

  1. MethodInterceptor 接口
    MethodInterceptor 接口是一个方法拦截器接口,接口中只有一个intercept回调方法,我们要实现的业务在这个方法中添加即可。
  2. Enhancer
    CGLIB通过Enhancer类来动态创建代理类和实例对象。
CGLIB动态代理使用步骤
  1. 创建一个实现接口 MethodInterceptor 的类,重写 intercept 方法。
  2. 通过 Enhancer 的 create 方法创建一个代理对象。
  3. 通过代理对象调用方法。
CGLIB动态代理示例

我们还拿前面JDK动态代理中的示例来做下演示。为了演示CGLIB对没有实现接口的类也可以实现动态代理,我们这里先把接口去掉。

OrderServiceImpl 订单业务层的具体处理类

/**
 * 订单业务层的具体处理类
 */
public class OrderServiceImpl{

    public void saveOrder() {
        System.out.println("保存订单信息");
    }
}

我们来创建一个方法拦截器。

LogMethodInterceptor 使用CGLIB动态代理时具体处理者

/**
 * 使用CGLIB动态代理时具体处理者
 */
public class LogMethodInterceptor implements MethodInterceptor {

    /**
     * @param obj CGLib动态生成的代理类实例
     * @param method 被代理的方法
     * @param args 方法的参数
     * @param methodProxy 代理类对方法的代理
     * @return 
     * @throws Throwable
     */
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println(String.format("%s.%s 方法被调用 -- 保存到数据库", method.getDeclaringClass().getSimpleName(),method.getName()));
        return methodProxy.invokeSuper(obj,args);
    }
}

我们来看下怎么来使用CGLIB动态代理

public class Client {

    public static void main(String[] args) {
        // 创建拦截器(MethodInterceptor)
        LogMethodInterceptor logMethodInterceptor = new LogMethodInterceptor();
        // 创建Enhancer对象,类似于JDK动态代理的Proxy类
        Enhancer enhancer = new Enhancer();
        // 设置被代理类,也就是父类
        enhancer.setSuperclass(OrderServiceImpl.class);
        // 设置回调函数
        enhancer.setCallback(logMethodInterceptor);
        // 这里的create方法就是正式创建代理类
        OrderServiceImpl orderServiceProxy = (OrderServiceImpl) enhancer.create();

        // 执行代理对象的保存订单方法
        orderServiceProxy.saveOrder();

    }
}

执行main方法后,得到下边的输出:

OrderServiceImpl.saveOrder 方法被调用 – 保存到数据库
保存订单信息

CGLIB动态代理实现原理

  CGLIB会让生成的代理类继承被代理类,并在代理类中对代理方法进行强化处理(前置处理、后置处理等)。在CGLIB底层,其实是借助了ASM这个非常强大的Java字节码生成框架。生成字节码文件,再将生成的字节码文件Load进内存,最后创建一个动态代理类对象。
  使用字节码技术生成代理类,比使用Java反射效率要高。

CGLIB动态代理注意事项

  CGLib实现动态代理,不受代理类必须实现接口的限制,但是它不能对 final 类进行代理,也不能拦截被 final 和 static 修饰的方法。

六、三种代理方式的对比

代理方式优点缺点
静态代理实现简单,容易理解。被代理对象增删等变动方法时,代理类也需要改变。并且,如果被代理的类很多,也需要相应创建很多个代理类。
JDK动态代理动态创建代理类,代码复用率高 。只能够代理实现了接口的委托类。
CGLIB动态代理动态创建代理类,且被代理类无需实现接口。使用字节码技术生成代理类,比使用Java反射效率要高。不能对 final 类,以及 final 和 static 修饰的方法进行代理。

七、代理模式的应用场景及案例

  • 需要为一个对象在不同的地址空间提供局部代表的时候,可以使用远程代理。
  • 需要按需创建开销很大的对象的时候,可以使用虚拟代理。
  • 需要控制对象访问权限的时候,可以使用保护代理。
  • 需要对访问对象执行一些附加操作的时候,可以使用智能引用代理。
  • Hibernate 的延迟加载(Lazy Load)就是虚拟代理的典型应用。
  • Spring AOP就是动态代理的典型应用。
  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

晓呆同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值