Quick notes of microservice in action

Quick notes of microservice in action

This is quick notes of learning Spring microservice in action(2nd edition). To be clarified, Spring-Boot and Spring-Cloud version are little different from book.

Here are versions used:

  • Spring-Boot version 2.6.13
  • Spring-Cloud version 2021.0.5

Whereas the guide book Spring Microservice In Action(2nd edition) uses 2.2.3.RELEASE for spring-boot and Hoxton.SR1 for spring-cloud. Which introduces some version 'traps'.

1) No spring.config.import property has been defined #

While configuring licensing-service as Spring Cloud Config Client, I encountered this error. After googled, I found that was a new version of spring with old configuration style.

You're getting this error because you're using a new version of Spring Boot and Spring Cloud, but you're trying to configure it in the old way.

Here is the full answer link: https://stackoverflow.com/questions/67507452/no-spring-config-import-property-has-been-defined

Spring Boot 2.4(later) introduced a new way to import config data.

Here is official doc: https://docs.spring.io/spring-cloud-config/docs/current/reference/html/#_spring_cloud_config_client

2 solutions were provided to solve problem.

2) Spring cloud config Git backend with ssh authentication #

Use command

ssh-keygen -m PEM -t rsa -b 4096 -f config_server_deploy_key.rsa

to gen rsa key pair.

Then config spring cloud config server like this:

 1  spring:
 2    cloud:
 3      config:
 4        server:
 5          git:
 6            uri: git@gitserver.com:team/repo1.git
 7            ignoreLocalSshSettings: true
 8#            hostKey: someHostKey
 9#            hostKeyAlgorithm: ssh-rsa
10            privateKey: |
11                         -----BEGIN RSA PRIVATE KEY-----
12                         MIIEpgIBAAKCAQEAx4UbaDzY5xjW6hc9jwN0mX33XpTDVW9WqHp5AKaRbtAC3DqX
13                         IXFMPgw3K45jxRb93f8tv9vL3rD9CUG1Gv4FM+o7ds7FRES5RTjv2RT/JVNJCoqF
14                         ol8+ngLqRZCyBtQN7zYByWMRirPGoDUqdPYrj2yq+ObBBNhg5N+hOwKjjpzdj2Ud
15                         1l7R+wxIqmJo1IYyy16xS8WsjyQuyC0lL456qkd5BDZ0Ag8j2X9H9D5220Ln7s9i
16                         oezTipXipS7p7Jekf3Ywx6abJwOmB0rX79dV4qiNcGgzATnG1PkXxqt76VhcGa0W
17                         DDVHEEYGbSQ6hIGSh0I7BQun0aLRZojfE3gqHQIDAQABAoIBAQCZmGrk8BK6tXCd
18                         fY6yTiKxFzwb38IQP0ojIUWNrq0+9Xt+NsypviLHkXfXXCKKU4zUHeIGVRq5MN9b
19                         BO56/RrcQHHOoJdUWuOV2qMqJvPUtC0CpGkD+valhfD75MxoXU7s3FK7yjxy3rsG
20                         EmfA6tHV8/4a5umo5TqSd2YTm5B19AhRqiuUVI1wTB41DjULUGiMYrnYrhzQlVvj
21                         5MjnKTlYu3V8PoYDfv1GmxPPh6vlpafXEeEYN8VB97e5x3DGHjZ5UrurAmTLTdO8
22                         +AahyoKsIY612TkkQthJlt7FJAwnCGMgY6podzzvzICLFmmTXYiZ/28I4BX/mOSe
23                         pZVnfRixAoGBAO6Uiwt40/PKs53mCEWngslSCsh9oGAaLTf/XdvMns5VmuyyAyKG
24                         ti8Ol5wqBMi4GIUzjbgUvSUt+IowIrG3f5tN85wpjQ1UGVcpTnl5Qo9xaS1PFScQ
25                         xrtWZ9eNj2TsIAMp/svJsyGG3OibxfnuAIpSXNQiJPwRlW3irzpGgVx/AoGBANYW
26                         dnhshUcEHMJi3aXwR12OTDnaLoanVGLwLnkqLSYUZA7ZegpKq90UAuBdcEfgdpyi
27                         PhKpeaeIiAaNnFo8m9aoTKr+7I6/uMTlwrVnfrsVTZv3orxjwQV20YIBCVRKD1uX
28                         VhE0ozPZxwwKSPAFocpyWpGHGreGF1AIYBE9UBtjAoGBAI8bfPgJpyFyMiGBjO6z
29                         FwlJc/xlFqDusrcHL7abW5qq0L4v3R+FrJw3ZYufzLTVcKfdj6GelwJJO+8wBm+R
30                         gTKYJItEhT48duLIfTDyIpHGVm9+I1MGhh5zKuCqIhxIYr9jHloBB7kRm0rPvYY4
31                         VAykcNgyDvtAVODP+4m6JvhjAoGBALbtTqErKN47V0+JJpapLnF0KxGrqeGIjIRV
32                         cYA6V4WYGr7NeIfesecfOC356PyhgPfpcVyEztwlvwTKb3RzIT1TZN8fH4YBr6Ee
33                         KTbTjefRFhVUjQqnucAvfGi29f+9oE3Ei9f7wA+H35ocF6JvTYUsHNMIO/3gZ38N
34                         CPjyCMa9AoGBAMhsITNe3QcbsXAbdUR00dDsIFVROzyFJ2m40i4KCRM35bC/BIBs
35                         q0TY3we+ERB40U8Z2BvU61QuwaunJ2+uGadHo58VSVdggqAo0BSkH58innKKt96J
36                         69pcVH/4rmLbXdcmNYGm6iu+MlPQk4BUZknHSmVHIFdJ0EPupVaQ8RHT
37                         -----END RSA PRIVATE KEY-----

Note: hostKey and hostkeyAlgorithm are no necessary configuration items.

Do Not forget to add public key to your gitHub settings.

By the way, if you already have an ssh key-pair in your local machine, just use it!

Referring official guide to learn more about git backend: https://docs.spring.io/spring-cloud-config/docs/current/reference/html/#_spring_cloud_config_server

3) Config sensitive info as env variables in Docker Compose #

Use System Environment Variable.

1docker compose run -e KEY_OF_ENV=value web python app.py

4) Debug Micro Service locally with docker and IDEA #

Docker services:

  • config server
  • eureka server
  • database

IDEA services:

  • licensing service
  • organization service

IDEA services need some extra configurations(program arguments) to override config server's backend configuration.

Use --key=value to do that.

1--eureka.client.serviceUrl.defaultZone=http://localhost:8070/eureka
2--spring.datasource.url=jdbc:postgresql://localhost:5432/ostock_dev
3--spring.config.import=optional:configserver:http://localhost:8071

5) Spring-Cloud discovery client and loadbalancer #

After introducing eureka client to licensing service, we also introduced spring-cloud-starter-loadbalancer and spring-cloud-starter-openfeign, which provide load-balance and microservice remote invoking ability to licensing service.

There 3 ways to achieve remote call, introduced by the book(microservice in action).

  • DiscoveryClient (Without load balance)
  • RestTemplate
  • OpenFeign

To Use DiscoveryClient in Spring Cloud, just inject DiscoveryClient bean into services.

 1@Component
 2public class OrganizationDiscoveryClient {
 3    @Autowired
 4    private DiscoveryClient discoveryClient;
 5
 6    /**
 7     * No LoadBalanced
 8     */
 9    public Organization getOrganization(String organizationId) {
10        RestTemplate restTemplate = new RestTemplate();
11        List<ServiceInstance> instances = discoveryClient.getInstances("organization-service");
12
13        if (instances.isEmpty()) return null;
14        String serviceUri = String.format("%s/v1/organization/%s",instances.get(0).getUri().toString(), organizationId);
15
16        ResponseEntity< Organization > restExchange =
17            restTemplate.exchange(
18                serviceUri,
19                HttpMethod.GET,
20                null, Organization.class, organizationId);
21
22        return restExchange.getBody();
23    }
24}

DiscoveryClient actually get all service instances by service name you set in eureka client. The service name was set by spring.application,name property.

And you could find out that DiscoveryClient was used to get remote service's url, and service was invoked by RestTemplate.

As you can see, there are still some 'hard code' in there, and DiscoveryClient always get the first service's url, which means Spring Cloud load-balance did not work in this scenario.

5.2 RestTemplate with @LoadBalanced #

If you want your RestTemplate run as load-balanced client, you need to config it with @LoadBalanced.

1@LoadBalanced
2@Bean
3public RestTemplate getRestTemplate(){
4    return new RestTemplate();
5}

Then you can use RestTemplate like this:

 1@Component
 2public class OrganizationRestTemplateClient {
 3
 4    @Autowired
 5    RestTemplate restTemplate;
 6
 7    public Organization getOrganization(String organizationId){
 8        ResponseEntity<Organization> restExchange =
 9            restTemplate.exchange(
10                "http://organization-service/v1/organization/{organizationId}",
11                HttpMethod.GET,
12                null, Organization.class, organizationId);
13
14        return restExchange.getBody();
15    }
16}

If you have more than 1 instances of organization service, the RestTemplate will query each instances Round-Robbin.

5.3 OpenFeign #

Spring Cloud integrates Eureka, Spring Cloud CircuitBreaker, as well as Spring Cloud LoadBalancer to provide a load-balanced http client when using Feign.

By using OpenFeign integrated with Spring Cloud, it already supported a load-balanced http-client.

The way to use OpenFeign is simple:

1@FeignClient("organization-service")
2public interface OrganizationFeignClient {
3
4    @RequestMapping(
5        method= RequestMethod.GET,
6        value="/v1/organization/{organizationId}",
7        consumes="application/json")
8    Organization getOrganization(@PathVariable("organizationId") String organizationId);
9}

Do not forget to annotate Main-class with @EnableFeignClients.

5.4 Other approaches #

Except ways mentioned above, there are other approaches to achieve RPC in Spring of course.

Spring WebFlux is a alternative:

More information, read:

Using a ReactiveLoadBalanced WebClients #

Filter UserContextFunctionFilter uses to transmit correlationId and other HTTP Headers like JWT while micro service invoking.

 1@Autowired
 2ReactorLoadBalancerExchangeFilterFunction lbFunction;
 3/**
 4 * {@link WebClient} load balancer HTTP client
 5 */
 6@Bean
 7@LoadBalanced
 8public WebClient.Builder getWebclient() {
 9    return WebClient.builder().filters(f -> {
10       f.add(new UserContextFunctionFilter());
11       f.add(lbFunction);
12    });
13}

And the UserContextFunctionFilter looks like:

 1public class UserContextFunctionFilter implements ExchangeFilterFunction {
 2    @Override
 3    public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
 4        ClientRequest buildRequest = ClientRequest.from(request).headers(h -> {
 5            h.add(UserContext.CORRELATION_ID, UserContextHolder.getContext().getCorrelationId());
 6            h.add(UserContext.AUTH_TOKEN, UserContextHolder.getContext().getAuthToken());
 7        }).build();
 8        return next.exchange(buildRequest);
 9    }
10
11}

There is a warning info by spring BeanPostProcessorAutoConfiguration:

12024-12-17 08:29:57.580  INFO 1 --- [           main] trationDelegate$BeanPostProcessorChecker :Bean 'org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerBeanPostProcessorAutoConfiguration' of type [org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerBeanPostProcessorAutoConfiguration] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
22024-12-17 08:29:57.594  INFO 1 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerBeanPostProcessorAutoConfiguration$ReactorDeferringLoadBalancerFilterConfig' of type [org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerBeanPostProcessorAutoConfiguration$ReactorDeferringLoadBalancerFilterConfig] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
32024-12-17 08:29:57.605  INFO 1 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'reactorDeferringLoadBalancerExchangeFilterFunction' of type [org.springframework.cloud.client.loadbalancer.reactive.DeferringLoadBalancerExchangeFilterFunction] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)

This ...is not eligible for getting processed by all BeanPostProcessors info always causes by circle dependency.

This info Spring Cloud did not fix it officially yet.