微服务环境,有A,B,C,D四个服务,调用关系为:A->B->C->D。用户在A的页面选择当前“语言”环境为“英文”,在某些业务场景下,其它几个服务需获取到这个“语言”信息。java
这个需求仍是很简单的,相似于“击鼓传花”:当前服务从上一个服务中获取参数,并传给下一个服务。我的感受基本上全部的RPC框架都会遇到这个问题,只是之前SOA架构下,服务层级比较少,将“语言”、“登录”等附加信息放在参数列表中并不会带来太多工做量,因此这个问题并非太突出。而引入了微服务架构思想后,服务调用层级急剧增加,这就须要一个更加优雅的方式来解决附加信息的传递问题。git
优势:思路简单,开发没有学习成本github
缺点:spring
思考:微服务之间绝大多数状况是经过HTTP调用的,HTTP的header中也能够放参数信息。这样,接口参数中就不用维护这些附加信了。缓存
实现:
1.自定义一个Filter,获取Request中本身须要的附加信息,
2.将这些信息放入ThreadLocal中,
3.实现feign.Client(这里先忽略RestTemplate)的execute()方法,将附件信息在调用下一层服务前塞入request的header中session
优势:参数解耦多线程
缺点:若是B在获取到附加信息后,新起了一个线程”T1“来调用服务C,这时T1就没法从HhreaLocal拿到附加信息了架构
思考:app
- 若是我知道怎么用无侵入的方式,在当前线程”T”建立子孙线程”T1”、”T1-1”时,将数据传给后代,就能解决这个问题了
- 微服务调用链框架Sleuth的核心功能便是跟踪一次请求从A到D的全过程,它确定支持多线程调用下的traceId的传递。所以,我能够复用Sleuth的相关功能夹带私货
优势:框架
思考:
目前获取参数的问题解决了,用Filter,只剩下保存并传给下一层的问题
既然Sleuth已经解决了多线程下traceId的传递问题,那我就直接用traceId来解决个人问题
实现:
优势:拥有上述方案全部的优势,解决上述方案全部缺点
缺点:看着很完美,可是你忽略了一件事:Sleuth要想传递本身的traceId,想必它已经重写了execute()方法(确定的,那就是TraceFeignClient),你要想用,那就要想办法在复用TraceFeignClient.execute()的同时,将本身的私货带进去
实现:有时候,改动源码并不须要直接在原有包里修改。好比:A->B->C->D,若是你要修改C的源码,那就将AB源码也copy出,做为A1,B1,C#,而后重写组件的入口,将组件加载顺序变为:A1->B1->C#->D,便可达到重写源码的目的。这时候注意的是,加载A1的条件必须跟加载A的相反。具体可参考我以前重写Consul的入口例子,示例代码以下
@ConditionalOnExpression("${spring.cloud.consul.ribbon.enabled:true}==false") public class MyRibbonConsulAutoConfiguration {} // 原有入口: @ConditionalOnProperty(value = "spring.cloud.consul.ribbon.enabled", matchIfMissing = true) public class RibbonConsulAutoConfiguration {}
综上,能够重写TraceFeigClient的入口 TraceFeignClientAutoConfiguration->TraceFeignObjectWrapper>TraceFeignClient,便可达到本身的目的.
优势:感受事儿基本就成了
缺点:配置为false生效,使用者会以为比较怪,Sleuth仿佛知作别人会这么干似的,它的类的访问权限基本都是default,为了copy过来的几个类能正常编译经过,你还要再copy九个它们的依赖类,程序太丑
思考:忽然想起来,还有一种改代码的方式叫字节码替换,若是我能在程序启动的时,将个人execute()直接替换掉Sleuth的execute(),就一劳永逸了
优势:高大上,不在源码级替换,却在字节码级替换,虚虚实实
缺点:没这么干过,总以为说着容易作着难
思考:基本上以为方案五已经能解决问题了。本着精益求精的态度,去技术群里问了下,很快有大神发来Demo,看过代码后顿觉惭愧:我一直在想怎么重写TraceFeignClient的execute(),其实这个execute()真正作http请求时,调用的是feign.Client的另一个实现类,注意那句”this.delegate.execute”,只要想办法用本身的Client替换掉delegate便可
private static final Log log = LogFactory.getLog(MethodHandles.lookup().lookupClass()); private final Client delegate; @Override public Response execute(Request request, Request.Options options) throws IOException { String spanName = getSpanName(request); Span span = getTracer().createSpan(spanName); if (log.isDebugEnabled()) { log.debug("Created new Feign span " + span); } try { AtomicReference<Request> feignRequest = new AtomicReference<>(request); spanInjector().inject(span, new FeignRequestTextMap(feignRequest)); span.logEvent(Span.CLIENT_SEND); addRequestTags(request); Request modifiedRequest = feignRequest.get(); if (log.isDebugEnabled()) { log.debug("The modified request equals " + modifiedRequest); } Response response = this.delegate.execute(modifiedRequest, options); logCr(); return response; } catch (RuntimeException | IOException e) { logCr(); logError(e); throw e; } finally { closeSpan(span); } }
实现:经过再次认真Debug源码知道,TraceFeignClient默认会加载你的Client实现类做为delegate(汗!),所以你只要直接实现feign.Client接口便可。我偷懒了一把,本身写个实现类,直接复用了LoadBalancerFeignClient.execute()
优势:基本什么都有了吧
缺点:若是你觉得只是简单地重写个execute()就行,那就大错特了。由于TraceFeignClient直接用了你的方法post过去,所以你要想办法把ribbon手动集成进来。若是不以为麻烦的话,能够好好看下TraceFeignClient怎么生成Client的实例:TraceFeignObjectWrapper.wrap(Object bean)
思考:既然你能够在程序里获取到trace和span,那为什么不将你的信息放到span里呢。若是span中能放点额外信息就行了,就不用本身写这么多东西。经大神提醒,Sleuth中有个baggage能够试试
实现:获取参数的方式不变,取得的参数放在baggage中
优势:简单,支持RestTemplate调用的状况,跟其余组件兼容性好
缺点:Sleuth的缺点
Github地址:https://github.com/bishion/sleuth-plugin
简介:微服务下使用,调用过程当中用户信息,页面语言信息的透传
使用方式
bizi: sleuth: config: headers: lang_info #若是由多个,逗号隔开.这里配置从filter里须要获取的headerName
调用方式
@Service public class SessionInfoService { @Resource private SessionInfoOperator sessionInfoOperator; public String getLangInfo(){ return sessionInfoOperator.getSessionInfo("lang_info"); } public void setUserId(){ sessionInfoOperator.setSessionInfo("user_id","bishion"); } }
由于附加信息的传递在RPC中扮演了很重要的角色,我潜意识里以为,确定会有更加简洁的方法或者框架我尚未了解到。但愿各位各位读者老师能不吝珠玉,批评指正