设计模式(十三):代理模式

Published on 2024-03-14 17:17 in 分类: 博客 with 狂盗一枝梅
分类: 博客

一、代理模式定义

代理模式(Proxy Pattern)是一个使用率非常高的模式,其定义如下:

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

代理模式通用类图如下所示

代理模式通用类图

上图中,Subject是一个抽象类或者接口,RealSubject是实现方法类,具体的业务执行,Proxy则是RealSubject的代理,直接和client接触的。

代理模式分为静态代理和动态代理两种类型。

二、静态代理

静态代理是代理模式的一种实现方式,它在编译时期就确定了代理类和被代理类的关系,代理类在编译时就已经存在。在静态代理中,代理类和被代理类需要实现同样的接口或继承同样的父类,以便代理类可以接收和处理与被代理类相同的方法调用。

上面的“代理模式通用类图”就是静态代理的体现。

静态代理的基本思想是在代理类中创建一个对被代理对象的引用,并在代理类中实现接口或继承父类的方法。当调用代理类的方法时,代理类会在适当的时机调用被代理类的方法,并可以在调用前后进行一些额外的处理。

下面是一个简单的示例来说明静态代理的概念:

// 定义一个接口
public interface Image {
    void display();
}

// 实现接口的具体类
public class RealImage implements Image {
    private String filename;

    public RealImage(String filename) {
        this.filename = filename;
    }

    public void display() {
        System.out.println("Displaying image: " + filename);
    }
}

// 代理类,实现与被代理类相同的接口
public class ImageProxy implements Image {
    private String filename;
    private RealImage realImage;

    public ImageProxy(String filename) {
        this.filename = filename;
    }

    public void display() {
        if (realImage == null) {
            realImage = new RealImage(filename);
        }
        // 在调用被代理对象的方法前后可以进行一些额外的处理
        System.out.println("Proxy: Before displaying image");
        realImage.display();
        System.out.println("Proxy: After displaying image");
    }
}

静态代理的优劣势:

使用静态代理的好处是可以在代理类中添加额外的逻辑,例如权限验证、性能监控、日志记录等,而不需要修改被代理类的源代码。静态代理的缺点是每个被代理类都需要创建一个对应的代理类,当代理类较多时会增加代码量和维护成本。

三、动态代理

动态代理是在运行时期创建代理对象的一种代理模式实现方式。与静态代理不同,动态代理不需要在编译时期就确定代理类和被代理类的关系,而是在运行时期动态地创建代理对象并关联到被代理对象上。

在java的世界中,动态代理有两种常用的实现方式:JDK动态代理和cglib动态代理。

1、JDK动态代理

JDK动态代理是Java提供的一种动态代理机制,它是基于接口的代理模式实现方式

JDK动态代理的核心类是java.lang.reflect.Proxyjava.lang.reflect.InvocationHandlerProxy类提供了创建代理对象的静态方法newProxyInstance(),该方法可以根据指定的类加载器、被代理接口和代理处理器来动态地创建代理对象。InvocationHandler接口定义了一个方法invoke(),代理处理器实现该方法来处理代理对象的方法调用。

1.1 一个简单的案例演示

这里来举个计算器的例子,当然这不是典型的应用场景,只是为了演示下JDK动态代理的使用

定义接口

interface Calculator {
    int add(int a, int b);
}

实现接口的具体类

class CalculatorImpl implements Calculator {
    @Override
    public int add(int a, int b) {
        return a + b;
    }
}

代理处理器类

class CalculatorProxyHandler implements InvocationHandler {
    private Object target;

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before method invocation");
        Object result = method.invoke(target, args);
        System.out.println("After method invocation");
        return result;
    }
}

使用JDK动态代理

public class Main {
    public static void main(String[] args) {
        Calculator calculator = new CalculatorImpl();

        Calculator proxy = (Calculator) Proxy.newProxyInstance(
                Calculator.class.getClassLoader(),
                new Class[]{Calculator.class},
                new CalculatorProxyHandler(calculator)
        );

        int result = proxy.add(2, 3);
        System.out.println("Result: " + result);
    }
}

运行结果:

Before method invocation
After method invocation
Result: 5

初学者看到这个案例其实很懵,不懂为什么要脱裤子放屁多此一举弄个代理对象出来,不能直接调用实现类吗?

如果只是这个案例,这个怀疑是完全是正当的,就是脱裤子放屁多此一举。但是要知道,JDK动态代理在真正使用的时候绝对不会这么使用,上面的案例只是演示它的用法而已。

为了更好的说明JDK动态代理的精妙之处,下面说下在动态代理在Feign框架中的使用。

1.2 Feign框架源码解析

先看下Feign的常用使用方式。

首先定义一个Feign接口

public interface UserApi {
    @GetMapping("/user")
    UserInfo getUserById(@RequestParam("userId") String userId);
}

然后构造UserApi对象

import feign.Feign;
import feign.Request;
import feign.jackson.JacksonDecoder;
import feign.jackson.JacksonEncoder;

public class FeignClientExample {
    public static void main(String[] args) {
        UserApi client = Feign.builder()
                .encoder(new JacksonEncoder())
                .decoder(new JacksonDecoder())
                .options(new Request.Options(5000, 5000))
                .target(UserApi.class, "http://xxx");

        String userInfo = client.getUserById();
        System.out.println("UserInfo: " + userInfo);
    }
}

重点是.target(UserApi.class, "http://xxx");这行代码,点进去看一看它的实现,经过多层调用,最终看到了如下代码实现

public <T> T newInstance(Target<T> target) {
    Map<String, MethodHandler> nameToHandler = this.targetToHandlersByName.apply(target);
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap();
    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList();
    Method[] var5 = target.type().getMethods();
    int var6 = var5.length;

    for(int var7 = 0; var7 < var6; ++var7) {
        Method method = var5[var7];
        if (method.getDeclaringClass() != Object.class) {
            if (Util.isDefault(method)) {
                DefaultMethodHandler handler = new DefaultMethodHandler(method);
                defaultMethodHandlers.add(handler);
                methodToHandler.put(method, handler);
            } else {
                methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
            }
        }
    }

    InvocationHandler handler = this.factory.create(target, methodToHandler);
    T proxy = Proxy.newProxyInstance(target.type().getClassLoader(), new Class[]{target.type()}, handler);
    Iterator var12 = defaultMethodHandlers.iterator();

    while(var12.hasNext()) {
        DefaultMethodHandler defaultMethodHandler = (DefaultMethodHandler)var12.next();
        defaultMethodHandler.bindTo(proxy);
    }

    return proxy;
}

重点看下这行代码

T proxy = Proxy.newProxyInstance(target.type().getClassLoader(), new Class[]{target.type()}, handler);

构造完成之后最后返回了proxy对象,也就是说调用的target方法实际上最后拿到的是JDK动态代理对象

有人可能会疑惑了,为啥这里的动态代理接口没有实现类呢?要知道,JDK动态代理真正在实践过程中,其实很少有实现类的,在这里,Feign调用本质上就是将本地方法调用转化为http请求,所以它不仅没有实现类,而且本质上还不应该有实现类。

1.3 JDK动态代理总结

JDK动态代理使用频率非常高,这个代理方式告诉我们:就算没有实现类,哪怕只有一个接口,我也能实例化它,从这个角度上来看,其实是很神奇的一件事,利用这个,我们可以做很多扩展,比如上面演示的是Feign调用,那么其它诸如消息队列这种消息类型的是否可以按照这种思路设计出更优雅的组件呢?

JDK动态代理是基于接口的代理方式,这是它的优点,也是它的缺点,假如我们只有一个类,那该如何实现代理呢?答案就是使用哦CGLIB代理。

2、CGLIB动态代理

CGLIB(Code Generation Library)是一个基于字节码生成的动态代理库,它可以在运行时期生成代理类来实现对目标类的代理。相对于JDK动态代理只能代理接口的限制,CGLIB可以代理任意的类,包括没有实现接口的类。

CGLIB动态代理的原理是通过生成目标类的子类来实现代理。在子类中,代理类继承了目标类,并重写了目标类中的方法,在重写的方法中可以添加额外的逻辑来实现代理的功能。CGLIB使用了ASM库来操作字节码,生成代理类的过程相对于JDK动态代理而言更加灵活和高效。

很显然,我们平时使用CGLIB动态代理太少太少,基本上清一色的JDK动态代理,但是这里还是要说下它。

Spring已经内置了cglib相关的类,在Spring中可以直接使用CGLIB相关的类。

首先,定义一个Service类

public class MyService {
    public void doSomething() {
        System.out.println("Doing something...");
    }
}

再定义一个方法拦截器

import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class MyMethodInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("Before method invocation");
        Object result = proxy.invokeSuper(obj, args);
        System.out.println("After method invocation");
        return result;
    }
}

最后,启动类如下

import org.springframework.cglib.proxy.Enhancer;

/**
 * @author kdyzm
 * @date 2024/3/14
 */
public class Main {

    public static void main(String[] args) {
        MyMethodInterceptor proxyExample = new MyMethodInterceptor();
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(MyService.class);
        enhancer.setCallback(proxyExample);
        MyService proxy = (MyService) enhancer.create();
        proxy.doSomething();
    }
}

运行结果

Before method invocation
Doing something...
After method invocation

可以看到,似乎比JDK动态代理使用起来还要简单一些。

CGLIB动态代理广泛应用于AOP框架组件,例如 Spring AOP,去实现方法拦截,使用Cglib代理,目标对象无须实现任何的接口

四、几种常见的代理模式变体

1、远程代理(Remote Proxy):远程代理用于在不同的地址空间中代表对象。它可以隐藏对象存在于远程服务器上的事实,使得客户端可以像访问本地对象一样访问远程对象。远程代理负责处理网络通信、序列化和反序列化等细节,使得客户端可以透明地使用远程对象。

2、虚拟代理(Virtual Proxy):虚拟代理延迟创建或加载对象,直到真正需要使用时才创建实际的对象。虚拟代理常用于对资源密集型对象的访问控制,例如大型图片或视频文件的加载,只有当需要显示时才加载实际的文件。

3、安全代理(Protection Proxy):安全代理用于控制对目标对象的访问权限。代理类在调用目标对象的方法之前进行权限验证,只有在满足特定条件的情况下才允许访问目标对象。

4、缓存代理(Caching Proxy):缓存代理用于为开销较大的操作结果提供缓存,以避免重复执行相同的操作。当客户端请求某个操作的结果时,代理首先检查缓存中是否存在,如果存在则直接返回缓存的结果,否则代理将执行操作并将结果保存到缓存中。



END.


#设计模式
目录
复制 复制成功