设计模式-Proxy

组成

代理模式为其他对象提供一种代理,以控制对这个对象的访问。

对一个对象进行访问控制,而不是给这个对象添加额外的功能,比如访问目标对象的时候,判断是否有权限、记录目标对象方法的执行时间等,但不会影响目标对象本身的功能。

如果要给目标对象添加和业务逻辑无关的功能(比如权限判断、记录日志、耗时等),又不希望修改目标对象的类,那么使用代理模式可以很容易添加和移除这些功能。

代理模式是一个中间人,而不是目标对象的外壳。

代理模式的组成如下:

图1. 代理模式的组成

Subject:定义的接口

RealSubject:接口的具体实现类,需要被代理的真实类

Proxy:代理类,提供与 Subject 相同的方法,并持有对 RealSubject 的一个引用,这样可以代替 RealSubject,对外提供相同的方法。

客户创建 Proxy 对象,同时在 Proxy 对象中创了 RealSubject 对象;客户端调用 Proxy 的方法,Proxy 把方法代理到 RealSubject 的同名方法:

应用场景

下面是一些可以使用 Proxy 模式的常见情况:

1、远程代理(Remote Proxy):为一个在不同地址空间的对象提供代理,从而让 client 对要调用的目标对象实现透明调用。

2、虚代理(Virtual Proxy):根据需要创建开销很大的对象,延迟对开销很大的对象的访问,实现懒加载

3、保护代理(Protection Proxy):控制对原始对象的访问,用于对象有不同访问权限的时候

4、智能指引(Smart Reference):它在访问对象时执行一些附加操作,比如对实际对象的引用计数、访问一个对象前检查是否已经锁定等。

还有一种代理模式的实现就是 copy-on-write 方式,访问目标对象时,如果要修改目标对象 Proxy 则拷贝出一个新的对象,否则直接读取原对象。

示例代码

最常见的代理模式应用,是JDK中的静态代理和动态代理。

静态代理

静态代理完全按照图1所示的结构,分别创建 Subject、RealSubject、Proxy 类,使用Proxy 实现 Subject 并持有 RealSubject,示例如下。

1、创建 Subject 接口和 RealSubject 类

java 复制代码
// 被代理的接口
public interface UserService {

    /**
     * 根据ID查询用户对象
     * @param id 用户ID
     * @return User
     */
    User getUserById(Long id);
}

// 被代理的实现类
@Slf4j
public class UserServiceImpl implements UserService {

    @Override
    public User getUserById(Long id) {
        User user = new User().setId(id);
        log.info("user ===== {}", user);
        return user;
    }
}

2、创建 Proxy 类

java 复制代码
@Slf4j
public class UserProxy implements UserService {

    private UserService userService;

    public UserProxy(UserService userService) {
        this.userService = userService;
    }

    @Override
    public User getUserById(Long id) {
        log.info("before getUserById === class: {}", this.getClass().getName());
        User user = userService.getUserById(id);
        log.info("after getUserById === class: {}", this.getClass().getName());
        return user;
    }

}

3、创建代理对象,并调用 ReaslSubject 中的方法

java 复制代码
public static void main(String[] args) {
    UserService proxy = new UserProxy(new UserServiceImpl());
    proxy.getUserById(1000L);
}

// ========== 打印结果 =============== //
===== UserProxy : getUserById : begin
===== UserServiceImpl : getUserById : id:1000 ===== 
===== UserProxy : getUserById : end

proxy 在执行同名方法时,会先执行自身的逻辑,然后再执行 RealSubject 的逻辑,从而实现了对真实对象的方法控制。

静态代理的缺点很明显:要求代理类实现与被代理类相同的接口,一个代理类只能服务于一个类,如果要服务于多个类需要创建多个代理类。因此,JDK中更常用的是动态代理。

JDK 动态代理

JDK提供了动态代理,与静态代理相比最大的不同在于:它可以动态地创建代理,并动态地处理对代理方法的调用。

JDK怎么动态地创建Proxy?

Java对象是由JVM根据编译完成的 .class 文件创建的,因此要想动态创建 Proxy 对象,并且持有 RealSubject 的引用,需要完成以下几个步骤:

1、创建 Proxy 的 .class 文件,并加载到 JVM 中,创建 Class 对象

2、使用 Proxy 的 Class 对象,调用构造器创建对象实例

3、Proxy 对象直接或间接持有 RealSubject 对象的引用,在调用同名方法时能够真正调用 RealSubject 的方法

JDK动态代理的使用示例如下。

1、创建 Subject 接口和 RealSubject 类,这部分与静态代理相同

java 复制代码
// 被代理的接口
public interface UserService {

    /**
     * 根据ID查询用户对象
     * @param id 用户ID
     * @return User
     */
    User getUserById(Long id);
}

// 被代理的实现类
@Slf4j
public class UserServiceImpl implements UserService {

    @Override
    public User getUserById(Long id) {
        User user = new User().setId(id);
        log.info("user ===== {}", user);
        return user;
    }
}

2、创建 InvocationHandler 接口的实现类

java 复制代码
@Slf4j
public class UserInvokeHandler implements InvocationHandler {
	// 被代理的 RealSubject 对象
    private Object target;

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        log.info("=== UserInvokeHandler invoke begin ===");
        Object result = method.invoke(target, args);
        log.info("=== UserInvokeHandler invoke end ===");
        return result;
    }
}

3、使用 Proxy.newProxyInstance() **创建代理类对象

java 复制代码
public static void main(String[] args) {
    UserInvokeHandler handler = new UserInvokeHandler(new UserServiceImpl());
    UserService proxy = (UserService) Proxy.newProxyInstance(
        handler.getClass().getClassLoader(), new Class[]{UserService.class}, handler);
    proxy.getUserById(200L);
}

// ========== 打印结果 =============== //
=== UserInvokeHandler invoke begin ===
===== UserServiceImpl : getUserById : id:200 ===== 
=== UserInvokeHandler invoke end ===

在动态代理的示例代理中,主要有3个步骤:

1、创建需要被代理的接口和实现类,这一步与静态代理相同

2、创建 InvocationHandler 接口的实现类, invoke 方法里实现控制逻辑,把 RealSubject 对象作为参数传入,从而让代理类间接持有 RealSubject 的引用。

3、使用 Proxy.newProxyInstance() 方法创建代理类对象

在调用接口方法时,实际上是调用 handler 的 invoke() 方法,相关的源码介绍:静态代理和动态代理。

JDK动态代理被广泛使用在各个框架中,比如 MyBatis 框架,下面简单介绍一下 MyBatis 中对JDK动态代理的使用。

MyBatis

在MyBatis测试用例中,使用 sqlSession.getMappser(Mapper.class) 创建 Mapper 接口的代理对象,再调用同名方法,使用的就是JDK动态代理实现的。

java 复制代码
@Test
void shouldGetAUserStatic() {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
        Mapper mapper = sqlSession.getMapper(Mapper.class);
        User user = mapper.getUserStatic(1);
        Assertions.assertNotNull(user);
        Assertions.assertEquals("User1", user.getName());
    }
}

sqlSession.getMapper()的时序图如图2所示:

图2. sqlSession.getMapper() 的时序图

MapperProxyFactory.newInstance 中使用 Proxy.newProxyInstance() 创建 Mpper 的代理对象,传入的参数 MapperProxy 就是 InvocationHandler 接口的实现类:

java 复制代码
public class MapperProxy<T> implements InvocationHandler, Serializable {

  // ..... 省略 ........ //

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else {
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }
}

优点和缺点

优点

1、高扩展性:不需要对真实类 RealSubject 进行修改,就可以扩展它的功能

2、把控制和职责分离,与 RealSubject 无关的逻辑可以由代理类实现

缺点

1、额外增加了代理类,特别是动态代理在运行时创建 .class 字节码和 对象,造成性能下降****

小结

1、使用代理模式控制对真实类的访问、提供与真实类无关的逻辑实现等

2、使用动态代理在运行时创建接口的代理实现

从两者的 UML 图上可以看出,代理模式和装饰器模式 很相似,但是两者有着很大的差异,两者的差异如下:

1、 重点不同:装饰器模式的重点在于增强目标对象的功能,代理模式的重点在于保护和隐藏目标对象(实现透明访问)或者增加与业务无关的功能。

2、实现方式不同:装饰器是把目标对象作为构造器参数,代理模式是在代理类中创建目标对象。

3、 结果不同:装饰器模式对目标对象产生一种连续的、叠加的增强效果,代理模式是在代理类中一次性为目标对象添加功能(比如检查、增加日志)等。

相关推荐
金銀銅鐵5 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)
java·后端·ai编程
念何架构之路5 小时前
go-grpc设计思想
开发语言·后端·golang
system_error_x6 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
步行cgn7 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
码事漫谈8 小时前
AI 读你的提示词,其实很没耐心
后端
用户8356290780519 小时前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python
我命由我123459 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
IT_陈寒9 小时前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记9 小时前
对接群晖NAS,上传和删除踩坑记录
前端·后端
Mk598789 小时前
P4:把 Agent 的记忆搬进 Redis —— AgentScope-Java 工程化实战
后端