com.tencent.devops.common.client.Client.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of common-client Show documentation
Show all versions of common-client Show documentation
Tencent blueking ci project
/*
* Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
*
* BK-CI 蓝鲸持续集成平台 is licensed under the MIT license.
*
* A copy of the MIT License is included in this file.
*
*
* Terms of the MIT License:
* ---------------------------------------------------
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of
* the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
* NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.tencent.devops.common.client
import com.fasterxml.jackson.databind.ObjectMapper
import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.LoadingCache
import com.tencent.devops.common.api.annotation.ServiceInterface
import com.tencent.devops.common.api.exception.ClientException
import com.tencent.devops.common.api.exception.RemoteServiceException
import com.tencent.devops.common.client.ms.MicroServiceTarget
import com.tencent.devops.common.client.pojo.enums.GatewayType
import com.tencent.devops.common.service.BkTag
import com.tencent.devops.common.service.config.CommonConfig
import com.tencent.devops.common.service.utils.KubernetesUtils
import com.tencent.devops.common.service.utils.SpringContextUtil
import feign.Contract
import feign.Feign
import feign.MethodMetadata
import feign.Request
import feign.RequestInterceptor
import feign.RetryableException
import feign.Retryer
import feign.jackson.JacksonDecoder
import feign.jackson.JacksonEncoder
import feign.jaxrs.JAXRSContract
import feign.okhttp.OkHttpClient
import feign.spring.SpringContract
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClient
import org.springframework.context.annotation.DependsOn
import org.springframework.core.annotation.AnnotationUtils
import org.springframework.stereotype.Component
import java.lang.reflect.Method
import java.security.cert.CertificateException
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
import kotlin.reflect.KClass
/**
*
* Powered By Tencent
*/
@Suppress("UNUSED")
@Component
@DependsOn("springContextUtil")
class Client @Autowired constructor(
private val compositeDiscoveryClient: CompositeDiscoveryClient?,
private val clientErrorDecoder: ClientErrorDecoder,
private val commonConfig: CommonConfig,
private val bkTag: BkTag,
objectMapper: ObjectMapper
) {
companion object {
private val logger = LoggerFactory.getLogger(Client::class.java)
private const val readWriteTimeoutSeconds = 15L
private const val connectTimeoutSeconds = 5L
private const val CACHE_SIZE = 1000L
private val longTimeOptions = Request.Options(10L, TimeUnit.SECONDS, 30L, TimeUnit.MINUTES, true)
}
private val beanCaches: LoadingCache, *> = Caffeine.newBuilder()
.maximumSize(CACHE_SIZE).build { key -> getImpl(key) }
private val interfaces = ConcurrentHashMap, String>()
private val trustAllCerts: Array = arrayOf(object : X509TrustManager {
@Throws(CertificateException::class)
override fun checkClientTrusted(chain: Array, authType: String) = Unit
@Throws(CertificateException::class)
override fun checkServerTrusted(chain: Array, authType: String) = Unit
override fun getAcceptedIssuers(): Array {
return arrayOf()
}
})
private fun sslSocketFactory(): SSLSocketFactory {
try {
val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, trustAllCerts, java.security.SecureRandom())
return sslContext.socketFactory
} catch (ignored: Exception) {
throw RemoteServiceException(ignored.message!!)
}
}
private val okHttpClient = okhttp3.OkHttpClient.Builder()
.connectTimeout(connectTimeoutSeconds, TimeUnit.SECONDS)
.readTimeout(readWriteTimeoutSeconds, TimeUnit.SECONDS)
.writeTimeout(readWriteTimeoutSeconds, TimeUnit.SECONDS)
.sslSocketFactory(sslSocketFactory(), trustAllCerts[0] as X509TrustManager)
.hostnameVerifier { _, _ -> true }
.build()
private val longRunClient = OkHttpClient(
okhttp3.OkHttpClient.Builder()
.connectTimeout(10L, TimeUnit.SECONDS)
.readTimeout(30L, TimeUnit.MINUTES)
.writeTimeout(30L, TimeUnit.MINUTES)
.sslSocketFactory(sslSocketFactory(), trustAllCerts[0] as X509TrustManager)
.hostnameVerifier { _, _ -> true }
.build()
)
private val feignClient = OkHttpClient(okHttpClient)
private val clientContract = ClientContract()
private val springContract = SpringContract()
private val jacksonDecoder = JacksonDecoder(objectMapper)
private val jacksonEncoder = JacksonEncoder(objectMapper)
@Value("\${spring.cloud.consul.discovery.service-name:#{null}}")
private val assemblyServiceName: String? = null
@Value("\${service-suffix:#{null}}")
private val serviceSuffix: String? = null
fun get(clz: KClass): T {
return get(clz, "")
}
@Suppress("UNCHECKED_CAST")
fun get(clz: KClass, suffix: String): T {
return try {
beanCaches.get(clz) as T
} catch (ignored: Throwable) {
getImpl(clz)
}
}
fun getSpringMvc(clz: KClass): T {
return get(clz, "", springContract)
}
@Suppress("UNCHECKED_CAST")
fun get(clz: KClass, suffix: String, contract: Contract = clientContract): T {
return try {
beanCaches.get(clz) as T
} catch (ignored: Throwable) {
getImpl(clz, contract)
}
}
fun getWithoutRetry(clz: KClass): T {
val requestInterceptor = SpringContextUtil.getBean(RequestInterceptor::class.java) // 获取为feign定义的拦截器
return Feign.builder()
.client(longRunClient)
.errorDecoder(clientErrorDecoder)
.encoder(jacksonEncoder)
.decoder(jacksonDecoder)
.contract(clientContract)
.requestInterceptor(requestInterceptor)
.options(longTimeOptions) // 可复用常量对象不重复创建
.retryer(WithoutRetry()) // 优化重复创建的匿名类
.target(
MicroServiceTarget(
serviceName = findServiceName(clz),
type = clz.java,
compositeDiscoveryClient = compositeDiscoveryClient!!,
bkTag = bkTag
)
)
}
fun getExternalServiceWithoutRetry(serviceName: String, clz: KClass): T {
val requestInterceptor = SpringContextUtil.getBean(RequestInterceptor::class.java) // 获取为feign定义的拦截器
return Feign.builder()
.client(longRunClient)
.errorDecoder(clientErrorDecoder)
.encoder(jacksonEncoder)
.decoder(jacksonDecoder)
.contract(clientContract)
.requestInterceptor(requestInterceptor)
.options(longTimeOptions) // 可复用常量对象不重复创建
.retryer(WithoutRetry()) // 优化重复创建的匿名类
.target(
MicroServiceTarget(
serviceName = serviceName,
type = clz.java,
compositeDiscoveryClient = compositeDiscoveryClient!!,
bkTag = bkTag
)
)
}
/**
* 通过网关访问微服务接口
*
*/
fun getGateway(clz: KClass, gatewayType: GatewayType = GatewayType.IDC): T {
// 从网关访问去掉后缀,否则会变成 /process-devops/api/service/piplines 导致访问失败
val serviceName = findServiceName(clz).removeSuffix(serviceSuffix ?: "")
val requestInterceptor = SpringContextUtil.getBean(RequestInterceptor::class.java) // 获取为feign定义的拦截器
return Feign.builder()
.client(feignClient)
.errorDecoder(clientErrorDecoder)
.encoder(jacksonEncoder)
.decoder(jacksonDecoder)
.contract(clientContract)
.requestInterceptor(requestInterceptor)
.target(clz.java, buildGatewayUrl(path = "/$serviceName/api", gatewayType = gatewayType))
}
// devnet区域的,只能直接通过ip访问
fun getScm(clz: KClass): T {
// 从网关访问去掉后缀,否则会变成 /process-devops/api/service/piplines 导致访问失败
val serviceName = findServiceName(clz).removeSuffix(serviceSuffix ?: "")
// 获取为feign定义的拦截器
val requestInterceptor = SpringContextUtil.getBeansWithClass(RequestInterceptor::class.java)
return Feign.builder()
.client(feignClient)
.errorDecoder(clientErrorDecoder)
.encoder(jacksonEncoder)
.decoder(jacksonDecoder)
.contract(clientContract)
.requestInterceptors(requestInterceptor)
.target(clz.java, buildGatewayUrl(path = "/$serviceName/api", gatewayType = GatewayType.IDC_PROXY))
}
/**
* 支持对spring MVC注解的解析
*/
fun getImpl(clz: KClass, contract: Contract = clientContract): T {
try {
return SpringContextUtil.getBean(clz.java)
} catch (ignored: Exception) {
logger.info("[$clz]|try to proxy by feign: ${ignored.message}")
}
val requestInterceptor = SpringContextUtil.getBean(RequestInterceptor::class.java) // 获取为feign定义的拦截器
return Feign.builder()
.client(feignClient)
.errorDecoder(clientErrorDecoder)
.encoder(jacksonEncoder)
.decoder(jacksonDecoder)
.contract(contract)
.requestInterceptor(requestInterceptor)
.retryer(HttpGetRetry()) // 优化重复创建的匿名类
.target(
MicroServiceTarget(
serviceName = findServiceName(clz),
type = clz.java,
compositeDiscoveryClient = compositeDiscoveryClient!!,
bkTag = bkTag
)
)
}
fun getServiceUrl(clz: KClass<*>): String {
return MicroServiceTarget(
serviceName = findServiceName(clz),
type = clz.java,
compositeDiscoveryClient = compositeDiscoveryClient!!,
bkTag = bkTag
).url()
}
private fun findServiceName(clz: KClass<*>): String {
// 单体结构,不分微服务的方式
if (!assemblyServiceName.isNullOrBlank()) {
return assemblyServiceName
}
val serviceName = interfaces.getOrPut(clz) {
val serviceInterface = AnnotationUtils.findAnnotation(clz.java, ServiceInterface::class.java)
if (serviceInterface != null && serviceInterface.value.isNotBlank()) {
serviceInterface.value
} else {
val packageName = clz.qualifiedName.toString()
val regex = Regex("""com.tencent.devops.([a-z]+).api.([a-zA-Z]+)""")
val matches = regex.find(packageName)
?: throw ClientException("无法根据接口\"$packageName\"分析所属的服务")
matches.groupValues[1]
}
}
return if (serviceSuffix.isNullOrBlank() || KubernetesUtils.inContainer()) {
serviceName
} else {
"$serviceName$serviceSuffix"
}
}
private fun buildGatewayUrl(path: String, gatewayType: GatewayType = GatewayType.IDC): String {
return if (path.startsWith("http://") || path.startsWith("https://")) {
path
} else {
val gateway = when (gatewayType) {
GatewayType.DEVNET_PROXY -> commonConfig.devopsDevnetProxyGateway!!
GatewayType.DEVNET -> commonConfig.devopsDevnetGateway!!
GatewayType.IDC -> commonConfig.devopsIdcGateway!!
GatewayType.IDC_PROXY -> commonConfig.devopsIdcProxyGateway!!
GatewayType.OSS -> commonConfig.devopsOssGateway!!
GatewayType.OSS_PROXY -> commonConfig.devopsOssProxyGateway!!
}
if (gateway.startsWith("http://") || gateway.startsWith("https://")) {
"$gateway/${path.removePrefix("/")}"
} else {
"http://$gateway/${path.removePrefix("/")}"
}
}
}
private class WithoutRetry : Retryer.Default() {
override fun clone(): Retryer = this
override fun continueOrPropagate(e: RetryableException): Unit = throw e
}
private class HttpGetRetry : Retryer.Default() {
override fun clone(): Retryer = this
override fun continueOrPropagate(e: RetryableException) {
if (e.method() != Request.HttpMethod.GET) {
throw e
} else {
super.continueOrPropagate(e)
}
}
}
}
class ClientContract : JAXRSContract() {
override fun parseAndValidateMetadata(targetType: Class<*>?, method: Method?): MethodMetadata {
val parseAndValidateMetadata = super.parseAndValidateMetadata(targetType, method)
parseAndValidateMetadata.template().decodeSlash(false)
return parseAndValidateMetadata
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy