27 Haziran 2023 Salı

SpringMVC Declarative REST Client - OpenFeign Yerine Kullanılır

Giriş
OpenFeign yerine kullanılır. Açıklaması şöyle
HTTP Interface Client introduced in Spring 6. The HTTP Interface Client enables to definition a declarative way for HTTP services using Java interfaces. Under the hood, Spring generates a proxy class that implements the interface and performs exchanges.
Bağımlılıklar
Açıklaması şöyle
All necessary components are in the spring-web module, that happens to be a transitive dependency for the spring-boot-starter-web or the spring-boot-starter-webflux modules. However, in practice the WebFlux dependency is always required at the moment due to the HttpServiceProxyFactory for generating the clients. 
Açıklaması şöyle
Why do you need Spring Reactive Web dependencies
When creating the project above, the dependency of Spring Reactive Web was introduced, and the WebClient type was used when creating the service object of the proxy. This is because HTTP Interface currently only has built-in implementation of WebClient, which belongs to the category of Reactive Web. Spring will launch a RestTemplate-based implementation in subsequent versions.
Anotasyonlar 
Anotasyonlar şöyle
@HttpExchange
@PutExchange
@DeleteExchange

baseUrl metodu
İsteğin gönderileceği URL adresini belirtir
Örnek
Şöyle yaparız
public interface CatFactsClient {
  @GetExchange(value = "/facts")
  List<Fact> getFacts();
}

@Configuration
public class CatFactsClientConfig {

  @Bean
  public CatFactsClient catFactsClient() {
    WebClient client = WebClient.builder()
      .baseUrl("https://cat-fact.herokuapp.com")
      .build();
    HttpServiceProxyFactory factory = HttpServiceProxyFactory
      .builder(WebClientAdapter.forClient(client))
      .build();
    return factory.createClient(CatFactsClient.class);
  }
}

@Service
public class MyService {

  @Autowired
  private CatFactsClient catFactsClient;

  public List<Fact> fetchCatFacts() {
    return catFactsClient.getFacts();
  }
}
clientConnector metodu
Örnek - WebClient Connection reset by peer error
Gönderilen istekler için keep-alive seçeneğini kapatmak gerekir. Şöyle yaparız
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.support.WebClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
import reactor.netty.http.client.HttpClient;


@Configuration
public class HttpProxyConfiguration {

  @Value("${tracker.url}")
  private String trackerUrl;

  @Bean
  TrackerClient trackerClient(WebClient.Builder builder) {
    var httpClient = HttpClient.newConnection().keepAlive(false); // Here
    var reactorClientHttpConnector = new ReactorClientHttpConnector(httpClient);

    var wc = builder.baseUrl(trackerUrl)
      .clientConnector(reactorClientHttpConnector)
      .build();

    var wca = WebClientAdapter.forClient(wc);
    return HttpServiceProxyFactory.builder()
      .clientAdapter(wca)
      .build()
      .createClient(TrackerClient.class);
  }
}

defaultStatusHandler metodu
HttpStatus.NOT_FOUND, HttpStatusCode::is5xxServerError gibi durumlarda ne yapılacağını belirtir

Örnek
Elimizde şöyle bir kod olsun
@HttpExchange(
        url = "/characters",
        accept = MediaType.APPLICATION_JSON_VALUE)
public interface CharacterClient {

  @GetExchange
  List<CharacterResponse> getByName(@RequestParam String lastName);

  @GetExchange("/{id}")
  Optional<CharacterResponse> getById(@PathVariable long id);

  @PutExchange(contentType = MediaType.APPLICATION_JSON_VALUE)
  CharacterResponse addCharacter(@RequestBody AddCharacterRequest request);

  @DeleteExchange("/{id}")
  void deleteById(@PathVariable long id);
}
Bu arayüzden Spring kod üretir. Daha sonra bu kodu WebClient ile birleştirmek gerekir. Şöyle yaparız
@Configuration
public class CharacterClientConfig {

  @Bean
  public CharacterClient characterClient(CharacterClientProperties properties) {
    WebClient webClient = WebClient.builder()
      .baseUrl(properties.getUrl())
      .defaultStatusHandler(
        httpStatusCode -> HttpStatus.NOT_FOUND == httpStatusCode,
        response -> Mono.empty())
      .defaultStatusHandler(
        HttpStatusCode::is5xxServerError,
        response -> Mono
          .error(new ExternalCommunicationException(response.statusCode().value())))
      .build();

    return HttpServiceProxyFactory
      .builder(WebClientAdapter.forClient(webClient))
      .build()
      .createClient(CharacterClient.class);
  }
}

@Data
@Component
@ConfigurationProperties("character-client")
public class CharacterClientProperties {
    private String url;
}
Açıklaması şöyle. Yani şimdilik HttpServiceProxyFactory + WebClientAdapter + WebClient kodunu elle yazmak gerekiyor.
Currently, unlike OpenFeign, the client is not yet supplied via auto-configuration in a Spring Boot setup (kindly track Support declarative HTTP clients #31337 for that matter). Therefore, we build a WebClient ourselves and create a declarative HTTP client from it by using the createClient method from HttpServiceProxyFactory. This is some kind of boilerplate code, but I am quite confident that the Spring Boot guys will come up with a nice solution to simplify this further. Up until this point, you will need such a bean definition for each declarative HTTP client in your application.
Kullanmak için şöyle yaparız
CharacterResponse brandon = characterClient
    .addCharacter(new AddCharacterRequest("Brandon", "Stark"));

List<CharacterResponse> starks = characterClient.getByName("Stark");

Optional<CharacterResponse> eddardStark = characterClient.getById(1);

Optional<CharacterResponse> unknown = characterClient.getById(1337L); // empty

characterClient.deleteById(brandon.id());
Eğer Sadece Web Client Kullansaydık şöyle yaparız. Bu da karışık kodlar demek
public List<CharacterResponse> getByName(String lastName) {
  return webClient
    .get()
    .uri(uriBuilder -> uriBuilder
                    .path("/characters")
                    .queryParam("lastName", "{lastName}")
                    .build(lastName))
    .retrieve()
    .toEntityList(CharacterResponse.class)
    .block()
    .getBody();
}

22 Haziran 2023 Perşembe

SpringBoot Actuator CacheMetricsRegistrar Sınıfı

Giriş
Şu satırı dahil ederiz
import org.springframework.boot.actuate.metrics.cache.CacheMetricsRegistrar;
bindCacheToRegistry metodu
Örnek
Şöyle yaparız
@Configuration
class EnableCacheMetrics(
    val cacheMetricsRegistrar: CacheMetricsRegistrar,
    val cacheManager: CacheManager
) {
    @EventListener(ApplicationStartedEvent::class)
    fun addCachesMetrics() {
        cacheMetricsRegistrar.bindCacheToRegistry(cacheManager.getCache("customers"))
    }
}


SpringCache Redis application.properties Ayarları

Giriş
Bazı ayarlar application.properties dosyasından yapılamıyorsa şu sınıflar kullanılabilir.
RedisCacheManager ile Redis'e bağlanırken kullanılacak serializer ve diğer ayarlar belirtilir. - Kullanmayın
RedisCacheConfiguration - Kullanmayın

Cache Tipi ve Redis Bağlantısı
Cache tipi olarak Redis ve Redis bağlantısı için ayarlar belirtilir. 
- Redis bağlantısı için ayarlar spring.redis.XX alanlarında belirtilir.
- Cache için ayarlar spring.cache.redis.XXX alanlarında belirtilir.


Örnek
Şöyle yaparız
# Redis Config
spring.cache.type=redis
spring.redis.host=localhost
spring.redis.port=6379
enable-statistics Alanı
Örnek
Şöyle yaparız
spring.cache.redis.enable-statistics = true
Açıklaması şöyle
But to my disappointment, my cache does not appear in the Spring metrics, even though I set the spring.cache.redis.enable-statistics parameter to true as suggested in the instructions.

It seems caches that are created dynamically do not have published metrics. “Created dynamically” means the cache is created by the @Cacheable annotation, and not during application initialization by explicitly defining it at the CacheManager.
time-to-live Alanı
Örnek
Şöyle yaparız
spring.redis.host=xxx.xxx.xxx.xxx
spring.redis.port=xx
spring.redis.password=xxxxxxxx

spring.cache.redis.time-to-live=10000
Örnek
Şöyle yaparız
cache:
    type: redis
    redis:
      time-to-live: 30
      cache-null-values: false
cache-null-values Alanı
Örnek
Şöyle yaparız. Böylece Redis null value kabul etmez
spring.redis.host=127.0.0.1 
spring.redis.port=6379 
spring.cache.redis.cache-null-values=false
spring.cache.redis.time-to-live=600000
sentinel Alanı
Örnek
Şöyle yaparız
server: 
  port: 4024
  servlet:
    context-path: /app

spring: 
  application:
    name: todolist-redis
  cache:
    type: redis
  redis:
    sentinel:
      master: redis-cluster
      nodes:
        - 172.18.0.5:6379
        - 172.18.0.6:6379
        - 172.18.0.7:6379
cluster Alanı
Örnek
Şöyle yaparız
server: 
  port: 4024
  servlet:
    context-path: /app

spring: 
  application:
    name: todolist-redis
  cache:
    type: redis
  redis:
     cluster:
       
       nodes:
        - 172.18.0.3:6379
        - 172.18.0.2:6379
        - 172.18.0.4:6379
       maxRedirects: 2





20 Haziran 2023 Salı

API Versioning

Giriş
Yöntemler şöyle
URL'yi Değiştiren Yöntemler
1. URL Path Versioning
2.  Request Parameter Versioning

URL'yi Değiştirmeyen Yöntemler
1. Media Type veya Content Negotiation (Accept Header) Versioning

1. URI/URL Path Versioning
Açıklaması şöyle
URL path versioning involves including the version number directly in the URL path of the API endpoints. This is a common approach and makes the version explicit in the URL.
Örnek
Şöyle yaparız
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {

  @GetMapping
  public ResponseEntity<List<User>> getUsers() {
    // Implementation here
  }
  // Other endpoints
}
2. Request Parameter Versioning
Açıklaması şöyle
In this approach, the version number is included as a query parameter in the URL.
Örnek
Şöyle yaparız
@RestController
public class UserController {

  @GetMapping(value = "/api/users", params = "version=1")
  public ResponseEntity<List<User>> getUsersV1() {
    // Implementation here
  }

  @GetMapping(value = "/api/users", params = "version=2")
  public ResponseEntity<List<User>> getUsersV2() {
     // Implementation here
  }
}
URL'yi Değiştirmeyen Yöntemler
1.Custom Header Versioning
2. Content Negotiation (Accept Header) Versioning

1.Custom Header Versioning
Örnek
Şöyle yaparız
@RestController
public class UserController {

  @GetMapping(value = "/api/users", headers = "X-API-Version=1")
  public ResponseEntity<List<User>> getUsersV1() {
    // Implementation here
  }

  @GetMapping(value = "/api/users", headers = "X-API-Version=2")
  public ResponseEntity<List<User>> getUsersV2() {
    // Implementation here
  }
}
2. Content Negotiation veya Media Type (Accept Header) Versioning
Açıklaması şöyle
In this approach, the version is specified in the Accept header of the HTTP request.
Örnek
Şöyle yaparız
@RestController
public class UserController {

  @GetMapping(value = "/api/users", produces = "application/vnd.example.api.v1+json")
  public ResponseEntity<List<User>> getUsersV1() {
    // Implementation here
  }

  @GetMapping(value = "/api/users", produces = "application/vnd.example.api.v2+json")
  public ResponseEntity<List<User>> getUsersV2() {
      // Implementation here
  }
}
Örnek
Açıklaması şöyle
Similar to media type versioning, content negotiation uses the Accept header to determine the version.
Bir başka yöntem şöyle
@Configuration
public class WebConfig implements WebMvcConfigurer {

  @Override
  public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    configurer.mediaType("v1", MediaType.valueOf("application/vnd.company.app-v1+json"));
    configurer.mediaType("v2", MediaType.valueOf("application/vnd.company.app-v2+json"));
  }
}

@RestController
@RequestMapping("/users")
public class UserController {
    
  @GetMapping(produces = "v1")
  public ResponseEntity<String> getUserV1() {
    // Endpoint implementation for version 1
  }
    
  @GetMapping(produces = "v2")
  public ResponseEntity<String> getUserV2() {
    // Endpoint implementation for version 2
  }
}


16 Haziran 2023 Cuma

SpringKafka Consumer JsonSerde Sınıfı

Giriş
Şu satırı dahil ederiz
import  org.springframework.kafka.support.serializer.JsonSerde;
JSON nesnesinden, Java nesnesine çevirir.
Örnek
Şöyle yaparız
public static JsonSerde<StockPrice> stockPrice() {
  JsonSerde<StockPrice> serde = new JsonSerde<>(StockPrice.class);
  serde.configure(Map.of(JsonDeserializer.TRUSTED_PACKAGES, "*"), false);
  return serde;
}
Açıklaması şöyle
For security reasons, configuration is required to specify the trusted packages of the data classes for deserialization.

Here we have the wildcard (*) to trust all for demo purposes. Production code should better have the packages defined accordingly.

Besides, the boolean flag is to let the JsonSerDes knows whether the StockPrice is a message key or not (false means it is a message value in our case).



SpringRetry RetryListenerSupport Sınıfı

Giriş
Şu satırı dahil ederiz
import org.springframework.retry.listener.RetryListenerSupport;
Kullanım
Örnek
Şöyle yaparız
@Bean public RetryTemplate installTemplate() { RetryTemplate retryTemplate = new RetryTemplate(); retryTemplate.registerListener(new MyRetryListener()); return retryTemplate; }
Örnek
Şöyle yaparız
public class MyRetryListener extends RetryListenerSupport {

  @Override
  public <T, E extends Throwable> void onError(RetryContext context, 
                                               RetryCallback<T, E> callback, 
                                               Throwable throwable) {
    Metrics.addMetric("mysql_connection_error",1);
    super.onError(context,callback,throwable);
  }
  ...
}

15 Haziran 2023 Perşembe

SpringKafka Consumer DefaultErrorHandler Sınıfı

Giriş
DefaultErrorHandler kullanırken acknowledgement mode ne olmalı emin değilim. Sanırım auto kullanılsa da olur. Buradaki örnekte acknowledgement mode manual yapılıyor gerçi. Açıklaması şöyle
The default behavior is attempting to consume one message at most 10 times, then consume the next message and print an error log if it still fails.
Çıktısı şöyle
2023-06-03T08:57:16.573Z ERROR [order-query-side,,] 1 --- 
[org.springframework.kafka.KafkaListenerEndpointContainer#0-0-C-1] 
o.s.kafka.listener.DefaultErrorHandler   : Backoff FixedBackOff
{interval=0, currentAttempts=10, maxAttempts=9} exhausted for ORDER-0@0
Açıklaması şöyle
Blocking Retry
Before we send the fail-processed message to the retry topic, we might want to retry a couple of times to save some network round trip. There are plenty of ways to change the default behavior likes:

1. provide your own @Bean of KafkaListenerErrorHandler
2. provide your own @Bean of DefaultErrorHandler
with different ConsumerRecordRecoverer (instead of just printing error logs) and different BackOff settings to customize attempts and retry intervals.


addNotRetryableExceptions metodu
Örnek
Şöyle yaparız
@Configuration
@Slf4j
public class KafkaConfiguration {
  @Bean
  public DefaultErrorHandler errorHandler() {
    BackOff fixedBackOff = new FixedBackOff(5000, 3);
    DefaultErrorHandler errorHandler = new DefaultErrorHandler( (consumerRecord, exception) -> {
      log.error("Couldn't process message: {}; {}", consumerRecord.value().toString(), exception.toString());
    }, fixedBackOff);

    errorHandler.addNotRetryableExceptions(NullPointerException.class);
    return errorHandler;
  }
}
Açıklaması şöyle
Here we have specified that in case of an error, we will do retries (maximum three times) at intervals of five seconds. But if we have an NPE, we won't do iterations in that case but just write a message to the log and skip the message.

14 Haziran 2023 Çarşamba

SpringContext PathMatchingResourcePatternResolver Sınıfı

Giriş
Şu satırı dahil ederiz
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
Kalıtım şöyle
  ResourcePatternResolver
    PathMatchingResourcePatternResolver

getResource metodu
Resource döner
Örnek - classpath
Şöyle yaparız
@Component public class ValidationUtil { private final ResourcePatternResolver resourcePatternResolver; public ValidationUtil(ResourcePatternResolver resourcePatternResolver) { this.resourcePatternResolver = resourcePatternResolver; } public void readResource() { String path = "classpath:schema/user_details-country.json"; Resource resource = resourcePatternResolver.getResource(path); if (!resource.exists()) { ... } try (InputStream schemaStream = resource.getInputStream()) { ... } catch (Exception e) { ... } } }
getResources metodu
Resource[] döner.

1. classpath: olarak kullanırsak  src/main/resources altında belirtilen örüntüye uyan dosyaları arar
2. file: olarak kullanırsak  belirtilen örüntüye uyan dosyaları arar

Örnek - classpath
Şöyle yaparızsrc/main/resources/csv altındaki dosyalara erişiriz
@Component  
public class CsvUpsertCallback implements Callback {  
  @Autowired  
  private ResourceLoader resourceLoader;  

  // Get the path to the CSV directory  
  PathMatchingResourcePatternResolver resolver = 
    new PathMatchingResourcePatternResolver(resourceLoader);  
  Resource[] resources = resolver.getResources("classpath:csv/*");  
  List<File> files = new ArrayList<>();  
  for (Resource resource : resources) {  
    File file = resource.getFile();  
    if (file.getName().toLowerCase().endsWith(".csv")){  
      files.add(file);  
    }  
  }  
}
Örnek - "file:/path/to/folder/*.sql"
Şöyle yaparız
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources(ExecuteSqlScriptRunner.BASE_SCRIPT_FOLDER +
dirName + "/*.sql");

SpringData Flyway Callbacks

Giriş
Şu satırı dahil ederiz
import org.flywaydb.core.api.callback.Callback;
import org.flywaydb.core.api.callback.Context;
import org.flywaydb.core.api.callback.Event;
Flyway Callbacks kullanım senaryosu şöyle
Rebuilding materialized views – we might want to rebuild materialized views whenever we apply migrations affecting the base tables of those views. SQL callbacks are a good fit for executing this type of logic
Flushing a cache – perhaps we have a migration that modifies data that happens to be cached. We can use callbacks to flush caches making sure that our application pulls fresh data from the database
Calling an external system – using callbacks, we can call out to an external system using an arbitrary technology. For example, we might want to publish an event, send an email, or trigger a server restart
Örnek - AFTER_MIGRATE
Şöyle yaparız
@Component  
public class CsvUpsertCallback implements Callback {  
  @Autowired  
  private ResourceLoader resourceLoader;  
  @Override  
  public String getCallbackName() { return "csv_upsert_callback";}  
  @Override  
  public boolean supports(Event event, Context context) { 
    return event == Event.AFTER_MIGRATE;
  }  
  @Override  
  public boolean canHandleInTransaction(Event event, Context context) {return true;}  
  @Override  
  public void handle(Event event, Context context) {  
    Connection connection = context.getConnection();  
    try {  
      // Get the path to the CSV directory  
      PathMatchingResourcePatternResolver resolver = 
        new PathMatchingResourcePatternResolver(resourceLoader);  
      Resource[] resources = resolver.getResources("classpath:csv/*");  
      List<File> files = new ArrayList<>();  
      for (Resource resource : resources) {  
        File file = resource.getFile();  
        if (file.getName().toLowerCase().endsWith(".csv")){  
          files.add(file);  
        }  
      }  
     for (File csvFile : files) {  
       processCsvFile(csvFile, connection);  
     }  
    } catch (IOException | SQLException e) {  
      throw new SQLException("Error processing CSV files", e);  
    }  
  }   
}
Şöyle yaparız
private void processCsvFile(File csvFile, Connection connection)
throws IOException, SQLException {  
  String tableName = "my_table";  
  try (BufferedReader reader = new BufferedReader(new FileReader(csvFile))) {  
    String line;  
    while ((line = reader.readLine()) != null) {  
      // Split the line into columns  
      String[] columns = line.split(",");  
      
      // Perform UPSERT operation  
      String sql = "INSERT INTO " + tableName + " (id, name, email) " +  
          "VALUES (?, ?, ?) " +  
          "ON CONFLICT (id) " +  
          "DO UPDATE SET name = EXCLUDED.name, email = EXCLUDED.email";  
      
      try (PreparedStatement statement = connection.prepareStatement(sql)) {  
       statement.setInt(1, Integer.parseInt(columns[0]));  
       statement.setString(2, columns[1]);  
       statement.setString(3, columns[2]);  
       statement.executeUpdate();  
      }  
    }  
  }  
}

SpringMVC MultipartBodyBuilder Sınıfı

Giriş
Açıklaması şöyle
MultipartBodyBuilder is a class in the Spring Framework that provides a fluent API for constructing a multipart/form-data request body. This type of request is commonly used to upload files to a server.

A common use case for MultipartBodyBuilder is in a microservices architecture where you want to transfer a multipart file from one service to another.
Örnek
Elimizde şöyle bir kod olsun
@RestController
@CrossOrigin
@RequiredArgsConstructor
@RequestMapping(value = "/send-multipartfile")
@Slf4j
public class APIGatewayController {
    
  @PostMapping(value = "/post-multipart-data", consumes = MULTIPART_FORM_DATA, produces = MediaType.APPLICATION_JSON_VALUE)
  @Operation(
     operationId = "sendMultipartFileToSampleService",
     summary = "To upload multipart file and send it to sample micro-service, Call this API",
     description = "sendMultipartFileToSampleService method is HTTP POST mapping so upload file to send to micro-service."
   )
  public ResponseEntity<Response> sendMultipartFileToSampleService(@RequestParam("file") @Valid MultipartFile file) {
    return ResponseEntity.ok(
      sendMultipartFile( file, "<ip-address>:<port>/receive-multipartfile/get-multipart-data")
    );
  }
}
Şöyle yaparız. Burada MappingJackson2HttpMessageConverter kullanılarak MULTIPART_FORM_DATA verisi JSON olarak gönderiliyor
public Response sendMultipartFile(MultipartFile file, String apiValue) {
  // response to be return to request.
  Response returnResponse = new Response();
  RestTemplate restTemplate = new RestTemplate();

  try {
    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    converter.setSupportedMediaTypes(MediaType.MULTIPART_FORM_DATA);
    restTemplate.getMessageConverters().add(converter);

    // Create a new MultipartBodyBuilder
    MultipartBodyBuilder builder = new MultipartBodyBuilder();

    builder.part("file", file.getResource());
    // Set the content type to "multipart/form-data"
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.MULTIPART_FORM_DATA);

    // Create a new HttpEntity using the MultipartBodyBuilder
    HttpEntity < MultiValueMap < String, HttpEntity << ? >>> requestEntity = new HttpEntity < > (builder.build(), headers);

    // Send the request and get the response
    Response apiResponse = restTemplate.exchange(
      apiValue, HttpMethod.POST, requestEntity, Response.class).getBody();

    log.info("Getting response from API: {}...", apiValue);
    if (apiResponse != null) {
      // getting data of storageConfigResponse
      log.info("Response from api: {} is fetched successfully!", apiValue);
      returnResponse = apiResponse;
      return returnResponse;
    } else {
      log.info("Response of API: [{}] is coming null. Please Check!", apiValue);
    }
  } catch (Exception exception) {
    log.error("Failed to call API: [{}], Reason is: {} ", apiValue, exception.getMessage());
    returnResponse.setResponseTime(LocalDateTime.now());
    returnResponse.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
    returnResponse.setMessage("Internal Server Error");
    returnResponse.setExecutionMessage(apiValue);
  }
  return returnResponse;
}

SpringData Jdbc JdbcTemplate.queryForStream metodu

Giriş
Açıklaması şöyle
In the Spring Framework, the QueryByStream feature provides a powerful mechanism to fetch data from a database and process it in a streaming fashion, offering significant advantages in terms of efficiency and performance. This approach becomes particularly valuable when dealing with large datasets or requiring continuous data processing.
Açıklaması şöyle
Using the try-with-resources block ensures that the necessary resources are properly managed and released after processing the stream.
queryForStream - String sql, RowMapper +  Object... args
İmzası şöyle
<T> Stream<T> queryForStream(String sql, RowMapper<T> rowMapper, Object... args)
Örnek
Şöyle yaparız
public Optional<User> findById(Integer id) {
  try (Stream<User> stream = jdbcTemplate
        .queryForStream("select * from users where id=?", userMapper, id)) {
   return stream.findAny();
  }
}

SpringData Jdbc JdbcTemplate.queryForObject metodu

1. queryForObject metodu - sql + Object[] + RowMapper
İmzası şöyle
@Deprecated
@Override
Nullable
public <T> T queryForObject(String sql, @Nullable Object[] args,
  RowMapper<T> rowMapper) throws DataAccessException {
Açıklaması şöyle. Tek bir sonuç nesnesi dönmeli, yoksa IncorrectResultSizeDataAccessException fırlatır
IncorrectResultSizeDataAccessException - if the query does not return exactly one row, or does not return exactly one column in that row
RowMapper arayüzü ile kullanılır. RowMapper ve ParameterizedRowMapper sınıfları sorgu sonuçlarını nesneye çevirmek için kullanılır. Spring'in sağladığı BeanPropertyRowMapper ile de kullanılabilir.

Örnek
Şöyle yaparız
String sql = "SELECT * FROM users WHERE email = ?";
User user = jdbcTemplate.queryForObject(
  sql,
  new Object[]{email},
  new BeanPropertyRowMapper<>(User.class));

Örnek
Eğer döndürülen nesne tek bir sütun ise RowMapper olmadan kullanılır.  Select Name From Employee Where ID = ? sorgusu bir String yani nesne döndürür.

Örnek
Şöyle yaparız.
public Person findById(Integer id) {
  return this.template.queryForObject(this.findByIdSql, new Object[]{id},
    this.personRowMapper);
}
IncorrectResultSizeDataccessException 
Eğer veri tabanı boş sonuç dönerse exception fırlatır. Bunu yakalamak gerekir. 
Örnek
Şöyle yaparız
public Optional<User> findById(Integer id) {
  try {
   return Optional.ofNullable(jdbcTemplate
    .queryForObject("SELECT * FROM users WHERE id=?", userMapper,id)):
  } catch (IncorrectResultSizeDataccessException e) {
   return Optional.empty();
  }
}

13 Haziran 2023 Salı

SpringQuartz Kullanımı Job Yaratma

Giriş
Açıklaması şöyle
Job — An interface to be implemented by components that we wish to have executed. It has a single method called execute() on which we need to provide the details to be performed by the Job
Job iki şekilde kodlanabilir
1. Quartz projesinin Job arayüzünden kalıtılır
2. Spring' ait QuartzJobBean sınıfından  kalıtılır

Job Arayüzü
Örnek
Şöyle yaparız
public class FileDeletionJob implements Job {

  @Override
  public void execute(JobExecutionContext jobExecutionContext)
     throws JobExecutionException {
    String regex = jobExecutionContext.getJobDetail()
      .getJobDataMap()
      .getString("regex");
    File folder = new File(jobExecutionContext.getJobDetail()
                                              .getJobDataMap()
                                              .getString("path"));
    File[] files = folder.listFiles();

    Function<File, Boolean> regexMatcher = (file) -> Pattern.compile(regex)
      .matcher(file.getName()).find();

    System.out.println("#####Deleting Files#####");
    Arrays.stream(files).filter(regexMatcher::apply)
      .peek(System.out::println)
      .forEach(File::delete);
    System.out.println("#####Done#####");
  }
}
2. Spring QuartzJobBean Sınıfı
Açıklaması şöyle
Spring Boot provides a wrapper around Quartz Scheduler’s Job interface called QuartzJobBean. This allows you to create Quartz Jobs as Spring beans where you can autowire other beans.
Örnek
Şöyle yaparız
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.springframework.scheduling.quartz.QuartzJobBean;

@Component
public class EmailJob extends QuartzJobBean {

  @Override
  protected void executeInternal(JobExecutionContext jobExecutionContext)
throws JobExecutionException {
    logger.info("Executing Job with key {}", jobExecutionContext.getJobDetail().getKey());
    JobDataMap jobDataMap = jobExecutionContext.getMergedJobDataMap();
    ...
  }
}


SpringQuartz Veritabanı

Giriş
Normalde tabloları Quartz'ın kendisini yaratmasında fayda var. Ancak biz kendimiz yapmak istiyorsak şöyle yaparız
CREATE TABLE qrtz_job_details
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    JOB_NAME  VARCHAR(200) NOT NULL,
    JOB_GROUP VARCHAR(200) NOT NULL,
    DESCRIPTION VARCHAR(250) NULL,
    JOB_CLASS_NAME   VARCHAR(250) NOT NULL,
    IS_DURABLE BOOL NOT NULL,
    IS_NONCONCURRENT BOOL NOT NULL,
    IS_UPDATE_DATA BOOL NOT NULL,
    REQUESTS_RECOVERY BOOL NOT NULL,
    JOB_DATA BYTEA NULL,
    PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
);

CREATE TABLE qrtz_triggers
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(200) NOT NULL,
    TRIGGER_GROUP VARCHAR(200) NOT NULL,
    JOB_NAME  VARCHAR(200) NOT NULL,
    JOB_GROUP VARCHAR(200) NOT NULL,
    DESCRIPTION VARCHAR(250) NULL,
    NEXT_FIRE_TIME BIGINT NULL,
    PREV_FIRE_TIME BIGINT NULL,
    PRIORITY INTEGER NULL,
    TRIGGER_STATE VARCHAR(16) NOT NULL,
    TRIGGER_TYPE VARCHAR(8) NOT NULL,
    START_TIME BIGINT NOT NULL,
    END_TIME BIGINT NULL,
    CALENDAR_NAME VARCHAR(200) NULL,
    MISFIRE_INSTR SMALLINT NULL,
    JOB_DATA BYTEA NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
 REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP)
);

CREATE TABLE qrtz_simple_triggers
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(200) NOT NULL,
    TRIGGER_GROUP VARCHAR(200) NOT NULL,
    REPEAT_COUNT BIGINT NOT NULL,
    REPEAT_INTERVAL BIGINT NOT NULL,
    TIMES_TRIGGERED BIGINT NOT NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
 REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);

CREATE TABLE qrtz_cron_triggers
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(200) NOT NULL,
    TRIGGER_GROUP VARCHAR(200) NOT NULL,
    CRON_EXPRESSION VARCHAR(120) NOT NULL,
    TIME_ZONE_ID VARCHAR(80),
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
 REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);

CREATE TABLE qrtz_simprop_triggers
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(200) NOT NULL,
    TRIGGER_GROUP VARCHAR(200) NOT NULL,
    STR_PROP_1 VARCHAR(512) NULL,
    STR_PROP_2 VARCHAR(512) NULL,
    STR_PROP_3 VARCHAR(512) NULL,
    INT_PROP_1 INT NULL,
    INT_PROP_2 INT NULL,
    LONG_PROP_1 BIGINT NULL,
    LONG_PROP_2 BIGINT NULL,
    DEC_PROP_1 NUMERIC(13,4) NULL,
    DEC_PROP_2 NUMERIC(13,4) NULL,
    BOOL_PROP_1 BOOL NULL,
    BOOL_PROP_2 BOOL NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
    REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);

CREATE TABLE qrtz_blob_triggers
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(200) NOT NULL,
    TRIGGER_GROUP VARCHAR(200) NOT NULL,
    BLOB_DATA BYTEA NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
        REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);

CREATE TABLE qrtz_calendars
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    CALENDAR_NAME  VARCHAR(200) NOT NULL,
    CALENDAR BYTEA NOT NULL,
    PRIMARY KEY (SCHED_NAME,CALENDAR_NAME)
);


CREATE TABLE qrtz_paused_trigger_grps
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_GROUP  VARCHAR(200) NOT NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP)
);

CREATE TABLE qrtz_fired_triggers
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    ENTRY_ID VARCHAR(95) NOT NULL,
    TRIGGER_NAME VARCHAR(200) NOT NULL,
    TRIGGER_GROUP VARCHAR(200) NOT NULL,
    INSTANCE_NAME VARCHAR(200) NOT NULL,
    FIRED_TIME BIGINT NOT NULL,
    SCHED_TIME BIGINT NOT NULL,
    PRIORITY INTEGER NOT NULL,
    STATE VARCHAR(16) NOT NULL,
    JOB_NAME VARCHAR(200) NULL,
    JOB_GROUP VARCHAR(200) NULL,
    IS_NONCONCURRENT BOOL NULL,
    REQUESTS_RECOVERY BOOL NULL,
    PRIMARY KEY (SCHED_NAME,ENTRY_ID)
);

CREATE TABLE qrtz_scheduler_state
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    INSTANCE_NAME VARCHAR(200) NOT NULL,
    LAST_CHECKIN_TIME BIGINT NOT NULL,
    CHECKIN_INTERVAL BIGINT NOT NULL,
    PRIMARY KEY (SCHED_NAME,INSTANCE_NAME)
);

CREATE TABLE qrtz_locks
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    LOCK_NAME  VARCHAR(40) NOT NULL,
    PRIMARY KEY (SCHED_NAME,LOCK_NAME)
);

create index idx_qrtz_j_req_recovery on qrtz_job_details(SCHED_NAME,REQUESTS_RECOVERY);
create index idx_qrtz_j_grp on qrtz_job_details(SCHED_NAME,JOB_GROUP);

create index idx_qrtz_t_j on qrtz_triggers(SCHED_NAME,JOB_NAME,JOB_GROUP);
create index idx_qrtz_t_jg on qrtz_triggers(SCHED_NAME,JOB_GROUP);
create index idx_qrtz_t_c on qrtz_triggers(SCHED_NAME,CALENDAR_NAME);
create index idx_qrtz_t_g on qrtz_triggers(SCHED_NAME,TRIGGER_GROUP);
create index idx_qrtz_t_state on qrtz_triggers(SCHED_NAME,TRIGGER_STATE);
create index idx_qrtz_t_n_state on qrtz_triggers(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,TRIGGER_STATE);
create index idx_qrtz_t_n_g_state on qrtz_triggers(SCHED_NAME,TRIGGER_GROUP,TRIGGER_STATE);
create index idx_qrtz_t_next_fire_time on qrtz_triggers(SCHED_NAME,NEXT_FIRE_TIME);
create index idx_qrtz_t_nft_st on qrtz_triggers(SCHED_NAME,TRIGGER_STATE,NEXT_FIRE_TIME);
create index idx_qrtz_t_nft_misfire on qrtz_triggers(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME);
create index idx_qrtz_t_nft_st_misfire on qrtz_triggers(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_STATE);
create index idx_qrtz_t_nft_st_misfire_grp on qrtz_triggers(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_GROUP,TRIGGER_STATE);

create index idx_qrtz_ft_trig_inst_name on qrtz_fired_triggers(SCHED_NAME,INSTANCE_NAME);
create index idx_qrtz_ft_inst_job_req_rcvry on qrtz_fired_triggers(SCHED_NAME,INSTANCE_NAME,REQUESTS_RECOVERY);
create index idx_qrtz_ft_j_g on qrtz_fired_triggers(SCHED_NAME,JOB_NAME,JOB_GROUP);
create index idx_qrtz_ft_jg on qrtz_fired_triggers(SCHED_NAME,JOB_GROUP);
create index idx_qrtz_ft_t_g on qrtz_fired_triggers(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP);
create index idx_qrtz_ft_tg on qrtz_fired_triggers(SCHED_NAME,TRIGGER_GROUP);
FIRED_TRIGGERS  Tablosu
Her çalışan Job bu tabloya bir kayıt atar. İş bitince de siler.



SpringTest Testcontainers @ServiceConnection

Giriş
Şu satırı dahil ederiz
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
Açıklaması şöyle
The @ServiceConnection removes the need to write @DynamicPropertySource property overrides.
Örnek
Şöyle yaparız
@SpringBootTest
public class ServiceConnectionFieldTest {

  @Container
  @ServiceConnection
  static PostgreSQLContainer postgres = new PostgreSQLContainer(DockerImageName.parse("postgres:15.1"))
      .withUsername("testUser")
      .withPassword("testSecret")
      .withDatabaseName("testDatabase");

  @BeforeAll
  static void setup(){
    postgres.start();
  }
  ...
}
@RestartScope Anotasyonu
Giriş
Şu satırı dahil ederiz
import org.springframework.boot.devtools.restart.RestartScope;
Örnek - 
Açıklaması şöyle
annotation which prevents the container restarts during automatic reloads by Spring Boot DevTools.