Skip to content

This starter module serves as a temporary solution of using Spring Boot with Coroutine Feign, until Spring Cloud OpenFeign officially supports it.

License

Notifications You must be signed in to change notification settings

sunny-chung/spring-starter-feign-coroutine

Repository files navigation

Spring Feign Coroutine Starter

GitHub Maven Central

NOTE: Since v0.4.0, the artifact groupId is changed to 'io.github.sunny-chung'. Also, a dependency resolution rule is needed to add to build.gradle.kts.

This starter module serves as a temporary solution of using Spring Boot with Coroutine Feign, until Spring Cloud OpenFeign officially supports it. This module provides Spring Boot integration with the Feign Kotlin module, the reactive Spring WebClient and Micrometer.

A limited set of Spring Cloud OpenFeign features is supported. Unsupported features include:

  • Default Encoder and Decoder beans (users must provide these beans)
  • Load balancer (URL starts with lb://)
  • Refresh context
  • Lazy attributes
  • AOT
  • Circuit breaker
  • PageJacksonModule and SortJacksonModule beans (users may still customize the encoders and decoders to support)
  • OAuth 2 interceptors
  • Caching
  • Client-scoped configuration beans
  • Blocking HTTP clients (users can use asynchronous HTTP clients)

Except above, users are expected to be able to migrate their existing Spring Cloud OpenFeign integrations to this module directly and painlessly (hopefully).

Performance

A load test was performed using JMeter against the example project in this repository.

Setting:

  • 1000 concurrent threads
  • Infinite loops of calls within 200 seconds
  • Each request requires 3s non-blocking processing time
  • Call path: Spring Cloud Gateway --> Example Application --(feign)--> Example Application
  • All services are run inside docker containers
  • Each docker container has only 200 MB memory limit
Direct call without feign Spring Feign Coroutine Starter feign-kotlin
Median 3025 ms 3017 ms Out-of-memory Crash
95% 3149 ms 3095 ms Out-of-memory Crash
99% 3322 ms 3137 ms Out-of-memory Crash
Maximum 3583 ms 3633 ms Out-of-memory Crash
Error 0.0 % 0.0 % Out-of-memory Crash
Throughput 311.1 / s 314.0 / s Out-of-memory Crash

It proved that Spring Feign Coroutine Starter takes full advantages of non-blocking coroutines to achieve a performance similar to direct calling the underlying endpoint. In the test, the median latency added by this module is about 17ms.

Note there is environment noise in the load test. The result does not mean using feign has lower latencies than direct call.

Interested individuals may carry out tests by making use of the JMeter script included, the starter script and the example projects (API Gateway, Example Application).

Getting Started

In build.gradle.kts (or equivalent), add:

dependencies {
    // ...
    implementation("io.github.sunny-chung:spring-starter-feign-coroutine:<version>")
}

Since v0.4.0, it is not compatible with some of the official feign modules, so please also add to the root level of build.gradle.kts:

configurations.all {
    resolutionStrategy.eachDependency {
        if (requested.group == "io.github.openfeign" && requested.name in setOf("feign-kotlin", "feign-core")) {
            useTarget("io.github.sunny-chung:${requested.name}:13.2.1-patch-1")
        }
    }
}

If there are multiple Gradle modules within the same project, the resolution can be added to the root build.gradle.kts instead:

subprojects {
    configurations.all {
        resolutionStrategy.eachDependency {
            if (requested.group == "io.github.openfeign" && requested.name in setOf("feign-kotlin", "feign-core")) {
                useTarget("io.github.sunny-chung:${requested.name}:13.2.1-patch-1")
            }
        }
    }
}

Example

@Configuration
@EnableCoroutineFeignClients(basePackages = ["com.sunnychung.example.springfeigncoroutine"])
class FeignConfig {
    @Bean
    fun decoder() = JacksonDecoder(jacksonObjectMapper())

    @Bean
    fun encoder() = JacksonEncoder()
}
@CoroutineFeignClient(name = "svc-a")
interface RemoteApi {

    @PostMapping("a")
    suspend fun a(@RequestBody req: ApiData): ApiData
}
spring:
  cloud:
    openfeign:
      client:
        config:
          default:
            default-request-headers:
              content-type: application/json
          svc-a:
            logger-level: full
            url: http://service-a:8080/api/
@RequestMapping("api")
class ApiController {

    @Autowired
    lateinit var remoteApi: RemoteApi

    @PostMapping("b")
    suspend fun b(): ApiData {
        return remoteApi.a(ApiData("b"))
    }
}

Limitation

Entire response body is read into memory before next step, this means it would perform not as optimized as reactive clients for large response body or streaming.

Flow<T> is not yet supported.

Micrometer Integration

Similar to Spring Cloud OpenFeign, just include Micrometer in classpath, then everything is autoconfigured. It can be configured using the same set of application properties.

Demo

Run the demo servers using ./run-local.sh, make a cURL request to API gateway which calls service-b which calls service-a.

curl --verbose \
  --request "POST" \
  --url "http://localhost:10000/api/b" \
  --header "Content-Type: application/json" \
  --data "{\"x\": 10}"

The server log combined in Docker Compose like below is observed:

spring-feign-coroutine-apigateway-1  | 2024-02-24T08:33:12.075Z  INFO 1 --- [ctor-http-nio-2] [65d9a9c86af376b32fea95088ceb267b-2fea95088ceb267b] c.s.e.springfeigncoroutine.LogFilter     : Request -- POST /api/b
spring-feign-coroutine-service-b-1   | 2024-02-24T08:33:12.602Z  INFO 1 --- [         task-1] [65d9a9c86af376b32fea95088ceb267b-5273a545473a290f] c.s.e.s.ApiController                    : API b
spring-feign-coroutine-service-b-1   | 2024-02-24T08:33:12.608Z DEBUG 1 --- [         task-1] [65d9a9c86af376b32fea95088ceb267b-5273a545473a290f] c.s.e.springfeigncoroutine.RemoteApi     : [RemoteApi#a] ---> POST http://service-a:8080/api/a HTTP/1.1
spring-feign-coroutine-service-b-1   | 2024-02-24T08:33:12.608Z DEBUG 1 --- [         task-1] [65d9a9c86af376b32fea95088ceb267b-5273a545473a290f] c.s.e.springfeigncoroutine.RemoteApi     : [RemoteApi#a] Content-Length: 15
spring-feign-coroutine-service-b-1   | 2024-02-24T08:33:12.608Z DEBUG 1 --- [         task-1] [65d9a9c86af376b32fea95088ceb267b-5273a545473a290f] c.s.e.springfeigncoroutine.RemoteApi     : [RemoteApi#a] content-type: application/json
spring-feign-coroutine-service-b-1   | 2024-02-24T08:33:12.608Z DEBUG 1 --- [         task-1] [65d9a9c86af376b32fea95088ceb267b-5273a545473a290f] c.s.e.springfeigncoroutine.RemoteApi     : [RemoteApi#a] 
spring-feign-coroutine-service-b-1   | 2024-02-24T08:33:12.608Z DEBUG 1 --- [         task-1] [65d9a9c86af376b32fea95088ceb267b-5273a545473a290f] c.s.e.springfeigncoroutine.RemoteApi     : [RemoteApi#a] {
spring-feign-coroutine-service-b-1   |   "x" : "b"
spring-feign-coroutine-service-b-1   | }
spring-feign-coroutine-service-b-1   | 2024-02-24T08:33:12.608Z DEBUG 1 --- [         task-1] [65d9a9c86af376b32fea95088ceb267b-5273a545473a290f] c.s.e.springfeigncoroutine.RemoteApi     : [RemoteApi#a] ---> END HTTP (15-byte body)
spring-feign-coroutine-service-a-1   | 2024-02-24T08:33:13.211Z  INFO 1 --- [ctor-http-nio-2] [65d9a9c86af376b32fea95088ceb267b-30c56df6c7d7ee63] c.s.e.s.ApiController                    : API a -- b
spring-feign-coroutine-service-b-1   | 2024-02-24T08:33:16.333Z DEBUG 1 --- [ctor-http-nio-4] [65d9a9c86af376b32fea95088ceb267b-5273a545473a290f] c.s.e.springfeigncoroutine.RemoteApi     : [RemoteApi#a] <--- HTTP/1.1 200 (3724ms)
spring-feign-coroutine-service-b-1   | 2024-02-24T08:33:16.333Z DEBUG 1 --- [ctor-http-nio-4] [65d9a9c86af376b32fea95088ceb267b-5273a545473a290f] c.s.e.springfeigncoroutine.RemoteApi     : [RemoteApi#a] content-length: 11
spring-feign-coroutine-service-b-1   | 2024-02-24T08:33:16.333Z DEBUG 1 --- [ctor-http-nio-4] [65d9a9c86af376b32fea95088ceb267b-5273a545473a290f] c.s.e.springfeigncoroutine.RemoteApi     : [RemoteApi#a] content-type: application/json
spring-feign-coroutine-service-b-1   | 2024-02-24T08:33:16.333Z DEBUG 1 --- [ctor-http-nio-4] [65d9a9c86af376b32fea95088ceb267b-5273a545473a290f] c.s.e.springfeigncoroutine.RemoteApi     : [RemoteApi#a] 
spring-feign-coroutine-service-b-1   | 2024-02-24T08:33:16.333Z DEBUG 1 --- [ctor-http-nio-4] [65d9a9c86af376b32fea95088ceb267b-5273a545473a290f] c.s.e.springfeigncoroutine.RemoteApi     : [RemoteApi#a] {"x":"b a"}
spring-feign-coroutine-service-b-1   | 2024-02-24T08:33:16.333Z DEBUG 1 --- [ctor-http-nio-4] [65d9a9c86af376b32fea95088ceb267b-5273a545473a290f] c.s.e.springfeigncoroutine.RemoteApi     : [RemoteApi#a] <--- END HTTP (11-byte body)
spring-feign-coroutine-apigateway-1  | 2024-02-24T08:33:16.370Z  INFO 1 --- [ctor-http-nio-2] [65d9a9c86af376b32fea95088ceb267b-2fea95088ceb267b] c.s.e.springfeigncoroutine.LogFilter     : Response -- POST /api/b -- 4294ms

Note the traceId (65d9a9c86af376b32fea95088ceb267b) is propagated and consistent.

About

This starter module serves as a temporary solution of using Spring Boot with Coroutine Feign, until Spring Cloud OpenFeign officially supports it.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages