backend scaffold
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
dependencies {
|
||||
api project(':platform:platform-web')
|
||||
|
||||
api 'org.springframework.boot:spring-boot-starter-web'
|
||||
api 'org.apache.httpcomponents.client5:httpclient5'
|
||||
// Boot 4 / Spring Framework 7 对应的 Resilience4j starter 是 -spring-boot4
|
||||
api 'io.github.resilience4j:resilience4j-spring-boot4:2.4.0'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-aspectj'
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.continental.retailapp.platform.integration
|
||||
|
||||
import jakarta.validation.Valid
|
||||
import jakarta.validation.constraints.Min
|
||||
import jakarta.validation.constraints.NotBlank
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.validation.annotation.Validated
|
||||
|
||||
/**
|
||||
* 外部依赖的连接参数,见 05-integration-layer.md。
|
||||
* 全部走 `@ConfigurationProperties` + `@Validated`,不用 `@Value`——配置错了要在启动时就炸,
|
||||
* 而不是等第一次调用外部系统时才发现(见 07-config-governance.md)。
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "integration")
|
||||
@Validated
|
||||
data class IntegrationClientProperties(
|
||||
val clients: Map<String, @Valid ClientConfig> = emptyMap(),
|
||||
) {
|
||||
data class ClientConfig(
|
||||
@field:NotBlank val baseUrl: String,
|
||||
@field:Min(100) val connectTimeoutMs: Long = 1000,
|
||||
@field:Min(100) val readTimeoutMs: Long = 2000,
|
||||
/**
|
||||
* 最容易漏配的一项:从连接池拿连接的等待超时。
|
||||
* 不配的话池满时线程会一直排队,connect/read 超时全都管不到它。
|
||||
*/
|
||||
@field:Min(50) val connectionRequestTimeoutMs: Long = 500,
|
||||
@field:Min(1) val maxConnections: Int = 60,
|
||||
@field:Min(1) val maxConnectionsPerRoute: Int = 30,
|
||||
)
|
||||
|
||||
fun require(name: String): ClientConfig =
|
||||
clients[name] ?: error("缺少 integration.clients.$name 配置")
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.continental.retailapp.platform.integration
|
||||
|
||||
import com.continental.retailapp.platform.web.BusinessException
|
||||
import org.springframework.http.HttpStatus
|
||||
|
||||
/**
|
||||
* 外部依赖失败的统一父类,见 05-integration-layer.md。
|
||||
* 默认 502:调用方(APP)需要知道"不是你的请求有问题,是我依赖的系统挂了"。
|
||||
*
|
||||
* 注意 message 是**给用户看的文案**,绝不能把下游的原始报错透出去
|
||||
* (可能含内网地址、SQL、堆栈)——原始信息只进日志。
|
||||
*/
|
||||
open class IntegrationException(
|
||||
code: Int,
|
||||
message: String,
|
||||
httpStatus: HttpStatus = HttpStatus.BAD_GATEWAY,
|
||||
) : BusinessException(code, message, httpStatus)
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package com.continental.retailapp.platform.integration
|
||||
|
||||
import org.apache.hc.client5.http.config.ConnectionConfig
|
||||
import org.apache.hc.client5.http.config.RequestConfig
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClients
|
||||
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder
|
||||
import org.apache.hc.core5.util.TimeValue
|
||||
import org.apache.hc.core5.util.Timeout
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory
|
||||
import org.springframework.web.client.RestClient
|
||||
|
||||
/**
|
||||
* 统一构建 [RestClient]:超时、连接池、trace 透传都收口在这里,见 05-integration-layer.md。
|
||||
*
|
||||
* 对文档的有意偏离 #3:05 文档把 `f6RestClient` 的 `@Bean` 放在 platform-integration,
|
||||
* 但它的 statusHandler 抛的是 f6-adapter 里的 `F6ServerException`——平台层反过来依赖适配层,
|
||||
* 方向是反的。这里把它拆成"平台提供工厂、各适配器自己声明 bean 并给自己的 statusHandler"。
|
||||
*
|
||||
* 用同步 `RestClient` + Apache HttpClient 5 而不是 WebClient:整条链路是同步阻塞的 Servlet 栈,
|
||||
* 引入 Reactor 只会带来 `.block()` 和难排查的线程模型。
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(IntegrationClientProperties::class)
|
||||
class RestClientFactory(
|
||||
private val tracePropagationInterceptor: TracePropagationInterceptor,
|
||||
) {
|
||||
|
||||
fun build(
|
||||
config: IntegrationClientProperties.ClientConfig,
|
||||
customizer: (RestClient.Builder) -> Unit = {},
|
||||
): RestClient = RestClient.builder()
|
||||
.baseUrl(config.baseUrl)
|
||||
.requestFactory(requestFactory(config))
|
||||
.requestInterceptor(tracePropagationInterceptor)
|
||||
.apply(customizer)
|
||||
.build()
|
||||
|
||||
private fun requestFactory(
|
||||
config: IntegrationClientProperties.ClientConfig,
|
||||
): HttpComponentsClientHttpRequestFactory {
|
||||
val connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
|
||||
.setMaxConnTotal(config.maxConnections)
|
||||
.setMaxConnPerRoute(config.maxConnectionsPerRoute)
|
||||
.setDefaultConnectionConfig(
|
||||
ConnectionConfig.custom()
|
||||
.setConnectTimeout(Timeout.ofMilliseconds(config.connectTimeoutMs))
|
||||
// 拿到连接后先探活,避免复用到对端已经关掉的半开连接
|
||||
.setValidateAfterInactivity(TimeValue.ofSeconds(5))
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
|
||||
val httpClient = HttpClients.custom()
|
||||
.setConnectionManager(connectionManager)
|
||||
.setDefaultRequestConfig(
|
||||
RequestConfig.custom()
|
||||
.setConnectionRequestTimeout(Timeout.ofMilliseconds(config.connectionRequestTimeoutMs))
|
||||
.setResponseTimeout(Timeout.ofMilliseconds(config.readTimeoutMs))
|
||||
.build(),
|
||||
)
|
||||
.evictIdleConnections(TimeValue.ofSeconds(30))
|
||||
.build()
|
||||
|
||||
return HttpComponentsClientHttpRequestFactory(httpClient)
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.continental.retailapp.platform.integration
|
||||
|
||||
import org.slf4j.MDC
|
||||
import org.springframework.http.HttpRequest
|
||||
import org.springframework.http.client.ClientHttpRequestExecution
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor
|
||||
import org.springframework.http.client.ClientHttpResponse
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
/**
|
||||
* 把当前 traceId 透传给下游,见 05-integration-layer.md / 08-observability.md。
|
||||
* 下游出问题时能用同一个 ID 把两边日志串起来。
|
||||
*/
|
||||
@Component
|
||||
class TracePropagationInterceptor : ClientHttpRequestInterceptor {
|
||||
|
||||
override fun intercept(
|
||||
request: HttpRequest,
|
||||
body: ByteArray,
|
||||
execution: ClientHttpRequestExecution,
|
||||
): ClientHttpResponse {
|
||||
MDC.get("traceId")?.let { request.headers.set("X-Trace-Id", it) }
|
||||
return execution.execute(request, body)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user