结构型模式
代理模式(Proxy Pattern)为其他对象提供一种代理,以控制对这个对象的访问。
组成
代理模式的实现需要3个参与者:接口—Subject、接口的具体实现类—RealSubject、对象的代理—Proxy。
- Subject:定义的接口
- RealSubject:接口的具体实现类,需要被代理的真实类
- Proxy:代理类,提供与 Subject 相同的方法,并持有对 RealSubject 的一个引用,这样可以代替 RealSubject,对外提供相同的方法
代理模式的组成UML如图1所示(来自参考文献[1]):
图1. 代理模式的组成
协作
- 客户创建 Proxy 对象,同时在 Proxy 对象中创了 RealSubject 对象
- 客户端调用 Proxy 的方法,调用 RealSubject 的同名方法,转发请求到 RealSubject
应用场景
在需要用比较通用和复杂的对象指针代替简单的指针的时候,需要使用 Proxy 模式(使用简单对象实现复杂功能),下面是一些可以使用 Proxy 模式的常见情况:
1、远程代理(Remote Proxy):为一个在不同地址空间的对象提供代理
2、需代理(Virtual Proxy):根据需要创建开销很大的对象,延迟对开销很大的对象的访问,实现懒加载
3、保护代理(Protection Proxy):控制对原始对象的访问,用于对象有不同访问权限的时候
4、智能指引(Smart Reference):它在访问对象时执行一些附加操作,比如对实际对象的引用计数、访问一个对象前检查是否已经锁定等
示例代码
静态代理
静态代理完全按照图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提供了动态代理,与静态代理相比最大的不同在于:Proxy 代理类不再是固定的,而是在运行时动态创建的。
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 // see issue #448
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、使用动态代理在运行时创建接口的代理实现
附录. 参考文献
- Erich Gamma, Richard Helm, Ralph Johnson, Jojn Vlissides. 设计模式—可复用面向对象软件的基础[M]. 机械工业出版社,北京. 2018.11
- 设计模式最佳套路3 —— 愉快地使用代理模式