掘金 后端 ( ) • 2024-04-15 10:29

theme: cyanosis

组成

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

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

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

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

代理模式的组成如下:

图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 类

// 被代理的接口
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 类

@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 中的方法

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 类,这部分与静态代理相同

// 被代理的接口
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 接口的实现类

@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() **创建代理类对象

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动态代理实现的。

@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 接口的实现类:

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