微服务中的HTTP标头转发

微服务已不再是趋势。 不管喜欢与否,他们会留下来。 但是,在接受微服务架构并正确实施它们之前,仍存在巨大差距。 提醒一下,人们可能首先想检查一下分布式计算的许多谬误 。 克服这些要求所必需的所有要求中,包括能够跟随特定业务场景中涉及的微服务遵循一个HTTP请求的能力-用于监视和调试。

它的一种可能的实现是专用的HTTP标头,该标头具有不变的值,该值沿调用链中涉及的每个微服务传递。 这周,我在Spring微服务上开发了这种排序监控,并希望分享实现方法。

标头即用转发

在Spring生态系统中, Spring Cloud Sleuth是专用于此 库:

Spring Cloud Sleuth从DapperZipkin和HTrace大量借鉴了Spring Cloud的分布式跟踪解决方案。 对于大多数用户而言,Sleuth应该是不可见的,并且您与外部系统的所有交互都应自动进行检测。 您可以简单地在日志中捕获数据,也可以将其发送到远程收集器服务。

在Spring Boot项目中,将Spring Cloud Sleuth库添加到类路径将自动为所有调用添加2个HTTP标头:

X-B3-Traceid

由单个事务的所有HTTP调用共享, 希望的事务标识符

X-B3-Spanid

识别交易期间单个微服务的工作

Spring Cloud Sleuth提供了一些自定义功能,例如备用标头名称,但需要付出一些额外的代码。

不同于开箱即用的功能

从整洁的架构开始时,这些功能非常方便。 不幸的是,我正在工作的项目具有不同的上下文:

  • 交易ID不是由调用链中的第一个微服务创建的-强制外观代理会创建
  • 交易ID不是数字-并且Sleuth仅处理数字值
  • 需要另一个标头。 其目标是将与不同呼叫链上的一个业务场景相关的所有请求分组
  • 第三个标头是必需的。 调用链中的每个新微服务都会增加它

解决方案架构师的第一步是检查API管理产品(例如Apigee(最近由Google购买))并搜索提供符合这些要求的功能的产品。 不幸的是,当前上下文不允许这样做。

编写要求

最后,我最终使用Spring框架编写了以下代码:

  1. 从初始请求中读取和存储标题
  2. 将它们写入新的微服务请求中
  3. 从微服务响应中读取和存储标头
  4. 将它们写入对发起方的最终响应中,不要忘记增加调用计数器
头流建模

标头持有人

第一步是创建负责保存所有必要标头的实体。 它被想象为HeadersHolder 。 怪我所有你想要的,我找不到一个更具描述性的名字。

privateconstvalHOP_KEY="hop"
privateconstvalREQUEST_ID_KEY="request-id"
privateconstvalSESSION_ID_KEY="session-id"

data classHeadersHolder(varhop:Int?,
                          varrequestId:String?,
                          varsessionId:String?)

有趣的部分是确定哪个范围与该类的实例更相关。显然,必须有多个实例,因此这使singleton不适合。 同样,由于数据必须存储在多个请求中,因此不能将其prototype 。 最后,管理实例的唯一可能方法是通过ThreadLocal

尽管可以管理ThreadLocal ,但让我们利用Spring的功能,因为它可以轻松添加新的作用域。 ThreadLocal已经有一个现成的作用域,只需要在上下文中注册它即可。 这将直接转换为以下代码:

internalconstvalTHREAD_SCOPE="thread"

@Scope(THREAD_SCOPE)
annotationclassThreadScope

@Configuration
openclassWebConfigurer{

    @Bean@ThreadScope
    openfunheadersHolder()=HeadersHolder()

    @BeanopenfuncustomScopeConfigurer()=CustomScopeConfigurer().apply{
        addScope(THREAD_SCOPE,SimpleThreadScope())
    }
}

服务器部分中的标题

让我们实现上面的要求1和4:从请求中读取标头,并将其写入响应。 同样,在请求-响应周期之后,还需要重置标头以准备下一个。

这也要求将持有人类别更新为对OOP更友好:

data classHeadersHolderprivateconstructor(privatevarhop:Int?,
                                              privatevarrequestId:String?,
                                              privatevarsessionId:String?){
    constructor():this(null,null,null)

    funreadFrom(request:HttpServletRequest){
        this.hop=request.getIntHeader(HOP_KEY)
        this.requestId=request.getHeader(REQUEST_ID_KEY)
        this.sessionId=request.getHeader(SESSION_ID_KEY)
    }

    funwriteTo(response:HttpServletResponse){
        hop?.let{response.addIntHeader(HOP_KEY,hopasInt)}
        response.addHeader(REQUEST_ID_KEY,requestId)
        response.addHeader(SESSION_ID_KEY,sessionId)
    }

    funclear(){
        hop=null
        requestId=null
        sessionId=null
    }
}

为了使控制器不受头管理的关注,相关代码应位于过滤器或类似组件中。 在Spring MVC生态系统中,这转化为拦截器。

abstractclassHeadersServerInterceptor:HandlerInterceptorAdapter(){

    abstractvalheadersHolder:HeadersHolder

    overridefunpreHandle(request:HttpServletRequest,
                           response:HttpServletResponse,handler:Any):Boolean{
        headersHolder.readFrom(request)
        returntrue
    }

    overridefunafterCompletion(request:HttpServletRequest,response:HttpServletResponse,
                                 handler:Any,ex:Exception?){
        with(headersHolder){
            writeTo(response)
            clear()
        }
    }
}

@ConfigurationopenclassWebConfigurer:WebMvcConfigurerAdapter(){

    overridefunaddInterceptors(registry:InterceptorRegistry){
        registry.addInterceptor(object: HeadersServerInterceptor(){
            overridevalheadersHolder:HeadersHolder
                get()=headersHolder()
        })
    }
}

请注意调用clear()方法以重置下一个请求的标头持有者。

最重要的是抽象的 headersHolder属性。 由于其作用域-线程小于适配器的作用域,因此不能直接注入它,而只能在Spring上下文启动期间注入它。 因此,Spring提供了查找方法注入 。 上面的代码是其在Kotlin中的直接翻译。

客户电话中的标题

前面的代码假定当前的微服务位于调用者链的末尾:它读取请求标头并将其写回响应中(不要忘记增加“跳数”计数器)。 但是,监视仅与具有多个单个链接的呼叫者链相关。 如何将标头传递给下一个微服务(并将其返回)-上面的要求2和3?

Spring提供了一个方便的抽象来处理该客户端部分ClientHttpRequestInterceptor ,可以将其注册到REST模板。 关于范围不匹配,使用与上面的拦截器处理程序相同的注入技巧。

abstractclassHeadersClientInterceptor:ClientHttpRequestInterceptor{

    abstractvalheadersHolder:HeadersHolder

    overridefunintercept(request:HttpRequest,
                           body:ByteArray,execution:ClientHttpRequestExecution):ClientHttpResponse{
        with(headersHolder){
            writeTo(request.headers)
            returnexecution.execute(request,body).apply{
                readFrom(this.headers)
            }
        }
    }
}

@Configuration
openclassWebConfigurer:WebMvcConfigurerAdapter(){

    @BeanopenfunheadersClientInterceptor()=object: HeadersClientInterceptor(){
        overridevalheadersHolder:HeadersHolder
            get()=headersHolder()
    }

    @BeanopenfunoAuth2RestTemplate()=OAuth2RestTemplate(clientCredentialsResourceDetails()).apply{
        interceptors=listOf(headersClientInterceptor())
    }
}

使用此代码,每个使用oAuth2RestTemplate() REST调用都将具有由拦截器自动管理的标头。

HeadersHolder只需要快速更新:

data classHeadersHolderprivateconstructor(privatevarhop:Int?,
                                              privatevarrequestId:String?,
                                              privatevarsessionId:String?){

    funreadFrom(headers:org.springframework.http.HttpHeaders){
        headers[HOP_KEY]?.let{
            it.getOrNull(0)?.let{this.hop=it.toInt()}
        }
        headers[REQUEST_ID_KEY]?.let{this.requestId=it.getOrNull(0)}
        headers[SESSION_ID_KEY]?.let{this.sessionId=it.getOrNull(0)}
    }

    funwriteTo(headers:org.springframework.http.HttpHeaders){
        hop?.let{headers.add(HOP_KEY,hop.toString())}
        headers.add(REQUEST_ID_KEY,requestId)
        headers.add(SESSION_ID_KEY,sessionId)
    }
}

结论

Spring Cloud提供了许多可以在开发微服务时直接使用的组件。 当需求开始偏离提供的需求时,可以利用底层Spring Framework的灵活性来编码那些需求。

翻译自: https://blog.frankel.ch/http-headers-management-with-spring/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值