十一、GoF之代理模式

1 对代理模式的理解

十一、GoF之代理模式_第1张图片
【在程序中,对象A和对象B无法直接交互时。】
【在程序中,功能需要增强时。】
【在程序中,目标需要被保护时】

业务场景:系统中有A、B、C三个模块,使用这些模块的前提是需要用户登录,也就是说在A模块中要编写判断登录的代码,B模块中也要编写,C模块中还要编写,这些判断登录的代码反复出现,显然代码没有得到复用,可以为A、B、C三个模块提供一个代理,在代理当中写一次登录判断即可。代理的逻辑是:请求来了之后,判断用户是否登录了,如果已经登录了,则执行对应的目标,如果没有登录则跳转到登录页面。【在程序中,目标不但受到保护,并且代码也得到了复用。】

代理模式是GoF23种设计模式之一。属于结构型设计模式。

代理模式的作用是:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个客户不想或者不能直接引用一个对象,此时可以通过一个称之为“代理”的第三者来实现间接引用。代理对象可以在客户端和目标对象之间起到中介的作用,并且可以通过代理对象去掉客户不应该看到的内容和服务或者添加客户需要的额外服务。 通过引入一个新的对象来实现对真实对象的操作或者将新的对象作为真实对象的一个替身,这种实现机制即为代理模式,通过引入代理对象来间接访问一个对象,这就是代理模式的模式动机。


代理模式中的角色:

  • 代理类(代理主题)
  • 目标类(真实主题)
  • 代理类和目标类的公共接口(抽象主题):客户端在使用代理类时就像在使用目标类,不被客户端所察觉,所以代理类和目标类要有共同的行为,也就是实现共同的接口。

代理模式的类图:
十一、GoF之代理模式_第2张图片


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

  • 静态代理
  • 动态代理

2 静态代理

有一个接口和实现类:

package com.powernode.proxy.service;

/**
 * 订单业务接口
 * 代理对象和目标对象的公共接口
 */
public interface OrderService {
    /**
     * 生成订单
     */
    void generate();
    /**
     * 修改订单信息
     */
    void modify();
    /**
     * 查看订单详情
     */
    void detail();
}
package com.powernode.proxy.service.impl;

import com.powernode.proxy.service.OrderService;

/**
 * 目标对象
 */
public class OrderServiceImpl implements OrderService {

    @Override
    // 目标方法
    public void generate() {
        // 模拟生成订单的耗时
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单已生成");
    }

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

    @Override
    public void detail() {
        // 模拟查看订单的耗时
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单详情");
    }
}

项目经理提出一个新的需求:要统计所有业务接口中每一个业务方法的耗时。

  • 解决方案一:硬编码,在每一个业务接口中的每一个业务方法中直接添加统计耗时的程序。

    • 这种方案的缺点:
      • 缺点一:违背OCP开闭原则。
      • 缺点二:代码没有得到复用。(相同的代码写了很多遍。)
  • 解决方案二:编写业务类的子类,让子类继承业务类,对每个业务方法进行重写。

    • 缺点一:虽然解决了OCP开闭原则。但是这种方式会导致耦合度很高,因为采用了继承关系。继承关系是一种耦合度非常高的关系,不建议使用。
    • 缺点二:代码没有得到复用。(相同的代码写了很多遍。)
  • 解决方案三:代理模式。(静态代理)

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

    package com.powernode.proxy.service.impl.static_proxy;
    
    import com.powernode.proxy.service.OrderService;
    
    /**
     * 代理对象(代理对象 和目标对象要具有相同的行为,就要实现公共接口)
     * 客户端在使用代理对象的时候就像在使用目标对象一样
     */
    public class OrderServiceProxy implements OrderService {
    
        // 将目标对象作为代理对象的一个属性.关联关系,比继承关系耦合度低
        // 这里写一个公共接口。因为公共接口耦合度低
        // 目标对象 目标对象一定实现了OrderService接口
        private OrderService target;
    
        // 创建代理对象的时候,传一个目标对象给代理对象
        public OrderServiceProxy(OrderService target) {
            this.target = target;
        }
    
        @Override
        // 代理方法
        public void generate() {
            // 增强
            long begin = System.currentTimeMillis();
            // 调用目标对象的目标方法
            target.generate();
            long end = System.currentTimeMillis();
            System.out.println("耗时"+(end - begin)+"毫秒");
        }
    
        @Override
        public void modify() {
            // 增强
            long begin = System.currentTimeMillis();
            // 调用目标对象的目标方法
            target.modify();
            long end = System.currentTimeMillis();
            System.out.println("耗时"+(end - begin)+"毫秒");
        }
    
        @Override
        public void detail() {
            // 增强
            long begin = System.currentTimeMillis();
            // 调用目标对象的目标方法
            target.detail();
            long end = System.currentTimeMillis();
            System.out.println("耗时"+(end - begin)+"毫秒");
        }
    }
    

    编写客户端程序:

    package com.powernode.proxy.client;
    
    import com.powernode.proxy.service.OrderService;
    import com.powernode.proxy.service.impl.OrderServiceImpl;
    import com.powernode.proxy.service.impl.static_proxy.OrderServiceProxy;
    
    public class Test {
        public static void main(String[] args) {
            // 创建目标对象
            OrderService target = new OrderServiceImpl();
            // 创建代理对象
            OrderService proxy = new OrderServiceProxy(target);
            // 调用代理对象的代理方法
            proxy.generate();
            proxy.modify();
            proxy.detail();
        }
    }
    

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

    优点1:解决了OCP问题。
    优点2:采用代理模式的has a,可以降低耦合度。

目前我们使用的是静态代理,这个静态代理的缺点是什么?
        类爆炸。假设系统中有1000个接口,那么每个接口都需要对应代理类,这样类会急剧膨胀。不好维护。

怎么解决类爆炸问题?
        可以使用动态代理来解决这个问题。

动态代理还是代理模式,只不过添加了字节码生成技术,可以在内存中为我们动态的生成一个class字节码,这个字节码就是代理类。
在内存中动态的生成字节码代理类的技术,叫做:动态代理。

3 动态代理

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

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

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

3.1 JDK动态代理

在静态代理的时候,除了接口和实现类之外,还要写一个代理类OrderServiceProxy
在动态代理中OrderServiceProxy 代理类是可以动态生成的。这个类不需要写。我们直接写客户端程序即可:

package com.powernode.proxy.client;

import com.powernode.proxy.service.OrderService;
import com.powernode.proxy.service.impl.OrderServiceImpl;
import com.powernode.proxy.service.impl.TimerInvocationHandler;

import java.lang.reflect.Proxy;

public class DynamicProxyJDKTest {
    public static void main(String[] args) {
        // 创建目标对象
        OrderService target = new OrderServiceImpl();
        // 创建代理对象
        // 参数:类加载器,代理类要实现的接口,调用处理器
        OrderService proxyObj = (OrderService)Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new TimerInvocationHandler(target));
        // 调用代理对象的代理方法
        proxyObj.generate();
        proxyObj.modify();
        proxyObj.detail();
    }
}

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

static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)

  1. newProxyInstance : 新建代理对象
    也就是说,通过调用这个方法可以创建代理对象。
    本质上,这个Proxy.newProxyInstance()方法的执行,做了两件事:
    第一件事:在内存中动态的生成了一个代理类的字节码class。
    第二件事:new对象了。通过内存中生成的代理类这个代码,实例化了代理对象。
  2. 关于newProxyInstance()方法的三个重要的参数,每一个什么含义,有什么用?
    1. 第一个参数:ClassLoader loader
      类加载器。这个类加载器有什么用呢?
      在内存当中生成的字节码也是class文件,要执行也得先加载到内存当中。加载类就需要类加载器。所以这里需要指定类加载器。
      并且JDK要求,目标类的类加载器必须和代理类的类加载器使用同一个。

    2. 第二个参数:Class[] interfaces
      接口类型
      代理类和目标类要实现同一个接口或同一些接口。
      在内存中生成代理类的时候,这个代理类是需要你告诉它实现哪些接口的。

    3. 第三个参数:InvocationHandler h
      InvocationHandler 被翻译为:调用处理器。是一个接口。
      在调用处理器接口中编写的就是:增强代码。
      既然是接口,就要写接口的实现类。
      这是一个JDK动态代理规定的接口,接口全名:java.lang.reflect.InvocationHandler。显然这是一个回调接口,也就是说调用这个接口中方法的程序已经写好了,就差这个接口的实现类了。

java.lang.reflect.InvocationHandler接口的实现类,并且实现接口中的方法,代码如下:

package com.powernode.proxy.service.impl;

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();

        // 调用目标对象上的目标方法
        // 方法四要素:哪个对象,哪个方法,传什么参数,返回什么类型
        // invoke方法执行过程中,使用method来调用目标对象的目标方法。
        Object retValue = method.invoke(target, args);

        // 目标执行之后增强。
        long end = System.currentTimeMillis();
        System.out.println("耗时" + (end - begin) + "毫秒");

        // 注意这个invoke方法的返回值,如果代理对象调用代理方法之后,需要返回结果的话,invoke方法必须将目标对象的目标方法执行结果继续返回。
        return retValue;
    }
}
  1. invoke方法不是我们负责调用的,JDK负责调用

  2. invoke方法什么时候调用

    当代理对象调用代理方法的时候,注册在InvocationHandler调用处理器当中的invoke()方法被调用

  3. invoke方法的三个参数:

    invoke方法是JDK负责调用的,所以JDK调用这个方法的时候会自动给我们传过来这三个参数。

    我们可以在invoke方法的大括号中直接使用。

    第一个参数:Object proxy 代理对象的引用。设计这个参数只是为了后期的方便,如果想在invoke方法中使用代理对象的话,尽管通过这个参数来使用。

    第二个参数:Method method 目标对象上的目标方法。(要执行的目标方法就是它。)

    第三个参数:Object[] args 目标方法上的实参。

3.2 CGLIB动态代理

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

<dependency>
  <groupId>cglibgroupId>
  <artifactId>cglibartifactId>
  <version>3.3.0version>
dependency>

准备一个没有实现接口的类

package com.dynamic.proxy.cglib.service;

/**
 * 目标类
 */
public class UserService {
    // 目标方法
    public boolean login(String username, String password){
        System.out.println("正在登录");
        if ("admin".equals(username) && "123".equals(password)) {
            return true;
        }
        return false;
    }

    // 目标方法
    public void logout(){
        System.out.println("退出系统");
    }
}

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

package com.dynamic.proxy.cglib.client;

import com.dynamic.proxy.cglib.service.TimerMethodInterceptor;
import com.dynamic.proxy.cglib.service.UserService;
import net.sf.cglib.proxy.Enhancer;

public class Client {
    public static void main(String[] args) {
        // 创建字节码增强器对象
        // 这个对象是CGLIB库中的核心对象,依靠它来生成代理类
        Enhancer enhancer = new Enhancer();

        // 告诉CGLIB父类(目标类)
        enhancer.setSuperclass(UserService.class);

        // 设置回调接口(等同于JDK动态代理中的调用处理器,InvocationHandler)
        // 在CGLIB中是方法拦截器接口:MethodInterceptor
        enhancer.setCallback(new TimerMethodInterceptor());

        // 创建代理对象
        // 这一步做两件事
        // 1. 在内存中生成UserService类的子类(代理类的字节码)
        // 2. 创建代理对象 new
        // 生成源码,编译class,加载到JVM,并创建代理对象
        // 父类是UserService,子类一定也是UserService
        UserService userServiceProxy = (UserService) enhancer.create();

        // CGLIB动态代理生成的代理对象的名字格式
        // com.dynamic.proxy.cglib.service.UserService$$EnhancerByCGLIB$$65b0e71c@5d3411d
        System.out.println(userServiceProxy);

        // 调用代理对象的代理方法
        boolean success = userServiceProxy.login("admin", "123");
        System.out.println(success ? "登录成功" : "登录失败");
        userServiceProxy.logout();
    }
}

底层本质
class UserService$$EnhancerByCGLIB$$65b0e71c extends UserService{}

net.sf.cglib.proxy.MethodInterceptor
编写MethodInterceptor接口实现类:

package com.dynamic.proxy.cglib.service;

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

import java.lang.reflect.Method;

public class TimerMethodInterceptor 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,需要在启动项中添加两个启动参数:
十一、GoF之代理模式_第3张图片

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






JDK动态代理和CGLIB动态代理的区别?

1、JDK动态代理只能代理接口,CGLIB动态代理既可以代理接口,也可以代理类。
2、JDK动态代理的底层是采用实现接口的方式实现的,而CGLIB动态代理底层是使用继承实现的。
3、CGLIB动态代理的效率比JDK动态代理的高。

你可能感兴趣的:(Sping老杜学习笔记,代理模式)