BiAiB
BiAiB

Reputation: 14122

spring boot: same feign client interface, multiple instances with different properties

I have one feignClient with this interface:

@FeignClient(
    name = "kmr-api",
    url = "${client.kmr.url}",
    configuration = KmrClient.KmrClientConfiguration.class
)
public interface KmrClient {
    @Configuration
    class KmrClientConfiguration {
        @Value("${client.kmr.access-token-uri}")
        private String accessTokenUri;

        @Value("${client.kmr.client-id}")
        private String clientId;
        
        // ...

        @Bean
        public Feign.Builder feignBuilder() {
            return Feign.builder()
                .retryer(Retryer.NEVER_RETRY)
                .options(new Request.Options(connectTimeoutMillis, readTimeoutMillis));
        }

        private OAuth2ProtectedResourceDetails resource() {
            ClientCredentialsResourceDetails resourceDetails = new ClientCredentialsResourceDetails();
            resourceDetails.setAccessTokenUri(this.accessTokenUri);
            resourceDetails.setClientId(this.clientId);
            resourceDetails.setClientSecret(this.clientSecret);
            // ...
            return resourceDetails;
        }

        @Component
        class KmrApiBrandRequestInterceptor implements RequestInterceptor {
            @Override
            public void apply(RequestTemplate requestTemplate) {
                requestTemplate.header("brand", "renault");
            }
        }
    }

    @RequestMapping(value = "/vehicle/summary", method = RequestMethod.POST)
    VehicleSummary getVehicleSummary(@RequestBody KmrVehicleId2 kmrVehicleId);
}

And associated properties such as

# Feign service
client.kmr.url=https://example/api/v1
client.kmr.access-token-uri=https://example/oauth2/access_token
client.kmr.client-id=myclientid

Now, I need several instances of this KmrClient, with the same exact interface and configuration, but with different properties. I'm trying to figure out something which could work out like this for example :

# Feign service
client.kmr[0].url=https://example/api/v1
client.kmr[0].access-token-uri=https://example/oauth2/access_token
client.kmr[0].client-id=myclientid
# Feign service
client.kmr[1].url=https://example/api/v1
client.kmr[1].access-token-uri=https://example/oauth2/access_token
client.kmr[1].client-id=myclientid
# there are 4 instances in total, one per region supported in app

From here I don't how I would load each property in client.kmr[1] and wire them into different configurations and clients.

How can I achieve this result ?

Upvotes: 5

Views: 3742

Answers (1)

BiAiB
BiAiB

Reputation: 14122

This is the solution I finally found:

First, I created my own config classes for holding the properties values:

@ConfigurationProperties(prefix = "client.kdiag")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class KdiagClientsProperties {
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    public static class KdiagClientProperties {
        String url;
        String accessTokenUri;
        String clientId;
        String clientSecret;
        String scope;
        int connectTimeoutMillis = 10000;
        int readTimeoutMillis = 15000;
    }

    Map<String, KdiagClientProperties> regions;
}

The .properties look like this:

client.kdiag.regions.EU.url=https://example.io/diag/api/v1
client.kdiag.regions.EU.access-token-uri=https://example.org/auth/oauth2/stuff/access_token
client.kdiag.regions.EU.client-id=redacted
client.kdiag.regions.EU.client-secret=redacted
client.kdiag.regions.EU.scope=scope1,scope2

client.kdiag.regions.RU.url=https://example.io/diag/api/v1
client.kdiag.regions.RU.access-token-uri=https://example.org/auth/oauth2/stuff/access_token
client.kdiag.regions.RU.client-id=redacted
client.kdiag.regions.RU.client-secret=redacted
client.kdiag.regions.RU.scope=scope1,scope2

Then I created a class called KmrClients for accessing all client instances which reads these properties:

/**
 * Allow requesting each Kdiag region.
 *
 * Get the kdiag client with @{@link KdiagClients#clientByRegion(KdisRegion)}
 */
@Order(SecurityProperties.IGNORED_ORDER)
@Configuration
@EnableConfigurationProperties(value = {KdiagClientsProperties.class})
@Import(FeignClientsConfiguration.class) // We need these to reinject decoder, encoder, contract in constructor
@Slf4j
public class KdiagClients {
    private Map<String, KdiagClient> clients;

    /**
     * builds the OAuth2 configuration for a client
     */
    private OAuth2ProtectedResourceDetails resource(KdiagClientsProperties.KdiagClientProperties properties) {
        ClientCredentialsResourceDetails resourceDetails = new ClientCredentialsResourceDetails();
        resourceDetails.setAccessTokenUri(properties.getAccessTokenUri());
        resourceDetails.setClientId(properties.getClientId());
        resourceDetails.setClientSecret(properties.getClientSecret());
        resourceDetails.setScope(Arrays.asList(properties.getScope().split(",")));
        resourceDetails.setGrantType("client_credentials");
        return resourceDetails;
    }

    @Autowired
    public KdiagClients(
        KdiagClientsProperties properties,
        Decoder decoder,
        Encoder encoder,
        Contract contract
    ) {
        Assert.notEmpty(properties.regions, "No KDiag region clients defined in properties (ex: kdiag.client.regions.EU etc)  ");
        log.info("found kdiag clients for regions:" + properties.regions.keySet().toString());
        this.clients = properties.getRegions().entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, clientPropertiesEntry -> {
            return Feign.builder()
               .retryer(Retryer.NEVER_RETRY)
               .options(new Request.Options(clientPropertiesEntry.getValue().getConnectTimeoutMillis(), clientPropertiesEntry.getValue().getReadTimeoutMillis()))
               .requestInterceptor(new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(), resource(clientPropertiesEntry.getValue())))
               .encoder(encoder)
               .decoder(decoder)
               .contract(contract)
               .target(KdiagClient.class, clientPropertiesEntry.getValue().getUrl());
        }));
    }

    public KdiagClient clientByRegion(KdisRegion region) {
        if (this.clients.containsKey(region.name())) {
            return this.clients.get(region.name());
        } else {
            throw new NoKdiagClientForRegionException(region);
        }
    }

}

In the end you can use it by injecting KdiagClients:

this.kdiagClients.clientByRegion(KdisRegion.EU)

This solution is a bit specific to a domain but one can adapt it to suit its need. So in the end the solution was:

  • Create a custom Configuration properties
  • Create a facing class which manually instantiate FeignClients reading the configuration properties

Upvotes: 2

Related Questions