前言
本篇是 spi 的第四篇, 本篇讲解的是 spi 中增加的 AOP, 还是和上一篇一样, 我们先从大家熟悉的 spring 引出 AOP.
AOP 是老生常谈的话题了, 思想都不会是一蹴而就的. 比如架构设计从 All in One 到 SOA 也是一个逐步演进的过程, 所以本篇也讲讲这个 AOP 的思想演进过程.
插播面试题
你提到了 dubbo 中 spi 也增加了 AOP, 那你讲讲这用到了什么设计模式, dubbo 又是如何做的.
直入主题
假如我们就以 AOP 最常用的场景事务来说, 我们最初的做法是怎么样的
简单做法
public class EmployeeServiceImpl implements IEmployeeService {
private TransactionManager txManager;
@Override
public void save() {
try {
txManager.begin();
System.out.println("保存操作");
txManager.commit();
}catch (Exception e){
txManager.rollback();
e.printStackTrace();
}
}
@Override
public void update() {
try {
txManager.begin();
System.out.println("更新操作");
txManager.commit();
}catch (Exception e){
txManager.rollback();
e.printStackTrace();
}
}
}
这些代码存在的问题就很明显了, 比如
处理事务的代码大量重复
根据责任分离思想, 在业务方法中, 只需要处理业务功能, 不该处理事务.
优化代码我们第一个想到的是设计模式, 那么我们进入如下的优化
装饰设计模式
public class APP {
@Test
public void testSave() throws Exception {
IEmployeeService service = new EmployeeServiceImplWapper(new TransactionManager(),
new EmployeeServiceImpl());
service.save();
}
@Test
public void testUpdate() throws Exception {
IEmployeeService service = new EmployeeServiceImplWapper(new TransactionManager(),
new EmployeeServiceImpl());
service.update();
}
}
通过装饰设计模式, 我们解决了上面遇到的两个问题, 但是同时也引出了新的问题, 在客户端我们暴露了真实的对象 EmployeeServiceImpl, 这样就很不安全, 那么我们可不可以把真实对象隐藏起来, 让使用者看不到呢? 那么我们进一步优化
静态代理
通过这种方式, 真实对象对使用者进行了一定的隐藏, 但是又引出了新的问题
如果需要代理的方法很多, 则每一种都要处理. 比如图中只处理了 save 方法, 万一有很多方法, 则需要处理很多次
接口新增了方法后, 除了所有实现类需要实现这个方法外, 所有代理类也需要实现此方法 (EmployeeServiceImpl 和
EmployeeServiceImplProxy
都要改动), 增加了代码的维护难度
代理对象的某个接口只服务于某一种类型的对象, 比如
EmployeeServiceImplProxy
是只给 IEmployeeService 接口服务的, 假如我新增了一个 IRoleService, 又要搞一个
RoleServiceImplProxy
, 增加了维护难度
鉴于以上问题, 我们能否再优化一下呢? 答案是可以的
动态代理
动态代理类是在程序运行期间由 JVM 通过反射等机制动态的生成的, 所以不存在代理类的字节码文件. 代理对象和真实对象的关系是在程序运行事情才确定的.
动态代理的方式和区别我们前面有讲过, 这里就简单演示一下 jdk 动态代理
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class JDKProxyTest {
@Autowired
private TransactionManagerInvocationHandle handle;
@Test
public void testSave() throws Exception {
IEmployeeService service = handle.getProxyObject();
service.save();
}
@Test
public void testUpdate() throws Exception {
IEmployeeService service = handle.getProxyObject();
service.update();
}
}
public class TransactionManagerInvocationHandle implements InvocationHandler {
@Setter
private TransactionManager txManager;
@Setter
private Object target;//真实对象
//生成代理对象
//泛型只是为了调用时不用强转,如果用Object的话调用时需要强转
public <T> T getProxyObject() {
return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(),//类加载器
target.getClass().getInterfaces(),//为哪些接口做代理(拦截什么方法)
this);//为哪个类监听增强操作的方法(把这些方法拦截到哪里处理)
}
//如何做增强操作(被拦截的方法在这里增强处理)
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object obj = null;
try {
txManager.begin();
//原封不动调用之前的方法
obj = method.invoke(target, args);
txManager.commit();
return obj;
} catch (Exception e) {
e.printStackTrace();
txManager.rollback();
}
return obj;
}
}
这样, 对于使用者来说, 就无需再关心事务的逻辑. 当然这个还需要 getProxyObject 获取动态代理对象是不是还是太麻烦, 那如何不调用 getProxyObject 就无声无息的注入动态代理对象呢? 可以观看之前的 dubbo 源码解析 - 简单原理,与 spring 融合
dubbo-spi-aop
看了这么多演进的过程, 是不是还是没有看到 dubbo 源码的影子? 因为 dubbo 在做 spi 的设计的时候, 也是有一个演进和优化的过程的. 我们来看看 dubbo 是怎么做的
//dubbo spi中的aop
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
下面引用文档介绍
ExtensionLoader 在加载扩展点时,如果加载到的扩展点有拷贝构造函数,则判定为扩展点 Wrapper 类.
Wrapper 类内容:
package com.alibaba.xxx;
import com.alibaba.dubbo.rpc.Protocol;
public class XxxProtocolWrapper implemenets Protocol {
Protocol impl;
public XxxProtocol(Protocol protocol) { impl = protocol; }
// 接口方法做一个操作后,再调用extension的方法
public void refer() {
//... 一些操作
impl.refer();
// ... 一些操作
}
// ...
}
通过 Wrapper 类可以把所有扩展点公共逻辑移至 Wrapper 中.新加的 Wrapper 在所有的扩展点上添加了逻辑,有些类似 AOP,即 Wrapper 代理了扩展点.
看到这里可能发现, dubbo 里面的 spi 增加的 aop, 其实就是装饰者设计模式. 但是从上面的演进中我们发现, 装饰者设计模式还是有很多弊端的, 后面是逐步演进, 最后到达动态代理. 那 dubbo 又是如何处理这个弊端逐步演进的
dubbo 里面有个概念叫扩展点自适应, 也就是给接口注入拓展点是一个 Adaptive 实例, 直到方法执行时, 才决定调用的是哪一个拓展点的实现. 这个在下一篇的 Adaptive 会详细介绍, 本篇其实也是下一篇的启蒙篇.
敲黑板划重点 - 小技巧
既然本篇提到了 spring 的 aop, 那么这里插播一个小技巧, Spring 的 AOP 增强方式一共有 5 种, 分别为
增强类型 | 应用场景 |
---|---|
前置增强 | 权限控制、记录调用日志 |
后置增强 | 统计分析结果数据 |
异常增强 | 通过日志记录方法异常信息 |
最终增强 | 释放资源 |
环绕增强 | 缓存、性能、权限、事务管理 |
面试的时候也会问到 5 种增强方式, 但是很多同学都是说, 我每天都在加班, 哪有时间记这些. 但是其实如果你理解他的设计思想, 那么就可以 "理解性记忆", 以后想忘都忘不掉.
//环绕
try {
//前置
System.out.println("=====");
//后置
} catch(Exception e) {
//异常
} finally {
//最终
}
其实他这 5 种方式就是根据 try-catch-finally 的模型来设计的, 只要你记住了这个设计的思想, 自然不会忘记这 5 种方式, 这也是我之前反复强调的, 理解透原理和设计思想, 很多东西都是一通百通的.
写在最后
spi 写了四篇, 本篇为结束篇, 下周开始下一个关键词 Adaptive, 期待继续与你相遇, 鉴于肥朝才疏学浅, 不足之处还望斧正, 也欢迎关注我的掘金, 名称为肥朝.
来源: http://www.jianshu.com/p/dba4447e5dc5