29 Ekim 2021 Cuma

SpringTest Testcontainers MySQLContainer

Maven
Örnek
Şöyle yaparız
<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>mysql</artifactId>
  <scope>test</scope>
</dependency>
Gradle
Örnek 
Şöyle yaparız
testImplementation("org.testcontainers:mysql")
testImplementation("org.testcontainers:spock")
testImplementation("org.testcontainers:testcontainers")
application-test.yml Dosyası
Örnek
Şöyle yaparız
datasources:
  default:
    url: jdbc:tc:mysql:8:///db
    driverClassName: org.testcontainers.jdbc.ContainerDatabaseDriver
Ayrıca testleri hızlandırmak için şöyle yaparız
testcontainers:
  reuse:
    enable: true
Açıklaması şöyle
Finally, in order to speed the testing process up, we should try to set a level of isolation in our tests that allow us to reuse the same image, which will reduce our test time dramatically:
MySQLContainer Sınıfı
Örnek
Şöyle yaparız
public static MySQLContainer mySQLContainer = new MySQLContainer("mysql:5.7")
  .withUsername("username")
  .withPassword("password")
  .withDatabaseName("test_db");
Örnek
Şöyle yaparız. Burada normalde container sınıfının start() metodunu çağırmaya gerek yok.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
@Testcontainers
class OrderControllerTest {

    static final MySQLContainer MY_SQL_CONTAINER;

    @Autowired
    WebTestClient webTestClient;

    @Autowired
    private OrderEntityRepository  orderEntityRepository;

    static {
        MY_SQL_CONTAINER = new MySQLContainer("mysql:latest");
        MY_SQL_CONTAINER.start();
    }

    @DynamicPropertySource
    static void configureTestProperties(DynamicPropertyRegistry registry){
      registry.add("spring.datasource.url",() -> MY_SQL_CONTAINER.getJdbcUrl());
      registry.add("spring.datasource.username",() -> MY_SQL_CONTAINER.getUsername());
      registry.add("spring.datasource.password",() -> MY_SQL_CONTAINER.getPassword());
      registry.add("spring.jpa.hibernate.ddl-auto",() -> "create");
  }
  ...
}    




24 Ekim 2021 Pazar

SpringData ElasticSearch RestHighLevelClient Sınıfı - Kullanmayın

Giriş
Açıklaması şöyle
There are two ways to do operations on elastic search using spring boot.
1. By using ElasticsearchRestTemplate: This we should use when we want to create more complex queries.
2. By Using Repositories: This is very simple and it has all the methods defined and internally it creates elastic-based queries.
Yani eğer ElasticsearchRepository kullanıyorsak bu sınıfı direkt kullanmaya gerek yok. Açıklaması şöyle
The Java High-Level REST Client works on top of the Java Low-Level REST client. Its main goal is to expose API specific methods, that accept request objects as an argument and return response objects, so that request marshalling and response un-marshalling is handled by the client itself.

Each API can be called synchronously or asynchronously. The synchronous methods return a response object, while the asynchronous methods, whose names end with the async suffix, require a listener argument that is notified (on the thread pool managed by the low-level client) once a response or an error is received.

The Java High-Level REST Client depends on the Elasticsearch core project. It accepts the same request arguments as the TransportClient and returns the same response objects.
Aslında bu sınıfı direk kullanmaya da gerek yok. Eğer ElasticsearchRestTemplate kullanacaksak şu açıklamayı bilmek lazım. Açıklaması şöyle
ElasticSearchRestTemplate is built on the top of RestHighLevelClient. You can think of it as a Spring wrapper over RestHighLevelClient.
Maven
Şu satırı dahil ederiz
<dependency>
  <groupId>org.elasticsearch.client</groupId>
  <artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
constructor
Örnek
Şöyle yaparız
@Bean
public RestHighLevelClient client() { RestHighLevelClient client = new RestHighLevelClient(new HttpHost("server",9200,"http")) .setMaxRetryTimeoutMillis(12000); }
Örnek
Elimizde şöyle bir dosya olsun
spring.data.elasticsearch.cluster-nodes=${ELASTICSEARCH_HOST:localhost:9200}
Şöyle yaparız
@Configuration
public class ElasticSearchConfig {
  @Value("${spring.data.elasticsearch.cluster-nodes}")
  String elasticHost;
  
  @Bean
  public RestHighLevelClient client() {
    return new RestHighLevelClient(RestClient.builder(HttpHost.create(elasticHost)));
  }
}
bulk metodu
Örnek
Şöylee yaparız
@Override
public BulkResponse saveAll(List<Product> products) throws IOException {

  BulkRequest bulkRequest = Requests.bulkRequest();
  products.forEach(product -> {
    try {
      IndexRequest indexRequest = Requests.indexRequest(INDEX_NAME)
        .source(convertProductToMap(product));
      bulkRequest.add(indexRequest);
    } catch (JsonProcessingException e) {
      // log error
    }
  });

  RequestOptions options = RequestOptions.DEFAULT;
  return restHighLevelClient.bulk(bulkRequest, options);
}
delete metodu
Veri tabanındaki kaydı siler
Bir örnek burada

index metodu
Veri tabanında yeni bir kayıt oluşturur
Örnek
Şöyle yaparız
@Override
public IndexResponse save(Product product) throws IOException {

  IndexRequest indexRequest = Requests.indexRequest(INDEX_NAME)
            .id(product.getId().toString())
            .source(convertProductToMap(product));

  RequestOptions options = RequestOptions.DEFAULT;
  return restHighLevelClient.index(indexRequest, options);
}

@Autowired
ObjectMapper objectMapper;
private Map<String, Object> convertProductToMap(Product product)
throws JsonProcessingException {
    String json = objectMapper.writeValueAsString(product);
    return objectMapper.readValue(json, Map.class);
}
Açıklaması şöyle
Pay attention to convertProductToMap() function, it will convert our Product object to a Map<String, Object>.
...
I am using Gson to encode it to JSON, then decode it to a Map class.
update metodu
Veri tabanındaki kaydı günceller

Bir örnek burada

search metodu
Bir örnek burada









SpringBoot StateMachine EnumStateMachineConfigurerAdapter Sınıfı

Giriş
Şu satırı dahil ederiz
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
İki tane configure metodu var. Biri states diğeri transitions için

configure metodu
Örnek
Şöyle yaparız
@Configuration
@EnableStateMachine
public class OrderStateMachineConfig extends 
  EnumStateMachineConfigurerAdapter<OrderState,OrderEvent> {

  @Override
  public void configure(StateMachineStateConfigurer<OrderState,OrderEvent> states)
  throws Exception {
    states.withStates()
      .initial(OrderState.ORDERED)
      .states(EnumSet.allOf(OrderState.class));
  }

  @Override
  public void configure(StateMachineTransitionConfigurer<OrderState,OrderEvent> transitions)
  throws Exception {
    transitions
      .withExternal()
        .source(OrderState.ORDERED).event(OrderEvent.ON_SHIPMENT)
.target(OrderState.SHIPPED) .and() .withExternal() .source(OrderState.ORDERED).event(OrderEvent.ON_CANCEL) .target(OrderState.CANCELLED) .and() ... } }
Örnek
Şöyle yaparız
@Configuration
@EnableStateMachineFactory
class PaymentStateMachineConfiguration :
  EnumStateMachineConfigurerAdapter<PaymentStates, PaymentEvents>() {

  override fun configure(states:
    StateMachineStateConfigurer<PaymentStates, PaymentEvents>) {
    states.withStates()
      .initial(AWAITING)
      .state(TRANSACTION_CREATED)
      .state(TRANSACTION_SENT)
      .state(TRANSACTION_FAILED)
      .end(SUCCEEDED)
  }

  override fun configure(transitions:
    StateMachineTransitionConfigurer<PaymentStates,PaymentEvents>) {
    transitions
      .withExternal()
      .source(AWAITING).target(TRANSACTION_CREATED).event(CREATE_TRANSACTION)
      .and()

      .withExternal()
      .source(TRANSACTION_CREATED).target(TRANSACTION_SENT).event(SEND_TRANSACTION)
      .and()
      ...
  }
}

SpringBoot StateMachine StateMachineListener Arayüzü

Giriş
Şu satırı dahil ederiz
import org.springframework.statemachine.listener.StateMachineListener;
İskeleti şöyle
public class OrderStateMachineListener extends StateMachineListener<OrderState, OrderEvent>{

 void stateChanged(State<OrderState, OrderEvent> var1, State<OrderState, OrderEvent> var2){}
void stateEntered(State<OrderState, OrderEvent> var1){}
void stateExited(State<OrderState, OrderEvent> var1){}
void eventNotAccepted(Message<OrderEvent> var1){}
void transition(Transition<OrderState, OrderEvent> var1){} void transitionStarted(Transition<OrderState, OrderEvent> var1){}
void transitionEnded(Transition<OrderState, OrderEvent> var1){}
void stateMachineStarted(StateMachine<OrderState, OrderEvent> var1){}
void stateMachineStopped(StateMachine<OrderState, OrderEvent> var1){}
void stateMachineError(State<OrderState, OrderEvent> var1, Exception var2){}
void extendedStateChanged(Object var1, Object var2){} void stateContext(StateContext<OrderState, OrderEvent> var1){}
}

SpringBoot StateMachine Guard Arayüzü

Giriş
Şu satırı dahil ederiz
import org.springframework.statemachine.Guard;
Kullanım
Örnek
Şöyle yaparız
@Configuration
@EnableStateMachine
public class OrderStateMachineConfig extends 
  EnumStateMachineConfigurerAdapter<OrderState, OrderEvent> {

  @Override
  public void configure(
    StateMachineTransitionConfigurer<OrderState, OrderEvent> transitions)
  throws Exception {
    transitions
      .withExternal()
        .source(OrderState.PICKUP).event(OrderEvent.INITIATE_REFUND)
        .target(OrderState.REFUND_INITIATED)
        .guard(new RefundInitiateGuard())
      .and()
      .withExternal()
      ...
  }
}
evaluate metodu
Örnek
Şöyle yaparız
public class RefundInitiateGuard implements Guard<OrderState,OrderEvent> {

  @Override
  public boolean evaluate(StateContext<OrderState, OrderEvent> stateContext) {
    ...
    return true;
  }
}

SpringBoot StateMachine StateMachine Arayüzü

Giriş
Şu satırı dahil ederiz
import org.springframework.statemachine.StateMachine;
sendEvent metodu
Örnek
Şöyle yaparız
public class StateMachineExample {

  StateMachine<OrderState,OrderEvent> stateMachine;
    
  public void onShipment(OrderDetails order){
    stateMachine.sendEvent(OrderEvent.ON_SHIPMENT);
  }
}


21 Ekim 2021 Perşembe

SpringTest Testcontainers Veri Tabanı Hazırlama

Örnek
Açıklaması şöyle
This custom specialization of the MySQLContainer builds a container based on mysql:latest, with a new schema HR, a user ‘docker’ and runs a setup script from src/test/resources/sql upon container startup.
Şöyle yaparız
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.MySQLContainer;

public class TestDBContainer extends MySQLContainer {

  public OurSQLContainer() {
    super("mysql:latest");
    withDatabaseName("HR")
      .withUsername("docker")
      .withPassword("docker")
      .withClasspathResourceMapping("sql/setup.sql", "/docker-entrypoint-initdb.d/", BindMode.READ_ONLY);
    }
}
Ama veri tabanını doldurmak uzun sürüyorsak kendi imajımızı hazırlarız. Şöyle yaparız
FROM mysql:latest

ENV MYSQL_ROOT_PASSWORD=root
ENV MYSQL_DATABASE=hr
ENV MYSQL_USER=docker
ENV MYSQL_PASSWORD=docker

COPY ./setup.sql /docker-entrypoint-initdb.d/

# necessary to persist the new database state when we commit the container
RUN cp -r /var/lib/mysql /var/lib/mysql-no-volume

CMD ["--datadir", "/var/lib/mysql-no-volume"]
Daha sonra bunu kullanırız. Şöyle yaparız
super(DockerImageName.parse("testdb:latest").asCompatibleSubstituteFor("mysql"));


20 Ekim 2021 Çarşamba

TransactionSynchronizationManager.registerSynchronization metodu - Post Commit İçin Kullanılır

Giriş
Şu satırı dahil ederiz
import org.springframework.transaction.support.TransactionSynchronizationManager;
Transaction bitince çalıştırılacak kodu belirtir. Bu iş zor olduğu için kendimiz bir anotasyon oluşturup yapabiliriz. Bir örnek burada.

Örnek
Şöyle yaparız.
TransactionSynchronizationManager.registerSynchronization(
  new TransactionSynchronization(){
    void afterCommit(){
      //do what you want to do after commit
    }
});
Örnek
Şöyle yaparız
TransactionSynchronizationManager.registerSynchronization(
  new TransactionSynchronization(){
    void afterCommit(){
      // do what you want to do after commit
      // in this case call the notifyUI method
    }
});

18 Ekim 2021 Pazartesi

SpringContext CommonAnnotationBeanPostProcessor Sınıfı

Giriş
Şu satırı dahil ederizz
import org.springframework.context.annotation.CommonAnnotationBeanPostProcessor;
Bu sınıf sayesinde @EJB anotasyonunu Spring bean içinde bile kullanabiliriz. Bu sınıf @EJB anotasyonunu görünce, belirtilen nesnesi JNDI'dan bulup getirir.

17 Ekim 2021 Pazar

spring-boot-gradle-plugin Fat Jar

bootRun Goal
Örnek
Spring projesini çalıştırmak için şöyle yaparız
./gradlew bootRun
bootBuildImage Goal
Açıklaması şöyle. Yani Dokcer image içinde BellSoft Liberica JDK kullanır
You may build a Docker image by defining a Dockerfile where the various layers of the Docker image need to be defined. The downside of this approach is that creating a Dockerfile needs a good understanding of the Docker technology. To empower the containerization, Cloud Native Buildpacks by Pivotal transform your source code into an OCI (Open Container Initiative) compatible Docker image without the need of a Dockerfile. The container image can then be deployed in any modern cloud.

Paketo.io is a Cloud Foundry project and one of the most popular implementations of Cloud Native Buildpacks. It can transform the source code of major programming languages into a container image.

Spring Boot natively supports the buildpacks that create an image with BellSoft Liberica JDK. It also looks at the build.gradle file and the Spring configuration file to build the Docker image.
Örnek
Şöyle yaparız
> sudo ./gradlew bootBuildImage
Çıktısı şöyle
> Task :bootBuildImage 
Building image 'docker.io/library/microservice-customer:1.0.0'   
> Pulling builder image 'docker.io/paketobuildpacks/builder:base' ..................................................  
> Pulled builder image 'paketobuildpacks/builder@sha256:35e29183d1aec1b4d79ebec4fb47ef309dc4e803e2706b5a7336d8ebe68053e8'  
> Pulling run image 'docker.io/paketobuildpacks/run:base-cnb' ..................................................  
> Pulled run image 'paketobuildpacks/run@sha256:d968d1e9827704283bdfd678d9cb2b85d6e0bd826b0cb1f14bbceb5bb6e0f571'  
> Executing lifecycle version v0.10.2  
> Using build cache volume 'pack-cache-9d89fc6213b6.build'   
> Running creator
.
.
.
Açıklaması şöyle
If you look at the output, it is evident that in the first step, it runs the Paketo BellSoft Liberica Buildpack 7.0.0, which in turn downloads the BellSoft Liberica JDK and JRE implementations for the JVM (version 11, as defined in the build.gradle file) from GitHub.

Thus it provides BellSoft Liberica JRE 11.0.10 as the JVM runtime layer of the Docker image.

Next the command runs Paketo Spring Boot Buildpack 4.0.0. It creates the layers for the application, dependencies, and the spring-boot-loader module.

Finally, it creates a Docker image. For our demo, the Customer microservice container image looks as such: docker.io/library/microservice-customer:1.0.0

Then we create one for the Order microservice going through the similar step: docker.io/library/microservice-order:1.0.0

Örnek
Şöyle yaparız
./gradlew bootBuildImage --imageName=splitdemo/spring-boot-docker

12 Ekim 2021 Salı

SpringSecurity OAuth2 Resource Server Kullanımı

Giriş
1. HttpSecurity sınıfının oauth2ResourceServer() metodu ile ayarlar yapılır. Bu metodu bir OAuth2ResourceServerConfigurer döndürür.

2. application.properties dosyasında ayarları yapılır

3. WebSecurityConfigurerAdapter sınıfına @EnableWebSecurity ve  eğer metod level security istiyorsak @EnableGlobalMethodSecurity(jsr250Enabled = true) anotasyonları eklenir.

SpringBoot Actuator - Metrics Endpoint - Micrometer Metriclerini Gösterir

Giriş
Açıklaması şöyle. Micrometer kütüphanesi aracılığıyla metric üretir.
Spring Boot Actuator provides dependency management and auto-configuration for Micrometer, an application metrics facade that supports numerous monitoring systems.
Açıklaması şöyle. Yani aslında Micrometer bir nevi SLF4J gibi düşünülebilir.
Both Quarkus and Spring use the Micrometer metrics library for collecting metrics. Micrometer provides a vendor-neutral interface for registering dimensional metrics and metric types, such as counters, gauges, timers, and distribution summaries. These core types are an abstraction layer that can be adapted and delegated to specific implementations, such as Prometheus.

The Quarkus Micrometer extension automatically times all HTTP server requests. Other Quarkus extensions also automatically add their own metrics collections. Applications can add their own custom metrics as well. Quarkus will automatically export the metrics on the /q/metrics endpoint. In Spring, Micrometer is supported by the Spring Boot Actuator starter if the Micrometer libraries are included on the application's classpath.
Çıktı Çeşitler
Şöyledir. Yani çok fazla çeşit ve formatta çıktı verebiliyor
Atlas
AWS CloudWatch
Datadog
Dynatrace
Elastic
Graphite
Influx
Instana
JMX
New Relic
OpenTelemetry Protocol
Prometheus
StatsD
DataDog Formatı
Metrics endpoint istenirse DataDog formatında çıktı da verebilir. DataDog yazısına taşıdım

Prometheus Formatı
Metrics endpoint istenirse Prometheus formatında çıktısı da verebilir.  Prometheus Endpoint yazısına taşıdım

Etkinleştirmek
Etkinleştirmek için şöyle yaparız
management.endpoint.metrics.enabled = true
management.endpoints.web.exposure.include = metrics
tags alanı
Prometheus için tag belirtir. Açıklaması şöyle
management.metrics.tags.application - we may monitor multiple applications on our Grafana dashboard, so we need to distinguish one application from another

Örnek
Şöyle yaparız
spring:
  application:
    name: [SERVICE_NAME]
...

management:
  endpoint:
    health:
      show-details: always
  endpoints:
    web:
      exposure:
        include: "*"
  metrics:
    tags:
      application: ${spring.application.name}
Örnek
Şöyle yaparız.
http://localhost:8080/actuator/metrics
Çıktı olarak şunu alırız. Burada toplanan tüm metriklerin isimleri var
"names": [
    "jvm.threads.states",
    "process.files.max",
    "jvm.memory.used",
    "jvm.gc.memory.promoted",
    "jvm.memory.max",
    "system.load.average.1m",
    ...
  ]
}
Eğer kullanılan diğer kütüphaneler de Micrometer metric üretiyorsa onları da görürüz. 

JVM Çıktısı
JVM İçin Metrics Endpoint yazısına taşıdım

Örnek - Hikari
Hikari ve Tomcat metric üretir. Şöyle yaparız
curl -s http://localhost:8080/actuator/metrics | jq
{
  "names": [
    "hikaricp.connections",
    "hikaricp.connections.acquire",
    "hikaricp.connections.active",
    "hikaricp.connections.creation",
    "hikaricp.connections.idle",
    "hikaricp.connections.max",
    "hikaricp.connections.min",
    "hikaricp.connections.pending",
    "hikaricp.connections.timeout",
    "hikaricp.connections.usage",
    "http.server.requests",
    "jdbc.connections.active",
    "jdbc.connections.idle",
    "jdbc.connections.max",
    "jdbc.connections.min",
    "spring.data.repository.invocations",
    "tomcat.sessions.active.current",
    "tomcat.sessions.active.max",
    "tomcat.sessions.alive.max",
    "tomcat.sessions.created",
    "tomcat.sessions.expired",
    "tomcat.sessions.rejected"
  ]
}

Örnek - Tek Metric Değerini Görmek
Şöyle yaparız
curl -s http://localhost:8080/actuator/metrics/hikaricp.connections.active | jq
{
  "name": "hikaricp.connections.active",
  "description": "Active connections",
  "baseUnit": null,
  "measurements": [
    {
      "statistic": "VALUE",
      "value": 4
    }
  ],
  "availableTags": [
    {
      "tag": "pool",
      "values": [
        "HikariPool-1"
      ]
    }
  ]
}
Örnek - Tek Metric Değerini Görmek
Şöyle yaparız
http://localhost:8080/actuator/metrics/cache.size
Örnek - Tek Metric Değerini Görmek
Detayları görmek için şöyle yaparız.
http://localhost:8080/actuator/metrics/system.cpu.count
Örnek  - Tek Metric Değerini Görmek
Detayları görmek için şöyle yaparız.
/actuator/metrics/http.server.requests

11 Ekim 2021 Pazartesi

SpringCloud LoadBalancer ServiceInstanceListSupplier Arayüzü

Giriş
Şu satırı dahil ederiz
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
get metodu - Request
Açıklaması şöyle
IgniteServiceInstanceListSuppler will return instances based on the request key.
Şöyle yaparız
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.RequestDataContext;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;

public class IgniteServiceInstanceListSuppler implements ServiceInstanceListSupplier {
  private final IgniteEx ignite = ...;
  private volatile List<ServiceInstance> instances = ...;
  @Override
  public String getServiceId() {
    return ignite.localNode().consistentId().toString();
  }
  @Override
  public Flux<List<ServiceInstance>> get() {
    return Flux.just(instances);
  }
  @Override
  public Flux<List<ServiceInstance>> get(Request req) {
    if (req.getContext() instanceof RequestDataContext) {
      HttpHeaders headers = ((RequestDataContext)req.getContext()).
        .getClientRequest().getHeaders();
      String cacheName = headers.getFirst("affinity-cache-name");
      String affinityKey = headers.getFirst("affinity-key");

      Affinity<Object> affinity = affinities.computeIfAbsent(
        cacheName, k -> ((GatewayProtectedCacheProxy)ignite.cache(cacheName)).
          context().cache().affinity()
      );

      ClusterNode node = affinity.mapKeyToNode(affinityKey);
      if (node != null)
        return Flux.just(singletonList(toServiceInstance(node)));
    }
    return get();
  }
}


SpringCloud LoadBalancer - Client Side Load Balancing İçindir

Giriş
Açıklaması şöyle
Spring Cloud provides several ways to implement load balancing, including Ribbon, Spring Cloud Load Balancer, Spring Cloud Gateway, and Kubernetes Load Balancer.
Bu proje, WebClient ile client side load balancing yapmak içindir.

Maven
Şu satırı dahil ederiz
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
LoadBalancerClientFactory Sınıfı
Örnek
Şöyle yaparız
@Bean
public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(
  Environment environment,
  LoadBalancerClientFactory loadBalancerClientFactory) {

  String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
  return new RoundRobinLoadBalancer(
    loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), 
    name);
}
@LoadBalancedAnotasyonu
@LoadBalancedAnotasyonu yazısına taşıdım

@LoadBalancerClient Anotasyonu
configuration Alanı
Load Balancing için iki gerçekleştirim var

1. RoundRobinLoadBalancer 
2. RandomLoadBalancer 

Örnek - RoundRobinLoadBalancer 
Şöyle yaparız
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
@LoadBalancerClient(name = WebClientConfig.CLIENT_NAME, 
                    configuration = IgniteLoadBalancerConfiguration.class)
public class WebClientConfig {
    public static final String CLIENT_NAME = "client";

    @Bean
    @LoadBalanced
    public WebClient.Builder usersClientBuilder() {
        return WebClient.builder()
            .defaultHeader("affinity-cache-name", "UserCache");
    }
}
Yardımcı kod şöyledir
@Configuration
public class IgniteLoadBalancerConfiguration {
  public static final String SERVICE_ID = "example";

  @Bean
  @Primary
  public ServiceInstanceListSupplier serviceInstanceListSupplier(IgniteEx ignite) {
    return new IgniteServiceInstanceListSuppler(ignite);
  }

  @Bean
  public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(
    Environment environment,
    LoadBalancerClientFactory loadBalancerClientFactory) {
    String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
    return new RoundRobinLoadBalancer(
      loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
        name);
  }
}
Açıklaması şöyle
IgniteServiceInstanceListSuppler will return instances based on the request key.
Örnek
Elimizde şöyle bir kod olsun. locahhost üzerinde çalışan 4 tane port döndürür
@Bean
public ReactiveDiscoveryClient customDiscoveryClient() {
  return new ReactiveDiscoveryClient() {
    @Override
    public String description() {
      return "Calling another API example";
    }

    @Override
    public Flux<ServiceInstance> getInstances(String serviceId) {
      log.debug("getInstances: {}", serviceId);

      return Flux.just(8080, 8081, 8082)
        .map(port -> new DefaultServiceInstance(serviceId + "-" + port, serviceId,
          "localhost", port, false));
      }

    @Override
    public Flux<String> getServices() {
      return Flux.just("ExampleApi");
    }
  };
}
Açıklaması şöyle
Implement theReactiveDiscoveryClient . It allows us to return a list of instances that can be used when a specific call is triggered.
Şöyle yaparız
@Service
public class AccountsService {
  private static final String API = "ExampleApi";
  private final WebClient apiClient;

  public AccountsService(WebClient.Builder loadBalancedWebClientBuilder) {
    this.apiClient = loadBalancedWebClientBuilder
      .build();
  }

  public Mono<String> login(String username) {
    return apiClient.post()
      .uri(uriBuilder -> uriBuilder
                        .host(API)
                        .path("/login")
                        .build())
    .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
    .header("username", username)
    .exchangeToMono(r -> handleResponse(r));
  }
  private Mono<String> handleResponse(ClientResponse r) {
    if (r.statusCode().is2xxSuccessful()) {
      return r.bodyToMono(String.class);
    }

    return r.bodyToMono(String.class)
      .switchIfEmpty(Mono.error(new IllegalStateException("Failed: " + r.statusCode())))
      .flatMap(response -> Mono.error(new IllegalStateException("Failed: " +
        r.statusCode() + ", " + response)));
  }
}