一、业务需求问题
由于业务需要,需要在接口中传递参数,调用消息中心的短信接口,进行短信的发送。如果使用Feign接口,没有携带token时,调用Feign接口,可以正常调用,但是如果携带token,就会出现appId拼接参数的情况。appId出现拼接时什么原因导致的呢?
由于Feign接口传播的时候会带有appId,在我们的登录信息里面也有appId的信息。
我当时遇到的场景是:登录信息有appId,而且我这边需要在header头里面需要传一个appId给到消息中心服务的短信发送。下图是我这边传给消息中心服务的相关参数:
很惊奇的发现,我们的参数appId居然莫名奇妙的拼上了一个,volvo的信息。
当我看到打印的Feign的header传播时,我发现,里面也居然有一个appId,而这个appId就是我们看到的volvo。
此时我们的问题可以得到解决:那就是需要将登录信息中的appId置空,那么当我们的传入的header信息appId就会将我们的空appId进行替换。
通过LoginInfoDto loginInfoDto = ApplicationContextHelper.getBeanByType(LoginInfoDto.class);拿到登录信息,将原来的appId置空,再次请求可以看到请求成功了,可以正常的调到消息中心的短信服务发送短信。
二、 父子线程传递
问题是解决了,但是我比较好奇的是为啥Feign传播会将原先的参数进行拼接。所以这个问题需要去框架里面查询一探究竟。
可以看到配置文件里面配置了:
#微服务之间调用传递header配置测试hystrixcommand default execution isolation strategy Semaphore
如果当前的场景需要用户登陆后,再次访问网页,将用户信息loginToken放在request的header中,首先经过网关,然后到达A服务,然后A服务调用B服务时如何把loginToken传递给B服务
同时传递的过程中会将两者进行拼接。
Hystrix 提供两种执行隔离策略( ExecutionIsolationStrategy ) :SEMAPHORE
、THREAD
当隔离策略为 THREAD
时,是没办法拿到 ThreadLocal
中的值的。如果需要传递,则需要选择SEMAPHORE。
SEMAPHORE
:信号量,命令在调用线程执行。THREAD
:线程池,命令在线程池执行。
可以找到框架中的Feign header传播代码:
publicclassFeiginHeaderConfigimplementsRequestInterceptor { privateLoggerlogger=LoggerFactory.getLogger(FeiginHeaderConfig.class); privatestaticSet<String>headerSet=newHashSet(16); publicFeiginHeaderConfig() { } //重写apply方法publicvoidapply(RequestTemplaterequestTemplate) { //获取登录信息LoginInfoDtologinInfoDto= (LoginInfoDto)ApplicationContextHelper.getBeanByType(LoginInfoDto.class); if (loginInfoDto==null) { loginInfoDto= (LoginInfoDto)LoginInfoThreadLocal.LOGIN_INFO.get(); } Map<String, Object>paramMap=newHashMap(16); //获取头信息Map<String, String>headerMap= (Map)LoginInfoThreadLocal.HEADER_INFO.get(); Iteratorvar5; if (!CommonUtils.isNullOrEmpty(headerMap)) { //执行迭代var5=headerSet.iterator(); while(var5.hasNext()) { Stringheader= (String)var5.next(); if (headerMap.containsKey(header)) { paramMap.put(header, headerMap.get(header)); } } } try { Map<String, Object>infoMap=ApplicationContextHelper.beanToMap(loginInfoDto); if (!CommonUtils.isNullOrEmpty(infoMap)) { paramMap.putAll(infoMap); } } catch (IOExceptionvar8) { this.logger.error("bean转Map失败", var8); } if (!CommonUtils.isNullOrEmpty(paramMap)) { var5=paramMap.entrySet().iterator(); while(var5.hasNext()) { Entry<String, Object>param= (Entry)var5.next(); Stringvalues=param.getValue() ==null?null : String.valueOf(param.getValue()); //可以看到这里会做一个合并的操作,将同样的参数,合并成一个,数组以分号分隔requestTemplate.header((String)param.getKey(), newString[]{values}); } //这个是我们在日志空看到feign header传播的信息this.logger.debug("feign header传播:{}", JSONUtil.objectToJson(paramMap)); } StringshardingOwnerCode= (String)ThreadLocalUtil.get("shardingOwnerCode"); if (!StringUtils.isNullOrEmpty(shardingOwnerCode)) { this.logger.debug("FeiginHeaderConfig传播:{}", shardingOwnerCode); requestTemplate.query("shardingOwnerCode", newString[]{shardingOwnerCode}); } } static { headerSet.add("authorization"); headerSet.add("user-agent"); } }
同时又发现登录的信息是有父子关系的,因此不免会去看登录信息是否有我们熟悉的InheritableThreadLocal:
publicclassLoginInfoThreadLocal { publicstaticfinalInheritableThreadLocal<LoginInfoDto>LOGIN_INFO=newInheritableThreadLocal(); publicstaticfinalInheritableThreadLocal<Map<String, String>>HEADER_INFO=newInheritableThreadLocal(); publicLoginInfoThreadLocal() { } }
果然登录信息就是使用父子线程实现对登录信息的存储的,因此不难发现appId的拼接关系。
三、 场景
也即产生这种父子关系的原因是因为登录信息使用了父子线程进行了上下文的传递:
有这样的一种需求:父线程生成的变量需要传递到子线程中进行使用,那么在使用ThreadLocal似乎就解决不了这个问题。ThreadLocal有一个子类InheritableThreadLocal就是为了解决这个问题而产生的,使用这个变量就可以轻松的在子线程中依旧使用父线程中的本地变量
产生原因的代码:
而产生这种结果的原因是在:UserAuthRestInterceptor
loginInfoDto.setAppId(this.getVal(request, "appId", "appid"));