前面我们了解过了Java的SPI扩展机制,对于Java扩展机制的原理以及优缺点也有了大概的了解,这里继续深入一下Dubbo的扩展点加载机制。玩过Dubbo框架的同学都知道,Dubbo框架最强大的地方就是他的SPI机制,可以满足使用者天马行空的扩展性需求。
本文主要讨论2点:Dubbo的spi机制实现原理;基于SPI思想的Filter实现。
Dubbo的spi机制实现原理
这里以Protocol 协议接口来讲解,先上一张图来帮助理解:
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
ExtensionLoader类的实现思想参考了JDK中的ServiceLoader类,也是用来加载指定路径下的接口实现,具体实现细节比JDK的复杂了很多。
首先看ExtensionLoader的静态方法getExtensionLoader。
public static ExtensionLoader getExtensionLoader(Class type) {
if (type == null)
throw new IllegalArgumentException("Extension type == null");
if(!type.isInterface()) {
throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
}
if(!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type(" + type +
") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
}
//根据接口对象取ExtensionLoader类
ExtensionLoader loader = (ExtensionLoader) EXTENSION_LOADERS.get(type);
if (loader == null) {
//如果为空保存接口类对应的 新建的ExtensionLoader对象
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader(type));
loader = (ExtensionLoader) EXTENSION_LOADERS.get(type);
}
return loader;
}
有4个点需要注意:
1.EXTENSION_LOADERS这个Map中以接口为key,以ExtensionLoader对象为value。
2.判断Map中根据接口get对象,如果没有就new个ExtensionLoader对象保存进去。并返回该ExtensionLoader对象。
3.注意创建ExtensionLoader对象的构造函数代码,将传入的接口type属性赋值给了ExtensionLoader类的type属性
4.创建ExtensionFactory objectFactory对象
@SPI("dubbo")
public interface Protocol {
int getDefaultPort();
@Adaptive
Exporter export(Invoker invoker) throws RpcException;
@Adaptive
Invoker refer(Class type, URL url) throws RpcException;
void destroy();
}
ExtensionLoader使用loadExtensionClasses方法读取扩展点中的实现类
loadExtensionClasses先读取SPI注解的value值,如果value有值,就把这个值作为默认扩展实现的key。然后再以此读取META-INF/dubbo/internal/,META-INF/dubbo/,META-INF/services/下对应的文件。
loadFile逐行读取com.alibaba.dubbo.rpc.Protocol文件中的内容,每行内容以key/value形式存储。先判断实现类上是否打上了@Adaptive注解,如果打上了该注解,将此类作为Protocol协议的设配类缓存起来,读取下一行。如果实现类上没有打上@Adaptive注解,判断实现类是否存在参数为该接口的构造器,有的话作为包装类存储在该ExtensionLoader的Set> cachedWrapperClasses;集合中,这里用到了装饰器模式。如果该类既不是设配类,也不是wrapper对象,那就是扩展点的具体实现对象,查找实现类上是否打了@Activate注解,有缓存到变量cachedActivates的map中将实现类缓存到cachedClasses中,以便于使用时获取。如ProtocolFilterWrapper的实现如下:
public class ProtocolFilterWrapper implements Protocol {
private final Protocol protocol;
public ProtocolFilterWrapper(Protocol protocol) {
if (protocol == null) {
throw new IllegalArgumentException("protocol == null");
}
this.protocol = protocol;
}
..........
}
获取或则创建设配对象getAdaptiveExtension
如果cachedAdaptiveClass有值,说明有且仅有一个实现类打了@Adaptive, 实例化这个对象返回。如果cachedAdaptiveClass为空, 创建设配类字节码。
为什么要创建设配类,一个接口多种实现,SPI机制也是如此,这是策略模式,但是我们在代码执行过程中选择哪种具体的策略呢。Dubbo采用统一数据模式com.alibaba.dubbo.common.URL(它是dubbo定义的数据模型不是jdk的类),它会穿插于系统的整个执行过程,URL中定义的协议类型字段protocol,会根据具体业务设置不同的协议。url.getProtocol()值可以是dubbo也是可以webservice, 可以是zookeeper也可以是redis。
设配类的作用是根据url.getProtocol()的值extName,去ExtensionLoader. getExtension( extName)选取具体的扩展点实现。
有上述的分析可知,能够使用javasist生成设配类的条件:
1)接口方法中必须至少有一个方法打上了@Adaptive注解
2)打上了@Adaptive注解的方法参数必须有URL类型参数或者有参数中存在getURL()方法
仔细看看Protocol接口代理的具体实现,在使用接口代理中的方法时,都会根据URL来确定接口的具体实现,因为URL中携带了用户大部分的参数配置,根据里面的属性来获取。里面关键代码:
com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
看到这里思路应该比较清晰了!所有的接口代理中,并没有给定具体的实现,全部根据用户的参数配置来动态创建接口的具体实现。这样做让程序非常的灵活,让接口的实现插拔更加方便。如果想增加一个接口的实现,只需要按照SPI的配置方式增加配置文件,xml标签配置指定新接口实现的标记即可。
基于SPI思想的Filter实现
在微服务场景下,一次调用过程常常会涉及多个应用,在定位问题时,往往需要在多个应用中查看某一次调用链路上的日志,为了达到这个目的,一种常见的做法是在调用入口处生成一个traceId,并基于RpcContext来实现traceId的透传。下面来看一下怎么通过filter实现traceId的跟踪记录。
1.创建Dubbo框架的api项目,创建类FilterTest
package com.enjoy.filter;
import com.alibaba.dubbo.common.Constants;
import com.alibaba.dubbo.common.extension.Activate;
import com.alibaba.dubbo.rpc.*;
@Activate(group = Constants.CONSUMER)
public class FilterSpi implements Filter{
@Override
public Result invoke(Invoker> invoker, Invocation invocation) throws RpcException {
String traceId = String.valueOf(System.currentTimeMillis());
RpcContext.getContext().setAttachment("tracdId",traceId);
System.out.println("traceId:"+traceId);
Result result = invoker.invoke(invocation);
return result;
}
}
项目结构图为
2.创建server项目,提供服务,创建订单类
package com.enjoy.service.impl;
import com.enjoy.dao.OrderDao;
import com.enjoy.entity.OrderEntiry;
import com.enjoy.service.OrderService;
import com.enjoy.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private ProductService productService;
@Override
public OrderEntiry getDetail(String id) {
OrderEntiry orderEntiry = orderDao.getDetail(id);
orderEntiry.addProduct(productService.getDetail("P001"));
orderEntiry.addProduct(productService.getDetail("P002"));
System.out.println(super.getClass().getName()+"被调用一次:"+System.currentTimeMillis());
return orderEntiry;
}
@Override
public OrderEntiry submit(OrderEntiry order) {
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (1==order.getStatus()){
System.out.println("警告:订单重复提交!");
throw new RuntimeException("订单重复提交!");
}
System.out.println(super.getClass().getName()+"被调用一次:"+System.currentTimeMillis());
return orderDao.submit(order);
}
@Override
public String cancel(OrderEntiry order) {
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(super.getClass().getName()+"被调用一次:"+System.currentTimeMillis());
return orderDao.cancel(order);
}
}
配置dubbo.xm文件
3.创建消费端
package com.enjoy.controller;
import com.alibaba.dubbo.rpc.RpcContext;
import com.enjoy.entity.OrderEntiry;
import com.enjoy.service.OrderService;
import com.enjoy.service.PayService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@Controller
public class OrderController {
@Autowired
private PayService payService;
@Autowired
private OrderService orderService;
@RequestMapping(value = "/order", method = RequestMethod.GET)
public String getDetail(HttpServletRequest request, HttpServletResponse response){
OrderEntiry orderView = orderService.getDetail("1");
request.setAttribute("order", orderView);
return "order";
}
/**
* 异步并发调用
* @param request
* @param response
* @return
*/
@RequestMapping(value = "/cancel", method = RequestMethod.GET)
public String cancel(HttpServletRequest request, HttpServletResponse response) {
OrderEntiry orderView = orderService.getDetail("1");
String cancel_order = null,cancel_pay = null;
long start = System.currentTimeMillis();
//若设置了async=true,方法立即返回null
cancel_order = orderService.cancel(orderView);
//只有async=true,才能得到此对象,否则为null
Future cancelOrder = RpcContext.getContext().getFuture();
cancel_pay = payService.cancelPay(orderView.getMoney());
Future cancelpay = RpcContext.getContext().getFuture();
/**
* Future模式
*
*/
try {
cancel_order = cancelOrder.get();
cancel_pay = cancelpay.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
request.setAttribute("cancelOrder", cancel_order);
request.setAttribute("cancelpay", cancel_pay);
long time = System.currentTimeMillis() - start;
request.setAttribute("time", time);
return "/cancel";
}
/**
* 事件通知
* @param request
* @param response
* @return
*/
@RequestMapping(value = "/order/submit", method = RequestMethod.GET)
public String submit(HttpServletRequest request, HttpServletResponse response){
OrderEntiry orderView = orderService.getDetail("1");
orderView.setStatus(1);
orderService.submit(orderView);
request.setAttribute("order", orderView);
return "/order";
}
}
4.启动服务提供方和消费端项目,其中消费端项目控制台信息:
这个信息就是我们在api中定义的fiter过滤器的具体实现,即在微服务跨域调用过程中,traceId的追踪,方便后续排查日志。
终于写完了,接口代理的生成是不是有点动态代理的感觉。然后用户在XML中配置的dubbo标签属性都保存在了URL中,URL携带的参数贯穿了整个dubbo架构,所有的组件调用都根据URL中配置的参数做处理。其实SPI技术在很多地方都有用到,比如数据库的驱动,日志的处理,原理不是很复杂,仔细研究下就明白了。