静态代理还是动态代理?来聊聊Java中的代理设计模式

代理模式(Proxy Design Pattern)是一种结构型设计模式,为一个对象提供一个代理对象,然后使用代理对象控制对原对象的引用。即通过代理对象访问目标对象。被代理的对象可以是远程对象、创建开销大的对象或需要安全控制的对象。

一、代理模式介绍

代理模式主要有两个部分:

  • 抽象主题:声明一个公共接口,给代理类和真实对象进行实现。让真实对象和代理对象一一对应
  • 真实主题:定义所要代理的真实对象,其中包括实际的业务逻辑和操作
  • 代理主题:首先代理对象有真实对象的所有接口,保证能完全替代真实对象。此外还会有其他的方法和操作,来扩展相关的功能

其主要结构的UML图如下所示:

  1. Subject:抽象主题类,通过接口或抽象类声明主题和代理对象实现的业务方法
  2. RealSubject:真实主题类,实现Subject中的具体业务,是代理对象所代表的真实对象
  3. ProxySubject:代理类,其内部含有对真实主题的引用,它可以访问、控制或扩展RealSubject的功能
  4. Client:客户端,通过使用代理类来访问真实的主题类

按照上面的类图,可以实现如下代码:

java 复制代码
//主题类接口
public interface Subject {
    void Request();
}

//真实的主题类
public class RealSubject implements Subject{

    @Override
    public void Request() {
        System.out.println("我是真实的主题类");
    }
}

//代理类
public class Proxy implements Subject{

    private RealSubject realSubject = new RealSubject(); //保证对真实对象的引用
	
    public void PreRequest(){
	 ....//调用Request()前的操作
    }
    
    @Override
    public void Request() {
        PreRequest();
        //对真实对象的调用
        realSubject.Request();
        postRequest();
    }
    
    public void PostRequest(){
        ....//调用Request()后的操作
    }
    
  
}

//客户端
public class Client {
    public static void main(String[] args) {
        Proxy proxy = new Proxy();
        proxy.Request();
    }
}

代理模式有比较广泛的使用,比如Spring AOPRPC、缓存等。在 Java 中,根据代理的创建时期,可以将代理模式分为静态代理和动态代理,下面就来分别阐述:

二、代理模式实现

动态代理和静态代理的区分就是语言类型是在运行时检查还是在编译期检查。大白话就是静态代理中的代理类是程序员自己写的,动态代理中的代理类程序员不用写,在代码运行过程中它能自动生成。

2.1 静态代理

静态代理是指在编译期,也就是在JVM运行之前就已经获取到了代理类的字节码信息。即Java源码生成.class文件时期:

由于在JVM运行前代理类和真实主题类已经是确定的,因此也被称为静态代理。

在实际使用中,通常需要定义一个公共接口及其方法,被代理对象(目标对象)与代理对象一起实现相同的接口或继承相同的父类。也就是我们自己需要构造一个代理类。在实际的日常开发中,几乎看不到使用静态代理的场景。

2.2 动态代理

动态代理,也就是在JVM运行时期动态构建对象和动态调用代理方法。它主要的作用就是可以帮助动态生成代理类,可以大大减少代理类的数量。

在JDK中,常用的实现方式是反射。而反射机制是指程序在运行期间可以访问、检测和修改其本身状态或行为的一种能力,使用反射我们可以调用任意一个类对象,以及其中包含的属性及方法。比如JDK Proxy。

此外动态代理也可以通过ASM(Java 字节码操作框架)来实现。比如CGLib。下面就具体进行说明:

2.2.1 JDK 动态代理

JDK 动态代理是JDK自身提供的一种方式,它的实现不需要引用第三方类,只需要实现InvocationHandler接口,重写invoke()方法就可以动态创建代理类。

  1. 实现InvocationHandler 接口,创建动态代理

这是减少代理类数量的核心部分,通过实现InvocationHandler 接口,并重写invoke() 方法,来创建动态代理类:

java 复制代码
//核心部分 JDK Proxy 代理类
class JDKProxy implements InvocationHandler {
    //真实的委托对象
    private Object target;

    public Object getInstance(Object target) {
        this.target = target;
        //获得代理对象
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }
    /**
	* @param proxy 代理对象,动态生成的代理类实例
	* @param method 被调用的方法对象,即要执行的方法,可以通过该对象获取方法的相关信息
	* @param args 方法的参数数组,这是被调用方法的参数列表。可以通过该参数获取方法的参数值
	* @return Object
	* @throws Throwable
	*/
    @Override   
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //执行委托对象的内部方法
        Object result = method.invoke(target, args);
        return result;
    }
}
  1. 创建被代理对象

也就是需要被代理的真实对象,这里也可以创建多个对象:

java 复制代码
public interface Vehicle {
    void running();
}
public class Car implements Vehicle {
    @Override
    public void running() {
        System.out.println("Car is running");
    }
}
public class Bus implements Vehicle {
    @Override
    public void running() {
        System.out.println("Bus is running");
    }
}
public class Taxi implements Vehicle {
    @Override
    public void running() {
        System.out.println("Taxi is runnig");
    }
}
  1. 客户端调用实现

这里我们就能够通过一个代理类JDKProxy 来动态代理多个委托类了:

java 复制代码
public class ProxyExampleDemo {
    public static void main(String[] args) {
        JDKProxy jdkProxy = new JDKProxy();
        Vehicle carInstance = (Vehicle) jdkProxy.getInstance(new Car());
        carInstance.running();
        Vehicle busInstance = (Vehicle) jdkProxy.getInstance(new Bus());
        busInstance.running();
        Vehicle taxiInstance = (Vehicle) jdkProxy.getInstance(new Taxi());
        taxiInstance.running();
    }
}
  1. 测试结果
arduino 复制代码
Car is running
Bus is running
Taxi is running

从结果会发现通过一个JDKProxy,就代理实现多个真实对象的内部方法。但是存在一个问题,JDK提供的动态代理只能代理接口,而不能代理没有接口的类,有的,他就是Cglib。

2.2.2 CGLib 动态代理

CGLib类库可以代理没有接口的类,它采取的是创建目标类的子类的方式,通过子类化,我们可以达到近似使用被调用者本身的效果。实现代码如下所示:

  1. 核心代理类

和JDK proxy一样,需要实现一个接口MethodInterceptor ,但我们发现有些不同,其内部是通过实现被代理类的子类来实现动态代理的功能。所以在使用时一定要注意被代理的类不能使用final修饰

java 复制代码
class CGLibProxy implements MethodInterceptor {
    private Object target;

    public Object getInstance(Object target) {
        this.target = target;
        Enhancer enhancer = new Enhancer();
        //设置父类为实例类
        enhancer.setSuperclass(this.target.getClass());
        //回调方法
        enhancer.setCallback(this);
        //创建代理对象
        return enhancer.create();
    }
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        Object result = methodProxy.invokeSuper(o, objects);
        return result;
    }
}
  1. 创建被代理对象

这个被代理对象可以不用实现接口

java 复制代码
class car {
    public void running() {
        System.out.println("car is running");
    }
}
  1. 客户端及测试
java 复制代码
public class CGLibExample {
    public static void main(String[] args) {
        CGLibProxy cgLibProxy = new CGLibProxy();
        car instance = (car) cgLibProxy.getInstance(new car());
        instance.running();
    }
}
//测试结果:
// car is running

2.2.3 JDK Proxy 和 CGLib 的区别

  1. 来源:JDK Proxy 是JDK 自带的功能,CGLib 是第三方提供的工具
  2. 实现:JDK Proxy 通过拦截器加反射的方式实现;CGLib 基于ASM实现,性能比较高
  3. 接口:JDK Proxy 只能代理继承接口的类,CGLib 不需要通过接口来实现,它是通过实现子类的方式来完成调用,但是要注意被代理的类不能被final修饰

三、代理模式的应用场景

代理模式其实在日常的开发中并不常见,但是在一些框架中使用的很多,比如springAop, myBatis mapper, 远程代理等等

3.1 MapperProxyFactory

在MyBatis 中,也存在着代理模式的使用,比如MapperProxyFactory。其中的newInstance()方法就是生成一个具体的代理来实现功能,代码如下:

java 复制代码
public class MapperProxyFactory<T> {
  private final Class<T> mapperInterface;
    
  private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();
    
  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }

  public Class<T> getMapperInterface() {
    return mapperInterface;
  }

  public Map<Method, MapperMethodInvoker> getMethodCache() {
    return methodCache;
  }

  // 创建代理类
  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }
}

3.2 Spring AOP

代理模式最常使用的一个应用场景就是在业务系统中开发一些非功能性需求,比如监控、统计、鉴权、限流、事务、日志等。将这些附加功能与业务功能解耦,放在代理类中统一处理,让程序员只需要关注业务方面的开发。而Spring AOP 的切面实现原理就是基于动态代理

Spring AOP 的底层通过上面提到的 JDK Proxy 和 CGLib动态代理机制,为目标对象执行横向织入。当Bean实现了接口时, Spring就会使用JDK Proxy,在没有实现接口时就会使用 CGLib。也可以在配置中强制使用 CGLib:

<aop:aspectj-autoproxy proxy-target-class="true"/>

3.3 远程代理

java 中的RMI(Remote Method Invocation),比如

RPC 框架的实现可以看作成是一种代理模式,通过远程代理、将网络同步、数据编解码等细节隐藏起来,让客户端在使用 RPC 服务时,不必考虑这些细节。

四、代理模式实战

4.1 利用静态代理模式实现模拟访问网页功能

这里可以使用静态代理模式来实现模拟访问网页的功能

  1. 定义访问网站的接口和统一访问方法
java 复制代码
public interface PageVisitor {
    void visitPage();
}
  1. 实现真实访问和代理访问具体类

两个具体的访问类都要实现统一的访问接口

java 复制代码
public class RealPageVisitor implements PageVisitor {

    private final OkHttpClient httpClient;

    public RealPageVisitor() {
        this.httpClient = new OkHttpClient();
    }

    @Override
    public void visitPage() {
        String url = "要访问的网站地址Url";
        Request request = new Request.Builder()
                .url(url)
                .build();
        try {
            Response response = httpClient.newCall(request).execute();
            if (response.isSuccessful()) {
                System.out.println("正在访问网址:" + url);
            } else {
                System.out.println("访问失败网址是:" + url);
            }
        } catch (IOException e) {
            System.out.println("出现访问异常信息");
            e.printStackTrace();
        }
    }
}

可以通过对代理类,对真实委托类进行扩展和管理:

java 复制代码
public class ProxyPageVisitor implements PageVisitor {

    @Autowired
    private RealPageVisitor realPageVisitor;

    private int visitCount;

    public ProxyPageVisitor(RealPageVisitor realPageVisitor) {
        this.realPageVisitor = realPageVisitor;
        this.visitCount = 0;
    }

    @Override
    public void visitPage() {
        //管理真实代理的生命周期
        if (canVisit()) {
            realPageVisitor.visitPage();
            visitCount++;
        } else {
            throw new IllegalStateException("超出最大访问量");
        }

    }

    private boolean canVisit() {
        int maxVisitCount = 120;
        return visitCount < maxVisitCount;
    }

}
  1. 设置访问的定时任务

该部分也就相当于UML图中的client 客户端,调用代理类来实现具体业务逻辑

java 复制代码
public class PageVisitorScheduler {

    @Autowired
    private ProxyPageVisitor pageVisitor;

    @Scheduled(fixedDelay = 25000)
    public void visitPage() {
        pageVisitor.visitPage();
    }
}

五、参考资料

c.biancheng.net/view/1359.h...

kaiwu.lagou.com/course/cour...

time.geekbang.org/column/arti...

time.geekbang.org/column/arti...

《Java 重学设计模式》

《大话设计模式》

相关推荐
Q_19284999061 小时前
基于Spring Boot的九州美食城商户一体化系统
java·spring boot·后端
ZSYP-S1 小时前
Day 15:Spring 框架基础
java·开发语言·数据结构·后端·spring
Yuan_o_2 小时前
Linux 基本使用和程序部署
java·linux·运维·服务器·数据库·后端
程序员一诺2 小时前
【Python使用】嘿马python高级进阶全体系教程第10篇:静态Web服务器-返回固定页面数据,1. 开发自己的静态Web服务器【附代码文档】
后端·python
DT辰白3 小时前
如何解决基于 Redis 的网关鉴权导致的 RESTful API 拦截问题?
后端·微服务·架构
thatway19893 小时前
AI-SoC入门:15NPU介绍
后端
陶庵看雪3 小时前
Spring Boot注解总结大全【案例详解,一眼秒懂】
java·spring boot·后端
Q_19284999064 小时前
基于Spring Boot的图书管理系统
java·spring boot·后端
ss2734 小时前
基于Springboot + vue实现的汽车资讯网站
vue.js·spring boot·后端