dubbo提供的API是稳定的,如果通过spring配置来暴露和引用服务,甚至接触不到API,不过如果选择通过代码的方式来暴露服务和引用服务,那就需要和ReferenceConfig和ServiceConfig这两个API交互,这两个API背后所依赖的一些组件都是可扩展和可替换的,比如选择以什么样的协议暴露服务(Protocol)、以什么样的方式生成代理(consumer端)和创建invoker(provider端)、用什么样的通信层(Transport),都是可以通过参数配置的,这给了框架开发和维护人员很大的灵活性和施展空间,同时让框架的扩展对用户透明。举个简单的例子,假设有一个框架提供了一个API如下:
public class Action {
void execute();
}
用来获取数据,执行业务计算,最后输出结果。这个API有很多种实现,比如有从DB获取数据的实现,有从文件获取数据的实现,还有输出结果到屏幕的实现和输出结果到DB的实现。如果为每一种实现都创建一个子类,让业务方选择用某一个子类实现自己的业务功能,就是耦合API和SPI的一个例子。如果哪一天框架又新增了几种实现,那么业务方必须替换原有的实现类才能使用新功能,这无疑是一种很差的设计。合理的做法是,将获取数据的方式抽象成一个SPI接口DataInput,将输出结果的方式抽象成另一个接口DataRender,然后为这两个SPI提供多种实现,而Action类将这两个工作代理给这两个SPI接口,业务方始终只和Action交互,至于Action要用SPI的哪一种实现去完成工作,这个由配置项在启动时决定或者运行时动态决定。
这其也符合尽可能使用组合而不是继承的思想。
没有任何一种框架可以满足用户全部的需求(哪怕当前满足了,未来还会有新需求),因此框架的设计者都会预留出扩展的余地,dubbo也不例外,除了功能强大的SPI机制,还有另外两种扩展的方式,它们是调用拦截和重要事件的通知机制。
ORM框架有sql执行过程的拦截,web框架有请求处理过程的拦截,rpc框架也有调用拦截。dubbo的调用拦截机制是通过Filter扩展点来实现的,服务提供端来说,在服务实现类真正处理请求之前,请求需要依次经过如下filter的处理:
EchoFilter --> ClassLoaderFilter --> GenericFilter --> ContextFilter --> ExceptionFilter -->
TimeoutFilter --> MonitorFilter --> TraceFilter
这些filter在服务暴露的时候,通过ProtocolFilterWrapper的buildInvokerChain形成一个filter链:
private static Invoker buildInvokerChain(final Invoker invoker, String key, String group) {
Invoker last = invoker;
//FIXME key => service.filter group => provider add by jileng
List filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);
if (filters.size() > 0) {
for (int i = filters.size() - 1; i >= 0; i --) {
final Filter filter = filters.get(i);
final Invoker next = last;
last = new Invoker() {
public Class getInterface() {
return invoker.getInterface();
}
public URL getUrl() {
return invoker.getUrl();
}
public boolean isAvailable() {
return invoker.isAvailable();
}
public Result invoke(Invocation invocation) throws RpcException {
return filter.invoke(next, invocation);
}
public void destroy() {
invoker.destroy();
}
@Override
public String toString() {
return invoker.toString();
}
};
}
}
return last;
}
一般需要放在filter里面实现的功能,都是对主流程有影响的功能,甚至可以提前中断整个执行流程,以达到类似AOP的效果。要想达到让框架自动加载用户自己实现的filter的目的,一般通过SPI机制。调用拦截机制的另一个形式是pipeline和valve机制,不同的是,一般pipeline里面有哪些valve是通过配置显示指定的。
框架的执行可能会有一些比较重要的状态变更,比如收到请求了、请求处理完了,容器初始化完成了等等。dubbo也定义了一些事件,并提供了一些Listener接口,留给用户进行扩展,比如监听服务暴露事件的ExporterListener、监听引用服务的InvokerListener,监听服务变更的NotifyListener等。dubbo框架也使用了Listener机制来实现框架自身的功能,比如使用NotifyListener感知服务提供者发生了变更,然通知服务订阅方更新服务提供者列表。
和filter拦截主链路不同,listener机制通常是旁路,它不影响执行主链路,更多是被动的接受通知,然后去做一些响应,作用是让框架的用户能够观察到框架内部状态的变化。
我们平时写业务代码,大部分时候要调什么方法,new什么对象,编译期就知道了,如果编译期不知道,那就使用反射机制,通过method.invoke和Class.newInstance之类的方式来达到同样的效果。但是反射是很耗性能的,应该尽量避免使用,那如果避免不了怎么办呢?比如RPC框架服务端怎么根据请求中的接口名和方法名去调用实现类呢?答案是利用字节码工具,动态生成一个类根据相应的参数去调真正的实现类。举个简单的例子,如果服务端暴露了一个服务 com.mogujie.service.Foo,实现类是 com.mogujie.service.FooImpl,Foo定义如下:
public interface Foo {
void method1();
void method2(int i);
}
那么服务暴露的时候,可以动态生成一个Wrapper类,Wrapper类的源码是根据Foo类利用反射动态生成的:
public class Wrapper {
private Foo ref;
Object invokeMethod(String mn, Object[] args) {
if ("method1".equals(mn)) {
ref.method1();
} else if ("method2".equals(mn)) {
ref.method2(args);
} else {
throw new NoSuchMethodException();
}
}
}
然后将源码编译成Class并加载,运行时收到调用请求后根据请求信息获取对应的Wrapper类,执行invokeMethod方法就能调到服务实现类了,这样避免了反射调用。
dubbo为每一个暴露的服务生成了这样一个Wrapper类,内部根据方法名用很多if去找到真正的方法进行调用,然后将接口签名和Wrapper构造成一个Map,相比较而言,感觉我们tesla的实现更高效一些——为每个方法生成一个MethodCaller,然后将方法签名和MethodCaller构造成Map,运行时直接拿到MethodCaller去执行,避免了dubbo的很多if判断。dubbo和tesla默认都是使用javassist来生成代理类的。
如果我们的业务中有一些不得不使用反射来实现的功能,不妨考虑下这种代理模式。不过这种模式也不是万能的,使用它有一个前提,就是调用的方法名在编译期是要能穷举的,不然无法构造出能够通过编译的源码,也就无法生成代理类了,这种情况下,只能使用反射了。