2 Ocak 2020 Perşembe

SpringWebFlux WebClient Arayüzü

Giriş
Bu sınıf spring webflux projesine ait. Açıklaması şöyle.
Behind the scenes, WebClient calls an HTTP client. Reactor Netty is the default and reactive HttpClient of Jetty is also supported. Moreover, it's possible to plug other implementations of HTTP client by setting up a ClientConnector for WebClient.
Singleton
Açıklaması şöyle
Unlike RestTemplate, which was often instantiated per request or service, WebClient is designed to be used as a singleton. This means you should create a single instance of WebClient and reuse it across your application. This approach ensures efficient resource utilization and avoids the overhead of repeatedly creating and destroying WebClient instances.
Örnek
Şöyle yaparız
@Bean public WebClient.Builder webClientBuilder() { return WebClient.builder(); }
Kullanım
WebClient nesneyi yaratmak için iki tane yöntem var
1. WebClient.create() metodu ile bir WebClient nesnesi oluşturulur
2. WebClient.Builder.build() ile bir WebClient nesnesi oluşturulur

Daha sonra elimizdeki WebClient nesnesinin
delete()
get()
head()
options()
metodları kullanılarak bir WebClient.RequestHeadersUriSpec<?> nesnesi elde edilir.

veya
patch()
post()
put()
metodları kullanılarak WebClient.RequestBodyUriSpec nesnesi elde edilir.

WebClient Flux Dönüşümü yazısına bakabilirsiniz
WebClient Mono Dönüşümü yazısına bakabilirsiniz

Örnek
Şöyle yaparız
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;

public ResponseEntity<List<CustomerClientResponse>> getCustomerListWithTotalPagesHeader(Integer page, Integer size) {
  return WebClient.create()
    .get()
    .uri(builder -> builder.path(...)
                    .queryParam("page", page)
                    .queryParam("size", size).build())
    .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
    .retrieve()
    .toEntity(new ParameterizedTypeReference<List<CustomerClientResponse>>() {})
    .block();
}
create metodu
Netty kullanan bir WebClient nesnesi döndürür.

Örnek
Şöyle yaparız.
@GetMapping(path = "/streaming", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@ResponseBody
public Flux<Something> streamSomething() {
  return WebClient.create()
    .get().uri("http://example.org/resource")
    .retrieve().bodyToFlux(Something.class)
    .delaySubscription(Duration.ofSeconds(5))
    .repeat();
}
create metodu - String
Netty kullanan bir WebClient nesnesi döndürür.

Örnek
Şöyle yaparız
WebClient client = WebClient.create("https://example.com");
get metodu
WebClient.RequestHeadersUriSpec nesnesi döner.

post metodu
Şöyle yaparız.
WebClient webClient = WebClient.builder().baseUrl(baseUrl).build();

webClient.post().uri(uri)
  .contentType(MediaType.APPLICATION_JSON_UTF8)
  .accept(MediaType.APPLICATION_JSON_UTF8)
  .header(HttpHeaders.AUTHORIZATION, "Basic " + Base64Utils
  .encodeToString((plainCreds)
  .getBytes(Charset.defaultCharset())))
  .body(BodyInserters.fromObject(body)).retrieve()
  .bodyToFlux(EmployeeInfo.class)
  .doOnError(throwable -> {
    ...
  }).subscribe(new Consumer<EmployeeInfo>() {
    @Override
    public void accept(EmployeeInfo employeeInfo) {
      ...
    }
}); 
Örnek
Şöyle yaparız
@RestController
@RequestMapping("/api")
public class WebClientController {

  @Autowired
  private WebClient webClient;

  @GetMapping("/posts/{id}")
  public Mono <Post> getPost(@PathVariable String id) {
    return webClient.get()
      .uri("/question/{id}", id)
      .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
      .retrieve()
      .bodyToMono(Post.class);
  }

  @GetMapping("/posts")
  public Mono<String> getPosts() {
    return webClient.get()
      .uri("/question/")
      .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
      .retrieve()
      .bodyToMono(String.class);
  }

  @PostMapping("/posts")
  public Mono <String> createPost(final Post post) {
    return webClient.post()
      .uri("/question/")
      .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
      .body(Mono.just(post), Post.class)
      .retrieve()
      .bodyToMono(String.class);
    }
}
timeout metodu
Örnek
Şöyle yaparız
return webClient()
  .get()
  .uri("https://www.google.com")
  .exchangeToMono(
    response -> {
      if (response.statusCode().equals(HttpStatus.OK)) {
        return Mono.just("OK");
      }
      return Mono.just("OK");
    })
  .timeout(Duration.ofMillis(500)); //Timeout specified here

SpringValidation @Validated Anotasyonu - Spring'e Özel

Giriş
Açıklaması şöyle
@Valid
Origin: @Valid is part of the Java Bean Validation specification (JSR 380) which Spring supports. Hence, it's not Spring-specific and can be used in other Java contexts too.
...
@Validated
Origin: Unlike @Valid, @Validated is Spring-specific. It extends the functionality of @Valid and provides more flexibility.
@Validated Anotasyonu iki farklı amaç için kullanılabilir.

Maven
Şu satırı dahil ederiz
<dependency>
<groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>6.1.5.Final</version> </dependency>

1. Nesneye Değil Parametreye Uygulamak
Açıklaması şöyle. Bu kullanımda @Validated sınıfın üstüne eklenir. Eğer sınıfta JSR-380 kullanan ve metod parametresi olan nesneler varsa otomatikman onlara da uygulanır.
Validating request parameters and path variables is different because we are not validating a Java object like before. Path variables and parameters are primitive types so instead of annotating class fields, we add constraint annotations directly to the method parameter in the controller. In this case @Size annotation to the postcode path variable. We have to add Spring’s @Validated annotation to the controller at the class level, so that Spring can evaluate the constraint annotations on method parameters.
Örnek
Şöyle yaparız. Burada hem @PathVariable olarak işaretli parametreye, hem de User parametresine uygulanıyor
@RestController
@RequestMapping("/v1/users")
@RequiredArgsConstructor
@Validated
public class UserController {

  @GetMapping("/{postcode}")
  public ResponseEntity<List<User>> findAllByPostcode(@Size(min = 4, max = 6)
                                                      @PathVariable String postcode) {
   return ...
  }
}
Örnek
Şöyle yaparız
@Service
@Validated
public class MyBean {

  public Archive findByCodeAndAuthor(@Size(min = 8, max = 10) String code, Author author) {
    return ...
  }
}
2. Aynı Nesneye Farklı Validation Uygulamak İçin - Validation Group
Açıklaması şöyle
In addition to triggering validation just like @Valid, @Validated provides the ability for group-based validation. This is particularly helpful when you want to have different validation rules for the same object based on different contexts (like create vs update operations).
Açıklaması şöyle.
@Validated was added to support "validation groups", i.e. group of fields in the validated bean. This can be used in multi step forms where you may validate name, email, etc.
Örnek - Create ve Update İçin Farklı Doğrulama
Elimizde şöyle bir kod olsun. Burada kullanıcı yaratılırken id alanı boş olmalı, ancak güncellenirken boş olamaz.
public interface UpdateUser {}
public interface CreateUser {}

public class User {

  @Id()
  @Null(groups = CreateUser.class)
  @NotNull(groups = UpdateUser.class)
  private String id;

  @NotEmpty
  private String name;

  @NotNull
  @Valid
  private Address address;
}
Şöyle yaparız. Burada @Validated metod üzerinde, @Valid ise parametrede kullanılıyor
@RestController
@RequestMapping("/v1/users")
@RequiredArgsConstructor
@Validated
public class UserController {

  private final UserService userService;

  @PostMapping
  @Validated(CreateUser.class)
  public ResponseEntity<User> createUser(@Valid
                                         @RequestBody User user) {
    return ResponseEntity.ok(userService.createUser(user));
  }

  @PutMapping
  @Validated(UpdateUser.class)
  public ResponseEntity<User> updateUser(@Valid
                                         @RequestBody User user) {
    return ResponseEntity.ok(userService.updateUser(user));
  }
}
Örnek - Adımlar İçin Farklı Doğrulama
Elimizde şöyle bir kod olsun.
public class Account {

  @NotBlank(groups = {ValidationStepOne.class})
  private String username;

  @Email(groups = {ValidationStepOne.class})
  @NotBlank(groups = {ValidationStepOne.class})
  private String email;

  @NotBlank(groups = {ValidationStepTwo.class})
  @StrongPassword(groups = {ValidationStepTwo.class})
  private String password;

  @NotBlank(groups = {ValidationStepTwo.class})
  private String confirmedPassword;

}
Şöyle yaparız. Burada sadece @Validated parametrede kullanılıyor
@RequestMapping(value = "stepOne")
public String stepOne(@Validated(Account.ValidationStepOne.class) Account account) {...}

@RequestMapping(value = "stepTwo")
public String stepTwo(@Validated(Account.ValidationStepTwo.class) Account account) {...}