Spring6 - (14) GoF之代理模式

在这里插入图片描述

Spring6 -(14)GoF之代理模式

1. 对代理模式的理解

场景:拍电影的时候,演员找替身演员,这就是一个代理模式

替身演员 去代理 演员 完成表演

思考:演员为什么要找替身呢?(为什么要使用代理模式呢?)

  • 第一个原因:怕自己受伤,找个替身(保护自己)
  • 第二个原因:自己完成不来这种高难度的动作,替身演员可以完成(功能增强)
  • A对象无法和B对象直接交互时,也可以使用代理模式来解决

Java程序中的代理模式的作用:

  • 当一个对象需要受到保护的时候,可以考虑使用代理对象去完成某个行为
  • 需要给某个对象的功能个进行增强的时候,可以考虑找一个代理进行增强

代理模式中有一个非常重要的特点:对于客户端程序来说,使用代理对象时就像在使用目标对象一样。【在程序中,目标需要被保护时】

代理模式中有三大角色:

  • 目标对象(演员)
  • 代理对象(替身演员)
  • 目标对象和代理对象的公共接口(演员和替身演员应该具有相同的行为动作)

为什么演员和替身演员要有相同的行为动作呢?

是因为不想让观众知道是替身演员。这里的观众其实就是“客户端程序”,如果你使用代理模式的话,对应客户端程序来所,客户端是无法察觉到的,客户端在使用代理对象的时候就象在使用目标对象。

代理模式的类图:

在这里插入图片描述

代理模式在代码实现上,包括两种形式:

  • 静态代理
  • 动态代理

2. 静态代理

代理类是程序员提前写好的,成为静态代理

下面是静态动力的演示:

定义一个接口:OrderService

package com.julissa.proxy.service;

public interface OrderService {
    /**
     * 生成订单
     */
    void generate();

    /**
     * 查看订单详情
     */
    void detail();

    /**
     * 修改订单
     */
    void modify();
}

定义接口的实现类:OrderServiceImpl

package com.julissa.proxy.service;

public class OrderServiceImpl implements OrderService{
    @Override
    public void generate() {
        //模拟生成订单的耗时
        try {
            Thread.sleep(1234);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单已生成");
    }

    @Override
    public void detail() {
        //模拟查看订单的耗时
        try {
            Thread.sleep(2541);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单信息如下:******");
    }

    @Override
    public void modify() {
        //模拟修改订单的耗时
        try {
            Thread.sleep(1010);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单已修改");
    }
}

客户端程序:

package com.julissa.proxy.client;

import com.julissa.proxy.service.OrderService;
import com.julissa.proxy.service.OrderServiceImpl;

public class Test {
    public static void main(String[] args) {
        OrderService orderService = new OrderServiceImpl();
        orderService.generate();
        orderService.modify();
        orderService.detail();
    }
}

其中Thread.sleep()方法的调用是为了模拟操作耗时。

项目已上线,并且运行正常,只是客户反馈系统有一些地方运行较慢,要求项目组对系统进行优化。于是项目负责人就下达了这个需求。首先需要搞清楚是哪些业务方法耗时较长,于是让我们统计每个业务方法所耗费的时长。如果是你,你该怎么做呢?

第一种方案:直接修改Java源代码,在每个业务方法中添加统计逻辑,如下:

package com.julissa.proxy.service;

public class OrderServiceImpl implements OrderService{
    @Override
    public void generate() {
        //模拟生成订单的耗时
        long begin = System.currentTimeMillis();
        try {
            Thread.sleep(1234);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单已生成");
        long end = System.currentTimeMillis();
        System.out.println("耗费时长"+(end - begin)+"毫秒");
    }

    @Override
    public void detail() {
        //模拟查看订单的耗时
        long begin = System.currentTimeMillis();
        try {
            Thread.sleep(2541);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单信息如下:******");
        long end = System.currentTimeMillis();
        System.out.println("耗费时长"+(end - begin)+"毫秒");
    }

    @Override
    public void modify() {
        //模拟修改订单的耗时
        long begin = System.currentTimeMillis();
        try {
            Thread.sleep(1010);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单已修改");
        long end = System.currentTimeMillis();
        System.out.println("耗费时长"+(end - begin)+"毫秒");
    }
}

需求可以满足,但显然是违背了OCP开闭原则,而且代码没有得到复用,这种方案不可取。

第二种方案:编写一个子类继承OrderServiceImpl,在子类中重写每个方法,代码如下:

package com.julissa.proxy.service;

public class OrderServiceImplSub extends OrderServiceImpl{
    @Override
    public void generate() {
        long begin = System.currentTimeMillis();
        super.generate();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }

    @Override
    public void detail() {
        long begin = System.currentTimeMillis();
        super.detail();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }

    @Override
    public void modify() {
        long begin = System.currentTimeMillis();
        super.modify();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }
}

这种方式可以解决,但是存在两个问题:

  • 第一个问题:假设系统中有100个这样的业务类,需要提供100个子类,并且之前写好的创建Service对象的代码,都要修改为创建子类对象。
  • 第二个问题:由于采用了继承的方式,导致代码之间的耦合度较高。

这种方案也不可取。

第三种方案:使用代理模式(这里采用静态代理)

可以为OrderService接口提供一个代理类。

package com.julissa.proxy.service;

public class OrderServiceProxy implements OrderService{// 代理对象(代理对象和目标对象要有相同的行为或方法,所有要实现公共接口)

   // 将目标对象作为代理对象的属性,这种关系叫做关联关系,比继承关系的耦合度低
    private OrderService orderService;

    // 通过构造方法将目标对象传递给代理对象
    public OrderServiceProxy(OrderService orderService) {
        this.orderService = orderService;
    }
    @Override
    public void generate() {// 代理方法
        //增强
        long begin = System.currentTimeMillis();
        // 执行目标对象的目标方法
        orderService.generate();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }

    @Override
    public void detail() {// 代理方法
        //增强
        long begin = System.currentTimeMillis();
        // 执行目标对象的目标方法
        orderService.detail();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }

    @Override
    public void modify() {// 代理方法
        //增强
        long begin = System.currentTimeMillis();
        // 执行目标对象的目标方法
        orderService.modify();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }
}

这种方式的优点:符合OCP开闭原则,同时采用的是关联关系,所以程序的耦合度较低。所以这种方案是被推荐的。

编写客户端程序:

package com.julissa.proxy.client;

import com.julissa.proxy.service.OrderService;
import com.julissa.proxy.service.OrderServiceImpl;
import com.julissa.proxy.service.OrderServiceProxy;

public class Client {
    public static void main(String[] args) {
        // 创建目标对象
        OrderService target = new OrderServiceImpl();
        // 创建代理对象
        OrderService proxy = new OrderServiceProxy(target);
        // 调用代理对象的代理方法
        proxy.generate();
        proxy.modify();
        proxy.detail();
    }
}

运行结果:

在这里插入图片描述

以上就是代理模式中的静态代理,其中OrderService接口是代理类和目标类的共同接口。OrderServiceImpl是目标类。OrderServiceProxy是代理类。

大家思考一下:如果系统中业务接口很多,一个接口对应一个代理类,显然也是不合理的,会导致类爆炸。怎么解决这个问题?动态代理可以解决。因为在动态代理中可以在内存中动态的为我们生成代理类的字节码。代理类不需要我们写了。类爆炸解决了,而且代码只需要写一次,代码也会得到复用。

3. 动态代理

在程序运行阶段,在内存中动态生成代理类,被称为动态代理,目的是为了减少代理类的数量。解决代码复用的问题。

在内存当中动态生成类的技术常见的包括:

  • JDK动态代理技术:只能代理接口。
  • CGLIB动态代理技术:CGLIB(Code Generation Library)是一个开源项目。是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。它既可以代理接口,又可以代理类,底层是通过继承的方式实现的。性能比JDK动态代理要好。(底层有一个小而快的字节码处理框架ASM。)
  • Javassist动态代理技术:Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态"AOP"框架。

3.1 JDK动态代理

还是使用静态代理中的例子:一个接口和一个实现类。

OrderService

package com.julissa.proxy.service;

public interface OrderService {
    /**
     * 生成订单
     */
    void generate();

    /**
     * 查看订单详情
     */
    void detail();

    /**
     * 修改订单
     */
    void modify();
}

OrderServiceImpl

package com.julissa.proxy.service;

public class OrderServiceImpl implements OrderService{
    @Override
    public void generate() {
        //模拟生成订单的耗时
        try {
            Thread.sleep(1234);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单已生成");
    }

    @Override
    public void detail() {
        //模拟查看订单的耗时
        try {
            Thread.sleep(2541);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单信息如下:******");
    }

    @Override
    public void modify() {
        //模拟修改订单的耗时
        try {
            Thread.sleep(1010);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单已修改");
    }
}

我们在静态代理的时候,除了以上一个接口和一个实现类之外,还需要写一个代理类UserServiceProxy!

在动态代理中UserServiceProxy代理类是可以动态生成的。这个类不需要写。我们直接写客户端程序即可:

package com.julissa.proxy.client;

import com.julissa.proxy.service.OrderService;
import com.julissa.proxy.service.OrderServiceImpl;

import java.lang.reflect.Proxy;

public class Client {
    public static void main(String[] args) {
        //创建目标对象
        OrderService target = new OrderServiceImpl();
        //创建代理对象
        Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),调用处理器对象)
        //调用代理对象的代理方法
    }
}

OrderService orderServiceProxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), 调用处理器对象);

这行代码做了两件事:

  • 第一件事:在内存中生成了代理类的字节码class
  • 第二件事:创建代理对象

Proxy类全名:java.lang.reflect.Proxy。这是JDK提供的一个类(所以称为JDK动态代理)。主要是通过这个类在内存中生成代理类的字节码。

其中newProxyInstance()方法有三个参数:

  • 第一个参数:类加载器。在内存中生成了字节码,要想执行这个字节码,也是需要先把这个字节码加载到内存当中的,加载类就需要类加载器,并且JDK要求,目标类的的类加载器类和代理类的类加载器要使用同一个。
  • 第二个参数:接口类型。代理类和目标类实现相同的接口,所以要通过这个参数告诉JDK动态代理生成的类要实现哪些接口。
  • 第三个参数:调用处理器。这是一个JDK动态代理规定的接口,接口全名:java.lang.reflect.InvocationHandler。显然这是一个回调接口,也就是说调用这个接口中方法的程序已经写好了,就差这个接口的实现类了,在调用处理器接口编写的代码,就是增强代码。

所以接下来我们要写一下java.lang.reflect.InvocationHandler接口的实现类,并且实现接口中的方法,代码如下:

package com.julissa.proxy.service;

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

public class TimerInvocationHandler implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return null;
    }
}

InvocationHandler接口中有一个方法invoke,这个invoke方法上有三个参数:

  • 第一个参数:Object proxy。代理对象。设计这个参数只是为了后期的方便,如果想在invoke方法中使用代理对象的话,尽管通过这个参数来使用。
  • 第二个参数:Method method。目标对象上的目标方法(要执行的目标方法就是它)。
  • 第三个参数:Object[] args。目标方法调用时要传的参数。

我们将来肯定是要调用“目标方法”的,但要调用目标方法的话,需要“目标对象”的存在,“目标对象”从哪儿来呢?我们可以给TimerInvocationHandler提供一个构造方法,可以通过这个构造方法传过来“目标对象”,代码如下:

package com.julissa.proxy.service;

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

public class TimerInvocationHandler implements InvocationHandler {

    // 目标对象
    private Object target;

    // 通过构造方法来传目标对象
    public TimerInvocationHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return null;
    }
}

有了目标对象我们就可以在invoke()方法中调用目标方法了。代码如下:

package com.julissa.proxy.service;

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

public class TimerInvocationHandler implements InvocationHandler {

    // 目标对象
    private Object target;

    // 通过构造方法来传目标对象
    public TimerInvocationHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 目标执行之前增强。
        long begin = System.currentTimeMillis();
        // 调用目标对象的目标方法
        //四要素:那个对象,哪个方法,什么参数。返回什么值
        Object retValue = method.invoke(target, args);
        // 目标执行之后增强。
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
        // 一定要记得返回
        return retValue;
    }
}

到此为止,调用处理器就完成了。接下来,应该继续完善Client程序:

package com.julissa.proxy.client;

import com.julissa.proxy.service.OrderService;
import com.julissa.proxy.service.OrderServiceImpl;
import com.julissa.proxy.service.TimerInvocationHandler;

import java.lang.reflect.Proxy;

public class Client {
    public static void main(String[] args) {
        //创建目标对象
        OrderService target = new OrderServiceImpl();
        //创建代理对象
        //实现了OrderService接口,所以可以向下转型
        OrderService proxyObj =(OrderService) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new TimerInvocationHandler(target));
        //调用代理对象的代理方法
        proxyObj.generate();
        proxyObj.detail();
        proxyObj.modify();
    }
}

运行客户端程序

image-20230323143352236

InvocationHandler接口中的invoke()方法什么时候被调用?

注意:当你调用代理对象的代理方法的时候,注册在InvocationHandler接口中的invoke()方法会被调用,注册在InvocationHandler接口中的invoke()方法都会被调用。

可以看到,不管你有多少个Service接口,多少个业务类,这个TimerInvocationHandler接口是不是只需要写一次就行了,代码是不是得到复用了

而且最重要的是,以后程序员只需要关注核心业务的编写了,像这种统计时间的代码根本不需要关注。因为这种统计时间的代码只需要在调用处理器中编写一次即可。

到这里,JDK动态代理的原理就结束了。

不过我们看以下这个代码确实有点繁琐,对于客户端来说,用起来不方便:

image-20230323143517491

我们可以提供一个工具类:ProxyUtil,封装一个方法:

package com.julissa.proxy.utils;

import com.julissa.proxy.service.OrderService;
import com.julissa.proxy.service.TimerInvocationHandler;

import java.lang.reflect.Proxy;

public class ProxyUtil {
    /**
     * 获取代理对象
     * @param target 目标对象
     * @return 代理对象
     */
    public static OrderService getProxyObj(Object target){
         //底层调用JDK动态代理
        OrderService proxyObj =(OrderService) Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new TimerInvocationHandler(target));
        return proxyObj;
    }
}

这样客户端代码就不需要写那么繁琐了:

package com.julissa.proxy.client;

import com.julissa.proxy.service.OrderService;
import com.julissa.proxy.service.OrderServiceImpl;
import com.julissa.proxy.service.TimerInvocationHandler;
import com.julissa.proxy.utils.ProxyUtil;

import java.lang.reflect.Proxy;

public class Client {
    public static void main(String[] args) {
        //创建目标对象
        OrderService target = new OrderServiceImpl();
        //创建代理对象
        //实现了OrderService接口,所以可以向下转型
        OrderService proxyObj = ProxyUtil.getProxyObj(target);
        //调用代理对象的代理方法
        proxyObj.generate();
        proxyObj.detail();
        proxyObj.modify();
    }
}

3.2 CGLIB动态代理

CGLIB既可以代理接口,又可以代理类。底层采用继承的方式实现。所以被代理的目标类不能使用final修饰。

使用CGLIB,需要引入它的依赖:

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

我们准备一个没有实现接口的类,如下:

package com.julissa.proxy.service;

/**
 * 目标类
 */
public class UserService {
    /**
     * 用户登陆
     * @param username 用户名
     * @param password 密码
     * @return 登陆结果
     */
    public Boolean login(String username, String password){
        System.out.println("系统正砸验证信息");
        if ("admin".equals(username) && "123456".equals(password)) {
            return true;
        }
        return false;
    }

    /**
     * 用户注销
     */
    public void logout(){
        System.out.println("用户退出");
    }
}

使用CGLIB在内存中为UserService类生成代理类,并创建对象

package com.julissa.proxy.client;

import com.julissa.proxy.service.UserService;
import net.sf.cglib.proxy.Enhancer;

/**
 * 客户端程序
 */
public class Client2 {
    public static void main(String[] args) {
        // 创建字节码增强器
        // 这个对象是CGLib库中的核心对象,就是依靠它生成代理类
        Enhancer enhancer = new Enhancer();
        // 告诉cglib要继承哪个类
        enhancer.setSuperclass(UserService.class);
        // 设置回调函数(等同于JDK动态代理的调用处理器)
        enhancer.setCallback(new UserServiceInterceptor());
        // 创建代理对象
        // 这一步会做两件事:
        // 1.在内存中生成UserService的子类,其实就是代理类的字节码
        // 2. 创建代理对象
        UserService userService = (UserService) enhancer.create();
        // 调用代理对象的方法
        Boolean aBoolean = userService.login("admin", "123456");
        System.out.println(aBoolean?"登陆成功":"登陆失败");
        userService.logout();
    }
}

编写MethodInterceptor接口实现类:

package com.julissa.proxy.service;

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class UserServiceInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object target, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        // 前增强
        long begin = System.currentTimeMillis();
        // 调用目标
        Object retValue = methodProxy.invokeSuper(target, objects);
        // 后增强
        long end = System.currentTimeMillis();
        System.out.println("耗时" + (end - begin) + "毫秒");
        // 一定要返回
        return retValue;
    }
}

MethodInterceptor接口中有一个方法intercept(),该方法有4个参数:

第一个参数:目标对象

第二个参数:目标方法

第三个参数:目标方法调用时的实参

第四个参数:代理方法

对于高版本的JDK,如果使用CGLIB,需要在启动项中添加两个启动参数:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7WCayQAn-1679894878459)(null)]

image-20230323150801496

image-20230323150833516

image-20230323150850589

image-20230323150938271

  • –add-opens java.base/java.lang=ALL-UNNAMED
  • –add-opens java.base/sun.net.util=ALL-UNNAMED

运行客户端程序

image-20230323151019366

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值