4 Mayıs 2021 Salı

SpringContext LocaleContextHolder Sınıfı

Giriş
Şu satırı dahil ederiz
import org.springframework.context.i18n.LocaleContextHolder;
getLocale metodu
Şöyle yaparız
Locale locale = LocaleContextHolder.getLocale();

SpringNative

Giriş
Spring 3 ile bu yazıyı iki kısma ayırmak gerekti.

1. Spring 3
Spring 3 SpringNative yazısına taşıdım

2. Spring 3'ten Önce
Daha önceki Spring sürümler için açıklama şöyle
Spring Boot offers two alternatives to create native binaries:
1. A system-dependent binary: this approach requires a local GraalVM installation with the native-image extension. It will create a non-cross-platform system-dependent binary.
For this, Spring Boot has a dedicated profile:
./mvnw -Pnative package

2. A Docker image: this approach builds a containerized version of the application. It requires a local image build, e.g., Docker. Internally, it leverages CNCF Buildpacks (but doesn’t require pack).
Spring Boot provides a Maven target for this:
./mvnw spring-boot:native-image
Eğer yerel GraalVM kurulumunu seçeceksek ilave olarak native-image extension da kurulmalıdır

Her iki yöntem için de 
1. dependency olarak spring-native eklenir. 
2. plugin  olarak şu eklenir
maven için : spring-aot-maven-plugin 
gradle için :  org.springframework.experimental.aot
Açıklaması şöyle
The Spring AOT plugin will automatically be run in the build pipeline to create a special Spring Boot JAR which is needed to run when compiled to a native image. It will try to create all needed reachability configurations for your program to work correctly as a native image.
reflection-config.json Dosyası
SpringNative reflection-config.json Dosyası yazısına taşıdım

Maven
Örnek
Şöyle yaparız
<dependency>
  <groupId>org.springframework.experimental</groupId>
  <artifactId>spring-native</artifactId>
  <version>${spring-native.version}</version>
</dependency>
<plugin>
  <groupId>org.springframework.experimental</groupId>
  <artifactId>spring-aot-maven-plugin</artifactId>
  <version>${spring-native.version}</version>
  <executions>
    <execution>
      <id>test-generate</id>
      <goals>
        <goal>test-generate</goal>
      </goals>
    </execution>
    <execution>
      <id>generate</id>
        <goals>
          <goal>generate</goal>
        </goals>
      </execution>
  </executions>
</plugin>
Gradle
Örnek
Şöyle yaparız
plugins {
  id 'org.springframework.boot' version '2.6.4'
  id 'io.spring.dependency-management' version '1.0.11.RELEASE'
  id 'java'
  id 'org.springframework.experimental.aot' version '0.11.3'
}

group = 'com.springnative.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
  maven { url 'https://repo.spring.io/release' }
  mavenCentral()
}

dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-web'
  testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
  useJUnitPlatform()
}

tasks.named('bootBuildImage') {
  builder = 'paketobuildpacks/builder:tiny'
  environment = ['BP_NATIVE_IMAGE': 'true']
}
Şöyle yaparız
./gradlew nativeRun
build-image seçeneği
Projeyi maven ile derlesek bir tane docker image üretir. Bunu derleyip çalıştırmak için şöyle yaparız
$ mvn clean package
$ mvn spring-boot:build-image

$ docker run --name spring-native-example -p 8080:8080 spring-native-example:0.0.1-SNAPSHOT
Örnek
Şöyle yaparız
//Build a normal Spring Boot image
mvn spring-boot:build-image -DskipTests

//Build a Spring Boot native image
mvn -Pnative spring-boot:build-image -DskipTests
native-image seçeneği
Şöyle yaparız
./mvnw spring-boot:native-image
@TypeHint Anotasyonu
Kodda reflection configuration belirtmeyi sağlar
Örnek - AccessBits.FULL_REFLECTION 
Şöyle yaparız
@SpringBootApplication
@NativeHint(options = ["--enable-https"])                        <1>
@TypeHint(
    types = [
        Model::class, Data::class, Result::class, Thumbnail::class,
        Collection::class, Resource::class, Url::class, URI::class
    ],
    access = AccessBits.FULL_REFLECTION                          <2>
)
class BootNativeApplication {...}
Örnek
Şöyle yaparız
@TypeHint(
  types = [News::class],
  access = [TypeAccess.DECLARED_CONSTRUCTORS, TypeAccess.PUBLIC_METHODS]
)
@SpringBootApplication
class SpringBootKotlinReactiveApplication





SpringData Multitenancy

Giriş
Multitenancy için 3 yöntem var. Bunlar şöyle
Separate database 
Separate schema
Shared schema
Multitenancyi için spring ve hibernate'i ayrı ayrı ayarlamak lazım

Not : Webflux MongoDB için örnek burada

Spring
Elimizde şöyle bir kod olsun
public abstract class TenantContext {
 
  public static final String DEFAULT_TENANT_ID = "public";
  private static ThreadLocal<String> currentTenant = new ThreadLocal<>();
 
  public static void setCurrentTenant(String tenant) {
    currentTenant.set(tenant);
  }
 
  public static String getCurrentTenant() {
    return currentTenant.get();
  }
 
  public static void clear() {
    currentTenant.remove();
  }
}
Bu kodu dolduracak bir interceptor yazılır. Açıklaması şöyle
In the earlier versions of Spring Boot, we could extend the org.springframework.web.servlet.handler.HandlerInterceptorAdapter class, but in newer versions, the class is deprecated,...
Yani org.springframework.web.servlet.AsyncHandlerInterceptor kullanılır. Şöyle yaparız
@Component
public class TenantRequestInterceptor implements AsyncHandlerInterceptor {
        
  private SecurityDomain securityDomain;
        
  public TenantRequestInterceptor(SecurityDomain securityDomain) {
    this.securityDomain = securityDomain;
  }
 
  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) {
    return Optional.ofNullable(request)
      .map(req -> securityDomain.getTenantIdFromJwt(req))
      .map(tenant -> setTenantContext(tenant))
      .orElse(false);
  }
 
  @Override
  public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) {
    TenantContext.clear();
  }
         
  private boolean setTenantContext(String tenant) {
    TenantContext.setCurrentTenant(tenant);
    return true;
  }
}
Interceptor eklenir
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
 
  @Autowired
  private TenantRequestInterceptor tenantInterceptor;
        
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(tenantInterceptor).addPathPatterns("/**");
  }      
}

1. Separate database
Hibernate tarafından sağlanan AbstractMultiTenantConnectionProvider sınıfından kalıtan yeni bir ConnectionProvider yazılır. Şeklen şöyle



Bu sınıfı kodlarken şu metodlar override edilir
protected ConnectionProvider getAnyConnectionProvider();
protected ConnectionProvider selectConnectionProvider(String tenantIdentifier);
Örnek
Şöyle yaparız
public class SchemaMultiTenantConnectionProvider extends 
  AbstractMultiTenantConnectionProvider {
        
  public static final String HIBERNATE_PROPERTIES_PATH = "/hibernate-%s.properties";
  private final Map<String, ConnectionProvider> connectionProviderMap;
 
  public SchemaMultiTenantConnectionProvider() {
    this.connectionProviderMap = new HashMap<String, ConnectionProvider>();
  }
        
  @Override
  protected ConnectionProvider getAnyConnectionProvider() {
    return getConnectionProvider(TenantContext.DEFAULT_TENANT_ID);
  }
 
   @Override
   protected ConnectionProvider selectConnectionProvider(String tenantIdentifier) {
     return getConnectionProvider(tenantIdentifier);
  }
        
  private ConnectionProvider getConnectionProvider(String tenantIdentifier) {
    return Optional.ofNullable(tenantIdentifier)
                   .map(connectionProviderMap::get)
                   .orElseGet(() -> createNewConnectionProvider(tenantIdentifier));
  }
 
   private ConnectionProvider createNewConnectionProvider(String tenantIdentifier) {
     return Optional.ofNullable(tenantIdentifier)
                    .map(this::createConnectionProvider)
                    .map(connectionProvider -> {
                      connectionProviderMap.put(tenantIdentifier, connectionProvider);
                      return connectionProvider;
                     })
                     .orElseThrow(() -> 
                        new ConnectionProviderException(
                          String.format("Cannot create new connection provider 
                                         for tenant: %s", tenantIdentifier))
                    );
  }
        
  private ConnectionProvider createConnectionProvider(String tenantIdentifier) {
    return Optional.ofNullable(tenantIdentifier)
                   .map(this::getHibernatePropertiesForTenantId)
                   .map(this::initConnectionProvider)
                   .orElse(null);
  }
        
  private Properties getHibernatePropertiesForTenantId(String tenantId) {
    try {
      Properties properties = new Properties();
      properties.load(getClass().getResourceAsStream(
        String.format(HIBERNATE_PROPERTIES_PATH, tenantId)));
      return properties;
    } catch (IOException e) {
      throw new RuntimeException(
     String.format("Cannot open hibernate properties: %s", HIBERNATE_PROPERTIES_PATH));
    }
  }
 
   private ConnectionProvider initConnectionProvider(Properties hibernateProperties) {
     DriverManagerConnectionProviderImpl connectionProvider = 
      new DriverManagerConnectionProviderImpl();
     connectionProvider.configure(hibernateProperties);
     return connectionProvider;
   }
}
Açıklaması şöyle
The only thing we have to implement in this class are the methods getAnyConnectionProvider() and selectConnectionProvider(String tenantId).

The first method sets up a connection to the database when the tenantId is not set. This happens when the application is starting. Validating whether the tenant is set can be implemented in AsyncHandlerInterceptor on the Spring side and throws an exception when it’s not set or is incorrect.

The second method is responsible for returning the appropriate ConnectionProvider for the indicated tenantId. The solution assumes that we collect ConnectionProvider on a map so as not to create a new one every time. If it’s not on the map yet, then we create a new one and add it to the map. Of course, this can be moved to the classic cache, where you can additionally manage lifetime (TTL).



31 Mart 2021 Çarşamba

SpringWebFlux @WebFluxTest Anotasyonu


@WebFluxTest Anotasyonu
Örnek
Şöyle yaparız
@WebFluxTest(controllers = {RateRestController.class})
@Tag("UnitTest")
public class RateRestControllerTest {

  @MockBean
  private RateService rateService;
  
  @Autowired
  WebTestClient webTestClient;
  
  @Test
  public void getLatestRates() throws Exception {

    // Mock return data of rate service
    when(rateService.fetchLatestRates(anyString()))    
    .thenAnswer(invocation -> {
      String baseCurrency = (String) invocation.getArgument(0);
      LocalDateTime timestamp = LocalDateTime.now();
      return Flux.just(
          new Rate(timestamp, baseCurrency, "USD", Math.random()),
          new Rate(timestamp, baseCurrency, "EUR", Math.random()),
          new Rate(timestamp, baseCurrency, "CAD", Math.random()),
          new Rate(timestamp, baseCurrency, "JPY", Math.random())
          );
    });
    
    // trigger API request to rate controller
    webTestClient.get()
    .uri("/rates/latest/GBP")
    .accept(MediaType.APPLICATION_JSON)
    .exchange()
    .expectStatus().isOk()
    .expectBody()
    .jsonPath("$").isArray()
    .jsonPath("$[0].baseCurrency").isEqualTo("GBP")
    .jsonPath("$[0].counterCurrency").isEqualTo("USD")
    .jsonPath("$[0].rate").isNumber()
    .jsonPath("$[1].baseCurrency").isEqualTo("GBP")
    .jsonPath("$[1].counterCurrency").isEqualTo("EUR")
    .jsonPath("$[1].rate").isNumber()
    .jsonPath("$[2].baseCurrency").isEqualTo("GBP")
    .jsonPath("$[2].counterCurrency").isEqualTo("CAD")
    .jsonPath("$[2].rate").isNumber()
    .jsonPath("$[3].baseCurrency").isEqualTo("GBP")
    .jsonPath("$[3].counterCurrency").isEqualTo("JPY")
    .jsonPath("$[3].rate").isNumber();    
  }
}

29 Mart 2021 Pazartesi

SpringContext Internationalization MessageSource Arayüzü

Giriş
Şu satırı dahil ederiz
import org.springframework.context.MessageSource;
Gösterilecek metinleri yükleyen arayüz. Açıklaması şöyle
MessageSource is an interface that defines several methods for resolving messages. The ApplicationContext interface extends this interface so that all application contexts are able to resolve text messages.
Bu arayüzü gerçekleştiren iki tane sınıf var. Bunlar ResourceBundleMessageSource ve ReloadableResourceBundleMessageSource.

Açıklaması şöyle
Spring Boot has i18n built-in thanks to the Spring Framework and its MessageSource implementations. There’s a ResourceBundleMessageSource that builds on ResourceBundle, as well as a ReloadableResourceBundleMessageSource that should be self-explanatory.

Inject MessageSource into a Spring bean and call getMessage(key, args, locale) to your heart’s content!

SpringKafka Consumer ConcurrentKafkaListenerContainerFactory.setErrorHandler() metodu

Giriş
Hataları ele almak için seçeneklerimiz şöyle
When you build your spring boot application and make use of Kafka in order to create some consumer, Spring provides on its own a listener container for asynchronous execution of POJO listeners. The provided listener container has three ways to handle a potential exception:

1. Ignores it and moves to the next record.
2. It can retry to process the same item from the listed topics/partitions of that listener.
3. It can send the item to a dead letter topic.
Açıklaması şöyle. Yani eğer bir şey yapmazsak varsayılan davranış hatalar dikkate almamak
By default, records that fail are simply logged, and we move on to the next one. We can, however, configure an error handler in the listener container to perform some other action. 
Retry
Retry için iki seçenek var
1. Kafka Client Kütüphanesini kullanmak. Bu yöntem stateless retry olarak anılıyor.
2. Spring sınıflarını kullanmak. Bu yöntem stateful retry olarak anılıyor.

setErrorHandler metodu - Stateful Retry İçindir
SeekToCurrentErrorHandler kullanılabilir.


setRetryTemplate metodu - Stateless Retry İçindir
Açıklaması şöyle
The Java Kafka client library offers stateless retry, with the Kafka consumer retrying a retryable exception as part of the consumer poll.
- Retries happen within the consumer poll for the batch.
- Consumer poll must complete before poll timeout, containing all retries, and total processing time (including REST calls & DB calls), retry delay and backoff, for all records in the batch.
- Default poll time is 5 minutes for 500 records in the batch. This only averages to 600ms per event.
- If poll time is exceeded this results in event duplication.
- Calculation of retries/time possible, but total retry duration will have to be short.
Örnek
Şöyle yaparız
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.retry.support.RetryTemplate;

//Stateless retry listener.
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String>
  kafkaStatelessRetryListenerContainerFactory(
    ConsumerFactory<String, String> consumerFactory, final RetryTemplate retryTemplate) {

  ConcurrentKafkaListenerContainerFactory<String, String> factory =
    new ConcurrentKafkaListenerContainerFactory();
  factory.setConsumerFactory(consumerFactory);
  factory.setRetryTemplate(retryTemplate);
  factory.setRecoveryCallback((context -> {
    log.warn("**** Retries exhausted - error class: "+context.getLastThrowable() +
             " - error message: "+context.getLastThrowable().getMessage());
      // Return null to mark processing complete.
      return null;
  }));
  return factory;
}

@Bean
public RetryTemplate retryTemplate() {
  return RetryTemplate.builder()
    .fixedBackoff(4000)
    .maxAttempts(5)
    .build();
}

23 Mart 2021 Salı

SpringBoot spring.jpa Hibernate'e Özel Ayarlar - Hibernate Naming Strategies

Giriş
İki tane naming strategy var. Bunlar ImplicitNamingStrategy ve PhysicalNamingStrategy. Eğer nesnemize bir isim vermediysek, ImplicitNamingStrategy devreye girer ve bir isim üretir. Ancak bu isim veri tabanındaki isim değildir. Hibernate tarafından kullanılan mnatıksa bir isimdir. Bu ismi gerçek veri tabanına dönüştüren şey ise PhysicalNamingStrategy.

Yani şeklen şöyle


Hibernate Açısından Arayüzlerin Açıklaması
Açıklaması şöyle
According to the documentation, there are two interfaces responsible for naming your tables, columns etc. in Hibernate: ImplicitNamingStrategy and PhysicalNamingStrategy.
Aslında bunlar arayüz oldukları için gerçekte kullanılan sınıflar ImplicitNamingStrategyJpaCompliantImpl ve PhysicalNamingStrategyStandardImpl.

Bir de ImplicitNamingStrategyLegacyJpaImpl var. Ancak bu JPA 1.0 için kullanılıyordu.

1. ImplicitNamingStrategy 
Açıklaması şöyle. Eğer bir nesneye isim vermezsek bu arayüz devreye girer ve bir isim üretir.
ImplicitNamingStrategy is in charge of naming all objects that were not explicitly named by a developer: e.g. entity name, table name, column name, index, FK etc. The resulting name is called the logical name, it is used internally by Hibernate to identify an object. It is not the name that gets put into the DB.
Örnek
Şöyle yaparız.
spring.jpa.hibernate.naming.physical-strategy=
  org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
Örnek
Şöyle yaparız
spring:
 jpa:
  hibernate:
   naming:
    physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
2. PhysicalNamingStrategy
Açıklaması şöyle
PhysicalNamingStrategy provides the actual physical name used in the DB based on the logical JPA object name. Effectively, this means that using Hibernate you cannot specify database object names directly, but only logical ones.
Bu aslında şu anlama geliyor
Effectively, this means that using Hibernate you cannot specify database object names directly, but only logical ones.

Spring Açısından Arayüzlerin Açıklaması
Ancak Spring, Hibernate tarafından sağlanan arayüzlere kendi sınıflarını takıyor. Açıklaması şöyle.
Spring Boot overrides Hibernate default implementations for both interfaces and uses SpringImplicitNamingStrategy and SpringPhysicalNamingStrategy instead.
1. SpringImplicitNamingStrategy
Açıklaması şöyle.
Effectively, SpringImplicitNamingStrategy copies the behaviour of ImplicitNamingStrategyJpaCompliantImpl with only a minor difference in join table naming. 
...
By default, Spring Boot configures the physical naming strategy with SpringPhysicalNamingStrategy. This implementation provides the same table structure as Hibernate 4: all dots are replaced by underscores and camel casing is replaced by underscores as well. Additionally, by default, all table names are generated in lower case. For example, a TelephoneNumber entity is mapped to the telephone_number table.
...
Basically, it always transforms camelCase and PascalCase to snake_case. In fact, using it isn't possible to work with non_snake_case at all. 
Açıklaması şöyle.
By default, Spring Boot configures the physical naming strategy with SpringPhysicalNamingStrategy. Which does this: For example, a TelephoneNumber entity is mapped to the telephone_number table (same goes for columns).
Burada Spring JPA standardından sapıyor ve isimleri snake_case olarak üretiyor. Açıklaması şöyle
The JPA default table name is the name of the class (minus the package) with the first letter capitalized. 
2. SpringPhysicalNamingStrategy
@Table(name = "PetType") versek bile veri tabanında pet_type isimli tablo oluşur
@Table(name = "\"PetType\"")  versek bile veri tabanında "pet_type" isimli tablo oluşur
Eğer kendi nesnemizi takmak istersek şöyle yaparız.
public class UpperCaseNamingStrategy extends SpringPhysicalNamingStrategy {
  @Override
  protected Identifier getIdentifier(String name, boolean quoted,
JdbcEnvironment jdbcEnvironment) {
    return new Identifier(name.toUpperCase(), quoted);
  }
}
Kullanmak için şöyle yaparız.
spring.jpa.hibernate.naming.physical-strategy=
com.baeldung.namingstrategy.UpperCaseNamingStrategy