在面向对象的世界中,对象与对象之间的相互协作构成了系统的运行状态。通常,我们可以在一个对象中直接引用另一个对象来获取想要的功能,但有时候事情并没有那么简单。我们来看一段简单的代码示例:
@Service
public class HealthService {
public void recordUserHealthData(HealthData data) {
healthRepository.recordUserHealthData(data);
logger.info("Record user health data successfully.");
}
…
}
上述代码很简单,是在 Service 层组件中调用数据访问层组件,并记录一个操作日志。现在假设这个 HealthService 中有很多方法,而对所有方法操作都需要添加日志。显然,在每个方法里都手工调用同一个日志方法不是一种很好的解决方案,会造成代码冗余,增加维护成本。
这个时候,代理机制就可以派上用场了。我们可以构建一个代理对象,然后由这个代理对象统一实现日志记录操作。
可以看到,通过代理机制,一个对象就可以在承接另一个对象功能的基础之上,同时添加新的功能。相比直接在原有对象中嵌入代码,代理机制为我们提供了更为优雅的解决方案。
那么,代理机制具体是如何实现的呢?让我们一起来看一下。
代理机制一般有两种实现方式,一种是 静态代理机制,一种是 动态代理机制。一般来说静态机制设计和实现上比较容易理解,而动态机制则较为复杂,所以我们先来学习一下静态机制。
静态代理机制在技术上比较简单,我们先看这样一个示例。
我们考虑有一个 Account 接口,包含一个用于开设账户的 open() 方法。
public interface Account{
void open();
}
然后针对该接口有一个实现类 RealAccount,提供了对 open() 方法的模拟实现。
public class RealAccount implements Account {
private String name;
public RealAccount(String name) {
this.name = name;
}
@Override
public void open() {
System.out.println("开账户: " + name);
}
}
接下来就是代理类的实现,我们称之为 ProxyAccount,代码如下所示:
public class ProxyAccount implements Account {
private Account account;
private String name;
public ProxyAccount(String name) {
this.name = name;
}
@Override
public void open() {
checkName(name);
if (account == null) {
account = new RealAccount(name);
}
account.open();
}
private void checkName(String name) {
System.out.println("验证用户名称: " + name);
}
}
可以看到 ProxyAccount 同样实现了 Account 接口,并保存了一个 RealAccount 实例。这样,ProxyAccount 对象的 open() 方法既包含了 RealAccount 中的原有逻辑,又额外代理了验证用户名称的逻辑。它们之间的类层结构如下图所示。
跟动态代理相比,静态代理模式中的代理关系是 编码阶段就能决定的,所以容易管理,也不存在因为外部代理框架而造成的性能损耗。你在 Mybatis 的缓存和连接池等的实现机制中,都能看到静态代理模式的应用场景。
介绍完静态代理,我们接着来看动态代理。在 Java 世界中,想要实现动态代理,可以使用 JDK 自带的代理机制。
现在假设同样存在静态代理中 Account 接口以及实现类 RealAccount,然后我们需要再调用其 open() 方法的前后记录操作日志。
在 JDK 自带的动态代理中存在一个 InvocationHandler 接口,想要实现动态代理,我们首先要做的就是提供该接口的一个实现类。
public class AccountHandler implements InvocationHandler{
private Object obj;
public AccountHandler(Object obj) {
super();
this.obj = obj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] arg)
throws Throwable {
Object result = null;
doBefore();
result = method.invoke(obj, arg);
doAfter();
return result;
}
public void doBefore() {
System.out.println("开户前");
}
public void doAfter() {
System.out.println("开户后");
}
}
InvocationHandler 接口中包含一个 invoke() 方法,我们必须实现这一方法。在该方法中,我们通常需要调用 method.invoke() 方法执行原有对象的代码逻辑,然后可以在该方法前后添加相应的代理实现。在上述代码中,我们只是简单打印了日志。
然后,我们编写测试类来应用上述 AccountHandler 类,如下所示。
public class AccountTest {
public static void main(String[] args) {
Account account = new RealAccount("xiaoyiran");
InvocationHandler handler = new AccountHandler(account);
Account proxy = (Account)Proxy.newProxyInstance(
account.getClass().getClassLoader(),
account.getClass().getInterfaces(),
handler);
proxy.open();
}
}
这里,Proxy.newProxyInstance() 方法的作用就是生成 RealAccount 类的代理类。当该方法被调用时,RealAccount 类的实例就会注入到这个代理类中。然后当代理类的 open() 方法被调用时,AccountHandler 中 invoke() 方法就会被执行,从而执行代理方法。这里的类层结构是这样的。
仔细分析上述代码结构,可以看到针对某一个业务结构,我们分别提供了一个实现类以及对应的代理类。通过 JDK 提供的动态代理机制,我们可以把这些类整合在一起。而在代码实现上,我们也可以发现其遵循这样一个流程:设计和实现业务接口→实现 Handler 类→创建代理类,然后在 Handler 类中构建具体的代理逻辑。上述流程也是代表了一种标准的代理机制实现流程。我们可以联想一下,有很多基于 AOP 机制的拦截器,它们的实现机制实际上就是类似的原理。
关于 JDK 自带的动态代理机制,在 Dubbo 和 Mybatis 框架中都得到了应用,其中 Dubbo 主要使用动态代理实现远程方法的调用,而 Mybatis 则基于这一机制来完成数据访问。我们将分别对这两种场景展开讨论,先来看 Dubbo 远程访问中的代理机制。
我们在 Dubbo 代码中找到了如下所示的 JdkProxyFactory 类,用来获取代理对象。
public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), interfaces, new InvokerInvocationHandler(invoker));
}
这里你可以看到 Proxy.newProxyInstance() 方法,这是典型的 JDK 动态代理的用法。根据传入的接口获得动态代理类,当调用这些接口的方法时都会转而调用 InvokerInvocationHandler(invoker)。我们来看一下 InvokerInvocationHandler 类。基于 JDK 动态代理的实现机制,可以想象 InvokerInvocationHandler 类必定实现了 InvocationHandler 接口。
public class InvokerInvocationHandler implements InvocationHandler {
private final Invoker<?> invoker;
public InvokerInvocationHandler(Invoker<?> handler) {
this.invoker = handler;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
if (method.getDeclaringClass() == Object.class) {
return method.invoke(invoker, args);
}
…
return invoker.invoke(new RpcInvocation(method, args)).recreate();
}
}
可以看到,这里把方法的执行流程转向了 invoker.invoke() 方法,而这个 invoker 对象会负责执行具体的远程调用操作。
使用过 Mybatis 的同学都知道,我们只需要定义 Mapper 层的接口而不需要对其进行具体的实现,该接口就能够正常完成 SQL 执行等一系列操作,这是怎么做到的呢?
实际上 Mybatis 能够做到这一点的背后就是使用了强大的代理机制,具体来说就是如下所示的 MapperProxy 类。
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 if (method.isDefault()) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
}
可以看到,这里对于执行 SQL 语句的方法而言,MapperProxy 会把这部分工作交给 MapperMethod 处理。MapperMethod 会进一步调用 Mybatis 中的 SqlSession 对象并执行具体的 SQL 语句。
目前为止,我们看到了 MapperProxy 类实现了 InvocationHandler 接口,但还没有看到 Proxy.newProxyInstance() 方法的调用,该方法实际上位于 MapperProxyFactory 类中,该类还存在 newInstance() 重载方法,通过传入 mapperProxy 的代理对象最终完成代理方法的执行。
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
作为总结,我们梳理了 Mybatis 中 Mapper 层动态代理相关类的类层结构。
可以看到,Mybatis 在实现 SQL 执行时使用的就是 JDK 中所提供了原生动态代理机制。通过合理划分各个组件的职责,Mybatis 设计了用来生成代理对象 MapperProxy 的代理工厂类 MapperProxyFactory,并最终把执行 SQL 的操作封装在了 MapperMethod 中。
今天我们系统介绍了代理机制。在日常开发过程中,代理可以说是一种通用性非常高的实现机制,它是面向切面编程的基础,也在主流的开源框架中得到了广泛地应用。例如,Dubbo 在实现远程方法调用时就用到了动态代理,而 Mybatis 则基于它来完成数据访问。这些应用方式和实现过程值得我们学习和模仿。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。