前面我們了解過了Java的SPI擴(kuò)展機(jī)制吱型,對(duì)于Java擴(kuò)展機(jī)制的原理以及優(yōu)缺點(diǎn)也有了大概的了解帐萎,這里繼續(xù)深入一下Dubbo的擴(kuò)展點(diǎn)加載機(jī)制跟狱。玩過Dubbo框架的同學(xué)都知道扎运,Dubbo框架最強(qiáng)大的地方就是他的SPI機(jī)制脖母,可以滿足使用者天馬行空的擴(kuò)展性需求士鸥。
本文主要討論2點(diǎn):Dubbo的spi機(jī)制實(shí)現(xiàn)原理;基于SPI思想的Filter實(shí)現(xiàn)谆级。
Dubbo的spi機(jī)制實(shí)現(xiàn)原理
這里以Protocol 協(xié)議接口來講解烤礁,先上一張圖來幫助理解:
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
ExtensionLoader類的實(shí)現(xiàn)思想?yún)⒖剂薐DK中的ServiceLoader類,也是用來加載指定路徑下的接口實(shí)現(xiàn)肥照,具體實(shí)現(xiàn)細(xì)節(jié)比JDK的復(fù)雜了很多脚仔。
首先看ExtensionLoader的靜態(tài)方法getExtensionLoader。
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> 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!");
}
//根據(jù)接口對(duì)象取ExtensionLoader類
ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
if (loader == null) {
//如果為空保存接口類對(duì)應(yīng)的 新建的ExtensionLoader對(duì)象
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;
}
有4個(gè)點(diǎn)需要注意:
1.EXTENSION_LOADERS這個(gè)Map中以接口為key,以ExtensionLoader對(duì)象為value舆绎。
2.判斷Map中根據(jù)接口get對(duì)象鲤脏,如果沒有就new個(gè)ExtensionLoader對(duì)象保存進(jìn)去。并返回該ExtensionLoader對(duì)象。
3.注意創(chuàng)建ExtensionLoader對(duì)象的構(gòu)造函數(shù)代碼猎醇,將傳入的接口type屬性賦值給了ExtensionLoader類的type屬性
4.創(chuàng)建ExtensionFactory objectFactory對(duì)象
@SPI("dubbo")
public interface Protocol {
int getDefaultPort();
@Adaptive
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
@Adaptive
<T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
void destroy();
}
ExtensionLoader使用loadExtensionClasses方法讀取擴(kuò)展點(diǎn)中的實(shí)現(xiàn)類
loadExtensionClasses先讀取SPI注解的value值窥突,如果value有值,就把這個(gè)值作為默認(rèn)擴(kuò)展實(shí)現(xiàn)的key硫嘶。然后再以此讀取META-INF/dubbo/internal/阻问,META-INF/dubbo/,META-INF/services/下對(duì)應(yīng)的文件沦疾。
loadFile逐行讀取com.alibaba.dubbo.rpc.Protocol文件中的內(nèi)容称近,每行內(nèi)容以key/value形式存儲(chǔ)。先判斷實(shí)現(xiàn)類上是否打上了@Adaptive注解曹鸠,如果打上了該注解煌茬,將此類作為Protocol協(xié)議的設(shè)配類緩存起來,讀取下一行彻桃。如果實(shí)現(xiàn)類上沒有打上@Adaptive注解坛善,判斷實(shí)現(xiàn)類是否存在參數(shù)為該接口的構(gòu)造器,有的話作為包裝類存儲(chǔ)在該ExtensionLoader的Set<Class<?>> cachedWrapperClasses;集合中邻眷,這里用到了裝飾器模式眠屎。如果該類既不是設(shè)配類,也不是wrapper對(duì)象肆饶,那就是擴(kuò)展點(diǎn)的具體實(shí)現(xiàn)對(duì)象改衩,查找實(shí)現(xiàn)類上是否打了@Activate注解,有緩存到變量cachedActivates的map中將實(shí)現(xiàn)類緩存到cachedClasses中驯镊,以便于使用時(shí)獲取葫督。如ProtocolFilterWrapper的實(shí)現(xiàn)如下:
public class ProtocolFilterWrapper implements Protocol {
private final Protocol protocol;
public ProtocolFilterWrapper(Protocol protocol) {
if (protocol == null) {
throw new IllegalArgumentException("protocol == null");
}
this.protocol = protocol;
}
..........
}
獲取或則創(chuàng)建設(shè)配對(duì)象getAdaptiveExtension
如果cachedAdaptiveClass有值,說明有且僅有一個(gè)實(shí)現(xiàn)類打了@Adaptive, 實(shí)例化這個(gè)對(duì)象返回板惑。如果cachedAdaptiveClass為空橄镜, 創(chuàng)建設(shè)配類字節(jié)碼。
為什么要?jiǎng)?chuàng)建設(shè)配類冯乘,一個(gè)接口多種實(shí)現(xiàn)洽胶,SPI機(jī)制也是如此,這是策略模式裆馒,但是我們?cè)诖a執(zhí)行過程中選擇哪種具體的策略呢姊氓。Dubbo采用統(tǒng)一數(shù)據(jù)模式com.alibaba.dubbo.common.URL(它是dubbo定義的數(shù)據(jù)模型不是jdk的類),它會(huì)穿插于系統(tǒng)的整個(gè)執(zhí)行過程喷好,URL中定義的協(xié)議類型字段protocol翔横,會(huì)根據(jù)具體業(yè)務(wù)設(shè)置不同的協(xié)議。url.getProtocol()值可以是dubbo也是可以webservice梗搅, 可以是zookeeper也可以是redis棕孙。
設(shè)配類的作用是根據(jù)url.getProtocol()的值extName,去ExtensionLoader. getExtension( extName)選取具體的擴(kuò)展點(diǎn)實(shí)現(xiàn)。
有上述的分析可知蟀俊,能夠使用javasist生成設(shè)配類的條件:
1)接口方法中必須至少有一個(gè)方法打上了@Adaptive注解
2)打上了@Adaptive注解的方法參數(shù)必須有URL類型參數(shù)或者有參數(shù)中存在getURL()方法
仔細(xì)看看Protocol接口代理的具體實(shí)現(xiàn)钦铺,在使用接口代理中的方法時(shí),都會(huì)根據(jù)URL來確定接口的具體實(shí)現(xiàn)肢预,因?yàn)閁RL中攜帶了用戶大部分的參數(shù)配置矛洞,根據(jù)里面的屬性來獲取。里面關(guān)鍵代碼:
com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
看到這里思路應(yīng)該比較清晰了烫映!所有的接口代理中沼本,并沒有給定具體的實(shí)現(xiàn),全部根據(jù)用戶的參數(shù)配置來動(dòng)態(tài)創(chuàng)建接口的具體實(shí)現(xiàn)锭沟。這樣做讓程序非常的靈活抽兆,讓接口的實(shí)現(xiàn)插拔更加方便。如果想增加一個(gè)接口的實(shí)現(xiàn)族淮,只需要按照SPI的配置方式增加配置文件辫红,xml標(biāo)簽配置指定新接口實(shí)現(xiàn)的標(biāo)記即可。
基于SPI思想的Filter實(shí)現(xiàn)
在微服務(wù)場(chǎng)景下祝辣,一次調(diào)用過程常常會(huì)涉及多個(gè)應(yīng)用贴妻,在定位問題時(shí),往往需要在多個(gè)應(yīng)用中查看某一次調(diào)用鏈路上的日志蝙斜,為了達(dá)到這個(gè)目的名惩,一種常見的做法是在調(diào)用入口處生成一個(gè)traceId,并基于RpcContext來實(shí)現(xiàn)traceId的透?jìng)髟熊O旅鎭砜匆幌略趺赐ㄟ^filter實(shí)現(xiàn)traceId的跟蹤記錄娩鹉。
1.創(chuàng)建Dubbo框架的api項(xiàng)目,創(chuàng)建類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;
}
}
項(xiàng)目結(jié)構(gòu)圖為
2.創(chuàng)建server項(xiàng)目稚伍,提供服務(wù),創(chuàng)建訂單類
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()+"被調(diào)用一次:"+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("警告:訂單重復(fù)提交弯予!");
throw new RuntimeException("訂單重復(fù)提交!");
}
System.out.println(super.getClass().getName()+"被調(diào)用一次:"+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()+"被調(diào)用一次:"+System.currentTimeMillis());
return orderDao.cancel(order);
}
}
配置dubbo.xm文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.1.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<context:component-scan base-package="com.enjoy"/>
<!-- 提供方應(yīng)用信息槐瑞,用于計(jì)算依賴關(guān)系 -->
<dubbo:application name="storeServer"/>
<!-- 使用zookeeper注冊(cè)中心暴露服務(wù)地址 -->
<dubbo:registry address="zookeeper://10.xxx.xxx.xxx:2181"/>
<!--用dubbo協(xié)議在20880端口暴露服務(wù) -->
<dubbo:protocol name="dubbo" port="20880"/>
<dubbo:consumer check="false" />
<!-- 聲明需要暴露的服務(wù)接口 -->
<dubbo:service interface="com.enjoy.service.OrderService" ref="orderService" protocol="dubbo" />
<dubbo:service interface="com.enjoy.service.PayService" ref="payService" protocol="dubbo" />
<dubbo:service interface="com.enjoy.service.OtherService" ref="otherService" protocol="dubbo" />
<dubbo:service interface="com.enjoy.service.ProductService" ref="productService" protocol="dubbo"/>
<dubbo:service interface="com.enjoy.service.UserService" ref="userService" />
<!-- 聲明需要引用的服務(wù)接口 -->
<!--和本地bean一樣實(shí)現(xiàn)服務(wù) -->
<bean id="orderService" class="com.enjoy.service.impl.OrderServiceImpl"/>
<bean id="payService" class="com.enjoy.service.impl.PayServiceImpl"/>
<bean id="otherService" class="com.enjoy.service.impl.OtherServiceImpl"/>
<bean id="productService" class="com.enjoy.service.impl.ProductServiceImpl"/>
<bean id="userService" class="com.enjoy.service.impl.UserServiceImpl"/>
</beans>
3.創(chuàng)建消費(fèi)端
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";
}
/**
* 異步并發(fā)調(diào)用
* @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();
//若設(shè)置了async=true,方法立即返回null
cancel_order = orderService.cancel(orderView);
//只有async=true阁苞,才能得到此對(duì)象困檩,否則為null
Future<String> cancelOrder = RpcContext.getContext().getFuture();
cancel_pay = payService.cancelPay(orderView.getMoney());
Future<String> 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.啟動(dòng)服務(wù)提供方和消費(fèi)端項(xiàng)目,其中消費(fèi)端項(xiàng)目控制臺(tái)信息:
這個(gè)信息就是我們?cè)赼pi中定義的fiter過濾器的具體實(shí)現(xiàn)那槽,即在微服務(wù)跨域調(diào)用過程中悼沿,traceId的追蹤,方便后續(xù)排查日志骚灸。
終于寫完了糟趾,接口代理的生成是不是有點(diǎn)動(dòng)態(tài)代理的感覺。然后用戶在XML中配置的dubbo標(biāo)簽屬性都保存在了URL中,URL攜帶的參數(shù)貫穿了整個(gè)dubbo架構(gòu)义郑,所有的組件調(diào)用都根據(jù)URL中配置的參數(shù)做處理蝶柿。其實(shí)SPI技術(shù)在很多地方都有用到,比如數(shù)據(jù)庫的驅(qū)動(dòng)非驮,日志的處理交汤,原理不是很復(fù)雜,仔細(xì)研究下就明白了劫笙。