14 Nisan 2023 Cuma

SpringCache SimpleCacheManager Sınıfı

Giriş
Şu satırı dahil ederiz
import  org.springframework.cache.support.SimpleCacheManager;
Örnek
Şöyle yaparız
@Configuration
@EnableCaching
public class CachingConfig {

  @Bean
  public CacheManager cacheManager() {
    SimpleCacheManager cacheManager = new SimpleCacheManager();
    cacheManager.setCaches(Arrays.asList(
      new ConcurrentMapCache("directory"), 
      new ConcurrentMapCache("addresses")));
    return cacheManager;
  }
}
Örnek
Şöyle yaparız.
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
  <property name="caches">
    <set>
      <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
         p:name="foo"/>
    </set>
  </property>
</bean>

10 Nisan 2023 Pazartesi

SpringData Persistable Arayüzü

Giriş
Şu satırı dahil ederiz
import  org.springframework.data.domain.Persistable;
Açıklaması şöyle. Böylece JPA sağlayıcısı belirtilen nesnenin durumu ile veri tabanındaki durumu senkronize etmeye çalışmaz. Yani kısaca nense için fazladan bir SELECT çalıştırmaz.
It enables us to instruct JPA that the entity that we are about to persist is new
Örnek
Şöyle yaparız
@MappedSuperclass
public abstract class AbstractPersistableEntity<T extends Serializable> 
        implements Persistable<T> {

  @Transient
  private boolean isNew = true;

  @Override
  public boolean isNew() {
    return isNew;
  }

  public void setNew(boolean isNew) {
    this.isNew = isNew;
  }

  @PostLoad
  @PostPersist
  void markNotNew() {
    this.isNew = false;
  }
}

@Entity
@Table(name = "employees")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
public class Employee extends AbstractPersistableEntity<Integer> {

  @Id
  @Column(name = "emp_no")
  private int employeeId;

  @Column(name = "birth_date")
  private LocalDate birthDate;

  @Column(name = "first_name")
  private String firstName;

  @Column(name = "last_name")
  private String lastName;

  @Column(name = "hire_date")
  private LocalDate hireDate;

  @OneToMany(mappedBy = "employeeId", fetch = FetchType.EAGER, 
    cascade = CascadeType.ALL, orphanRemoval = true)
  private Set<Salary> salaries;

  @OneToMany(mappedBy = "employeeId", fetch = FetchType.EAGER, 
    cascade = CascadeType.ALL, orphanRemoval = true)
  private Set<Title> titles;

  @Override
  public Integer getId() {
    return employeeId;
  }
}
Şöyle yaparız
@Entity
@Table(name = "employees")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
public class Employee extends AbstractPersistableEntity<Integer> {
  @Id
  @Column(name = "emp_no")
  private int employeeId;

  @Column(name = "birth_date")
  private LocalDate birthDate;

  @Column(name = "first_name")
  private String firstName;

  @Column(name = "last_name")
  private String lastName;
  ...

  @OneToMany(mappedBy = "employeeId", fetch = FetchType.EAGER, 
    cascade = CascadeType.ALL, orphanRemoval = true)
  private Set<Salary> salaries;

  @OneToMany(mappedBy = "employeeId", fetch = FetchType.EAGER, 
    cascade = CascadeType.ALL, orphanRemoval = true)
  private Set<Title> titles;

  @Override
  public Integer getId() {
    return employeeId;
  }
}

SpringData JpaRepository.saveAndFlush metodu - Kullanmayın

Giriş
Normalde JPA sağlayıcısı save() işlemini hemen veri tabanına göndermez. Optimizasyon amaçlı biraz bekletir. saveAndFlush () JPA sağlayıcısını işlemin hemen veri tabanında uygulanmasını sağlar.

İmzası şöyle.
PaymentMethod saveAndFlush(PaymentMethods entity);
Örnek
Tabloya yapılan işlemin hemen görülmesi için kullanılır. Şöyle yaparız.
@Transactional
public void saveAndGenerateResult(Data data) {
    saveDataInTableA(data.someAmountForA);
    saveDataInTableB(data.someAmountForB);
    callAnAggregatedFunction(data);
}

public void saveDataInTableA(DataA a) {
    tableARepository.saveAndFlush(a);
}

public void saveDataInTableA(DataB b) {
    tableBRepository.saveAndFlush(b);
}

public void callAnAggregatedFunction() {
  // Do something based on the data saved from the beginning in Table A and Table B
}

SpringKafka Consumer @RetryableTopic Anotasyonu - Non-Blocking Retry

Giriş
Şu satırı dahil ederiz
import org.springframework.kafka.annotation.RetryableTopic;
Açıklaması şöyle
Non-Blocking retries in Kafka are done via configuring retry topics for the main topic. An Additional Dead Letter Topic can also be configured if required. Events will be forwarded to DLT if all retries are exhausted.

Konfigürasyon için kullanılabilecek parametreler şeklen şöyle. springframework.kafka 2.7'den itibaren geliyor

Açıklaması şöyle
Spring Retryable Topics
Spring uses retryable topics to achieve non-blocking retry. Rather than retry an event from the original topic in a blocking manner, Spring Kafka instead writes the event to a separate retry topic. The event is marked as consumed from the original topic, so the next events continue to be polled and processed. Meanwhile a separate instance of the same consumer is instantiated by Spring as the consumer for the retry topic. This ensures that a single consumer instance is not polling and receiving events from both the original and a retry topic.

If the event needs to be retried multiple times, it can either be retried from the single retry topic, or it can be written to a further retry topic. The advantage of a single retry topic is that there are less topics to be dealing with. The downside is that events being retried from it will be blocking this retry topic. Alternatively any number of further retry topics can be used, ensuring each is not blocked when the event is retried. Each retry topic might have a longer back-off reflecting the need to give the system more time to be in a state that it can process the event successfully.

Once all retries are exhausted a dead letter topic can be configured to write the event to. Optionally a method in the consumer class can be annotated to consume from this topic.
Örnek
Şöyle yaparız. Burada non-blocking retry var. 3 tane retry için topic yaratılıyor. Ayrıca @Dlt ile bir dead letter topic yaratılıyor.
import org.springframework.kafka.annotation.DltHandler;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.annotation.RetryableTopic;
import org.springframework.kafka.retrytopic.FixedDelayStrategy;
import org.springframework.kafka.retrytopic.TopicSuffixingStrategy;
import org.springframework.kafka.support.KafkaHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.retry.annotation.Backoff;
import org.springframework.stereotype.Component;

@Slf4j
@RequiredArgsConstructor
@Component
public class UpdateItemConsumer {

  private final ItemService itemService;

  @RetryableTopic(
    attempts = "#{'${demo.retry.maxRetryAttempts}'}",
    autoCreateTopics = "#{'${demo.retry.autoCreateRetryTopics}'}",
    backoff = @Backoff(delayExpression = "#{'${demo.retry.retryIntervalMilliseconds}'}", multiplierExpression = "#{'${demo.retry.retryBackoffMultiplier}'}"),
    fixedDelayTopicStrategy = FixedDelayStrategy.MULTIPLE_TOPICS,
    include = {RetryableMessagingException.class},
    timeout = "#{'${demo.retry.maxRetryDurationMilliseconds}'}",
    topicSuffixingStrategy = TopicSuffixingStrategy.SUFFIX_WITH_INDEX_VALUE)
  @KafkaListener(topics = "#{'${demo.topics.itemUpdateTopic}'}", containerFactory = "kafkaListenerContainerFactory")
  public void listen(@Payload final String payload) {
    log.info("Update Item Consumer: Received message with payload: " + payload);
    try {
      UpdateItem event = JsonMapper.readFromJson(payload, UpdateItem.class);
      itemService.updateItem(event);
    } catch (RetryableMessagingException e) {
      // Ensure the message is retried.
      throw e;
    } catch (Exception e) {
      log.error("Update item - error processing message: " + e.getMessage());
    }
  }

  @DltHandler
  public void dlt(String data, @Header(KafkaHeaders.RECEIVED_TOPIC) String topic) {
    log.error("Event from topic "+topic+" is dead lettered - event:" + data);
  }
}
backoff  Alanı
Örnek
Şöyle yaparız
@RetryableTopic(kafkaTemplate = "kafkaTemplate",
attempts = "4", backoff = @Backoff(delay = 3000, multiplier = 1.5, maxDelay = 15000) ) @KafkaListener(topics = ORDER_TOPIC, groupId = ORDER_STATUS_GROUP_ID_PREFIX + "#{ T(java.util.UUID).randomUUID().toString() }") @Transactional public void orderEventListener(@Header(KafkaHeaders.RECEIVED_TOPIC) String receivedTopic, OrderEvent orderEvent, Acknowledgment ack) throws SocketException { log.info("Topic({}) handler receive data = {}", receivedTopic, orderEvent); try { orderEventRecordHandler.onEvent(orderEvent); if (receivedTopic.contains("retry")) { orderRecordHandler.onRequeueEvent(orderEvent); } else { orderRecordHandler.onEvent(orderEvent); } ack.acknowledge(); } catch (Exception e) { log.warn("Fail to handle event {}.", orderEvent); throw e; } }
exclude Alanı
Hangi exception olursa retry olmayacağını belirtir. Açıklaması şöyle.
In some cases, the message is definitely unprocessable (like parsing error, or invalid properties…). Then we should not waste our resources trying to consume it.

we can use the include and exclude properties to control which exception should/should not be retried 

Örnek
Şöyle yaparız
@RetryableTopic(kafkaTemplate = "kafkaTemplate",
  exclude = {DeserializationException.class,
             MessageConversionException.class,
             ConversionException.class,
             MethodArgumentResolutionException.class,
             NoSuchMethodException.class,
             ClassCastException.class},
  attempts = "4",
  backoff = @Backoff(delay = 3000, multiplier = 1.5, maxDelay = 15000)
)
include Alanı
Hangi exception olursa retry olacağını belirtir.
Örnek
Şöyle yaparız
@Slf4j
@Component
@RequiredArgsConstructor
public class CustomEventConsumer {

  private final CustomEventHandler handler;

  @RetryableTopic(attempts = "${retry.attempts}",
    backoff = @Backoff(
      delayExpression = "${retry.delay}",
      multiplierExpression = "${retry.delay.multiplier}"
    ),
    topicSuffixingStrategy = TopicSuffixingStrategy.SUFFIX_WITH_INDEX_VALUE,
    dltStrategy = FAIL_ON_ERROR,
    autoStartDltHandler = "true",
    autoCreateTopics = "false",
    include = {CustomRetryableException.class})
  @KafkaListener(topics = "${topic}", id = "${default-consumer-group:default}")
  public void consume(CustomEvent event,
                      @Header(KafkaHeaders.RECEIVED_TOPIC) String topic) {
    try {
      log.info("Received event on topic {}", topic);
      handler.handleEvent(event);
    } catch (Exception e) {
      log.error("Error occurred while processing event", e);
      throw e;
    }
  }

  @DltHandler
  public void listenOnDlt(@Payload CustomEvent event) {
    log.error("Received event on dlt.");
    handler.handleEventFromDlt(event);
  }
}




6 Nisan 2023 Perşembe

SpringBoot application.properties Compression Ayarları

server.compression.min-response-size Alanı
Örnek
Şöyle yaparız. Cevap 2048 byte'tan büyükse sıkıştırılır
server.compression.enabled=true
server.compression.min-response-size=2048
server.compression.mime-types Alanı
Örnek
Açıklaması şöyle
GZip compression is disabled by default in Spring Boot.
...
Note that, GZip compression has a small overhead. Therefore I’ve added a min-response-size property to tell spring boot server to compress the response only if the size is more than the given value.
Şöyle yaparız
# Enable response compression
server.compression.enabled=true

# The comma-separated list of mime types that should be compressed
server.compression.mime-types=text/html,text/xml,text/plain,text/css,
  text/javascript,application/javascript,application/json

# Compress the response only if the response size is at least 1KB
server.compression.min-response-size=1024
server.compression.servlet-path-patterns Alanı
Sadece belirtilen path'e gelen istekleri sıkıştırır
Örnek
Şöyle yaparız
server.compression.mime-types=text/html,text/xml,text/plain,text/css,\
  application/json,application/javascript
server.compression.servlet-path-patterns=/*

5 Nisan 2023 Çarşamba

SpringData Jdbc HikariDataSourcePoolMetadata Sınıfı

Giriş
Şu satırı dahil ederiz.
import org.springframework.boot.autoconfigure.jdbc.metadata.HikariDataSourcePoolMetadata;
Örnek
Şöyle yaparız
new HikariDataSourcePoolMetadata(dataSource).getActive();

4 Nisan 2023 Salı

SpringData JPA Multiple Databases Kullanımı - @EnableJpaRepositories İle

Giriş
İzlenmesi gereken adımların sırası şöyle
1. İki tane DataSource yaratılır. Bir tanesi @Primary olarak işaretlenir.
2. İki tane LocalContainerEntityManagerFactoryBean yaratılır. Bir tanesi @Primary olarak işaretlenir. Her birisine ilgili DataSource atanır
3. @EnableJpaRepositories anotasyonunda ilgili LocalContainerEntityManagerFactoryBean ve PlatformTransactionManager belirtilir. basePackages ile hangi sınıfların kullanılacağı belirtilir. Gerekiyorsa excludeFilters ile hangi JPA sınıflarının kullanılmayacağı belirtilir.
Bir örnek burada.

Örnek - Master ve Slave DB Ayrımı
Birinci veri tabanı konfigürasyonu için şöyle yaparız. Burada excludeFilters alanı önemli.
@Configuration
@EnableJpaRepositories(
        basePackages = "...",
        excludeFilters = @ComponentScan.Filter(ReadOnlyRepository.class),
        entityManagerFactoryRef = "primaryEntityManagerFactory"
)
public class PrimaryDataSourceConfiguration {

  @Bean
  @Primary
  public DataSource primaryDataSource() {
    ...
  }

  @Bean
  @Primary
  public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory() {
    ...    
  }
}
İkinci veri tabanı konfigürasyonu için şöyle yaparız. Burada includeFilters alanı önemli.
@Configuration
@EnableJpaRepositories(
        basePackages = "...",
        includeFilters = @ComponentScan.Filter(ReadOnlyRepository.class),
        entityManagerFactoryRef = "readOnlyEntityManagerFactory"
)
public class ReadOnlyDataSourceConfiguration {


  @Bean
  public DataSource readDataSource() {
    ...
  }

  @Bean
  public LocalContainerEntityManagerFactoryBean readOnlyEntityManagerFactory() {
    ...
  }
}
Seçim için kullanılacak anotasyon şöyledir
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
public @interface ReadOnlyRepository {
}
Entity ve Repository sınıfları şöyledir
@Entity
@Table(name = "books")
@Data
public class Books {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  ...
}

@Repository
@ReadOnlyRepository
public interface BooksReadOnlyRepository extends JpaRepository<Books, Long> {
}

@Repository
public interface BooksReadWriteRepository extends JpaRepository<Books, Long> {
}
Bunları kullanmak için şöyle yaparız
@Repository
public class BooksDAO implements BooksReadOnlyRepository, BooksReadWriteRepository {
  private BooksReadOnlyRepository booksReadOnlyRepository;
  private BooksReadWriteRepository booksReadWriteRepository;

  @Autowired
  public BooksDAO(BooksReadOnlyRepository booksReadOnlyRepository,
BooksReadWriteRepository booksReadWriteRepository) {
    this.booksReadOnlyRepository = booksReadOnlyRepository;
    this.booksReadWriteRepository = booksReadWriteRepository;
  }

  public List<Books> getAllBooksFromMaster() {
    return booksReadWriteRepository.findAll();
  }

  public List<Books> getAllBooksFromSlave() {
    return booksReadOnlyRepository.findAll();
  }
  ...
}
Örnek
application.properties şöyle olsun. Burada bir bağlantı normal Spring kuralları ile yapılıyor. Test için olan JPA ise elle kodlanıyor
#DataSource
spring.test1.jdbc-url=jdbc:mysql://localhost:3306/test1
spring.test1.username=root
spring.test1.password=root@1234
spring.test1.driverClassName=com.mysql.cj.jdbc.Driver

spring.test2.jdbc-url=jdbc:mysql://localhost:3306/test2
spring.test2.username=root
spring.test2.password=root@1234
spring.test2.driverClassName=com.mysql.cj.jdbc.Driver

#JPA
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.jpa.properties.hibernate.format_sql=true
Test bağlantısı için şöyle yaparız
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(entityManagerFactoryRef = "personEntityManagerfactoryBean",
	transactionManagerRef = "personTransactionManager",
	basePackages = {"com.spring.boot.multipledatabase.repo.person"}
)
public class Test2DbConfig {
	
  @Bean(name = "test2DataSource")
  @ConfigurationProperties(prefix = "spring.test2")
  public DataSource test1Datasource() {
    return DataSourceBuilder.create().build();
  }
	
  @Bean(name = "personEntityManagerfactoryBean")
  public LocalContainerEntityManagerFactoryBean entityManagerfactoryBean(
    EntityManagerFactoryBuilder builder,
    @Qualifier("test2DataSource") DataSource dataSource) {
      return builder.dataSource(dataSource)
        .packages("com.spring.boot.multipledatabase.entity.person")
	.persistenceUnit("PERSON")
	.build();
  }
  @Bean(name = "personTransactionManager")
  public PlatformTransactionManager transactionManager(
    @Qualifier("personEntityManagerfactoryBean") 
    EntityManagerFactory entityManagerFactory) {
    return new JpaTransactionManager(entityManagerFactory);
  }
}
Örnek
application.properties şöyle olsun. Kodla iki tane JPA bağlantısı açacağız
#Primary database connection
spring.primary.datasource.url = jdbc:postgresql://localhost:5432/MultipleDbDemo
spring.primary.datasource.username = postgres
spring.primary.datasource.password = 1234

#Secondary database connection
spring.secondary.datasource.url = jdbc:sqlserver://localhost:1433;databaseName=MultipleDbDemoSqlServer;encrypt=true;trustServerCertificate=true
spring.secondary.datasource.username = sa
spring.secondary.datasource.password = 1234
	 #spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.SQLServerDialect

spring.jpa.hibernate.ddl-auto=update
spring.jpa.hibernate.show-sql=true
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation= true
spring.jpa.properties.javax.persistence.validation.mode = none


server.port:3000
Birinci bağlantı için şöyle yaparız. Burada önemli olan JPA modellerini farklı bir pakette toplamak
@Configuration
@EnableJpaRepositories(entityManagerFactoryRef = "primaryEntityManagerFactory", 
		transactionManagerRef = "primaryTransactionManager", 
		basePackages = {"merveozer.multipledb.primary.repository"})
public class PrimaryDatabaseConnection {

  @Value("${spring.primary.datasource.url}")
  private String url;
	
  @Value("${spring.primary.datasource.username}")
  private String username;
	
  @Value("${spring.primary.datasource.password}")
  private String password;
	
  @Primary
  @Bean(name="primaryDbDataSource")
  public DataSource primaryDbDataSource() {
    return DataSourceBuilder.create()
      .url(url).username(username).password(password).build();
  }
	
  @Primary
  @Bean(name = "primaryEntityManagerFactory")
  public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory(
    EntityManagerFactoryBuilder builder,
    @Qualifier("primaryDbDataSource") DataSource primaryDataSource) {
    return builder.dataSource(primaryDataSource)
      .packages("merveozer.multipledb.primary.model")
      .build();
  }

  @Primary
  @Bean(name = "primaryTransactionManager")
  public PlatformTransactionManager primaryTransactionManager(
    @Qualifier("primaryEntityManagerFactory") EntityManagerFactory
    primaryEntityManagerFactory) {
      return new JpaTransactionManager(primaryEntityManagerFactory);
    }
}
İkinci bağlantı için de şöyle yaparız. JPA modelleri yine farklı bir pakette.
@Configuration
@EnableJpaRepositories(entityManagerFactoryRef = "secondaryEntityManagerFactory", 
  transactionManagerRef = "secondaryTransactionManager", 
  basePackages = {"merveozer.multipledb.secondary.repository"})
public class SecondaryDatabaseConnection {
  @Value("${spring.secondary.datasource.url}")
  private String url;
	
  @Value("${spring.secondary.datasource.username}")
  private String username;
	
  @Value("${spring.secondary.datasource.password}")
  private String password;

  @Bean(name="secondaryDbDataSource")
  public DataSource secondaryDbDataSource() {
    return DataSourceBuilder.create().url(url)
      .username(username).password(password).build();
  }
	
  @Bean(name = "secondaryEntityManagerFactory")
  public LocalContainerEntityManagerFactoryBean secondaryEntityManagerFactory(
    @Qualifier("secondaryDbDataSource") DataSource secondaryDataSource, 
    EntityManagerFactoryBuilder builder) {
    return builder.dataSource(secondaryDataSource)
      .packages("merveozer.multipledb.secondary.model").build();
  }

  @Bean(name = "secondaryTransactionManager")
  public PlatformTransactionManager secondaryTransactionManager(
    @Qualifier("secondaryEntityManagerFactory") EntityManagerFactory
      secondaryEntityManagerFactory) {
      return new JpaTransactionManager(secondaryEntityManagerFactory);
  }
}