30 Kasım 2022 Çarşamba

JOOQ Kullanımı

Giriş
JOOQ Access Layer bağımlılığı eklenir. 

Maven Plugin
jooq-codegen plugin eklenir

Maven
Şu satırı dahil ederiz
<dependency>
<groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jooq</artifactId> </dependency> <dependency> <groupId>org.jooq</groupId> <artifactId>jooq</artifactId> <version>3.17.5</version> </dependency>
application.properties
Örnek
Şöyle yaparız
#PostgreSQL Database Configuration
datasource.driver= org.postgresql.Driver
datasource.jdbcUrl= jdbc:postgresql://localhost:5432/librarydb
datasource.username= dev
datasource.password= qwerty

# configure spring data source
spring.application.name=jooq-api
spring.datasource.driver-class-name=${datasource.driver}
spring.datasource.url = ${datasource.jdbcUrl}
spring.datasource.username = ${datasource.username}
spring.datasource.password = ${datasource.password}
spring.jooq.sql-dialect= postgres
JOOQ iki şekilde kullanılabilir
1. DAO
2. DSLContext 

1. DAO
Açıklaması şöyle. Yani her tablo için DAO üretilmiyor. DAO üretilmesi için code generation plugin'e ayar belirtmek lazım
jOOQ generates one DAO per UpdatableRecord, i.e. per table with a single-column primary key. Generated DAOs implement a common jOOQ type called org.jooq.DAO. With this, we don’t need our custom repository interface JOOQRepository.java because generated DAO classes implement various useful CRUD methods.
DAO sınıfı insert(), update(), findAll(), findById(), deleteById() metodlarını sağlar.

Örnek
Şöyle yaparız
RequiredArgsConstructor
@Service(value = "BookServiceDAO")
public class BookServiceImpl implements BookService {

  private final BookDao repository;

  @Override
  public Book create(Book book) {
    repository.insert(book);
    return book;
  }
  @Override
  public Book update(Book book) {
    repository.update(book);
    return book;
  }
  @Override
  public List<Book> getAll(){
    return repository.findAll();
  }
  @Override
  public Book getOne(int id) {
    Book book = repository.findById(id);
    if(book == null){
      throw new DataNotFoundException(
        MessageFormat.format("Book id {0} not found", String.valueOf(id)));
    }
    return book;
  }
  @Override
  public void deleteById(int id) {
    repository.deleteById(id);
  }
}

2. DSLContext Sınıfı

Örnek
Elimizde şöyle bir arayüz olsun
public interface JOOQRepository<T> {

  T save(T tablePojo);

  T update(T tablePojo, int id);

  List<T> findAll();

  Optional<T> findById(int id);

  boolean deleteById(int id);
}
Şöyle yaparız
import org.jooq.DSLContext;
import org.springframework.util.ObjectUtils;
import static com.example.springbootjooq.model.Tables.BOOK;

@RequiredArgsConstructor
@Transactional
@Repository
public class BookRepository implements JOOQRepository<Book> {

  private final DSLContext context;

  @Override
  public Book save(Book book){
    BookRecord bookRecord = context.insertInto(BOOK)
      set(BOOK.DESCRIPTION, book.getDescription())
      ...
      returning(BOOK.ID).fetchOne();

    if (bookRecord != null) {
      book.setId(bookRecord.getId());
      return book;
    }
    return null;
  }
  @Override
  public Book update(Book book, int id) {
    BookRecord bookRecord = context.update(BOOK)
      .set(BOOK.DESCRIPTION, book.getDescription())
      ...
      .where(BOOK.ID.eq(5))
      .returning(BOOK.ID).fetchOne();
    return (bookRecord != null) ? book : null;
  }
  @Override
  public List<Book> findAll(){
    return context
      .selectFrom(BOOK)
      .fetchInto(Book.class);
  }
  @Override
  public Optional<Book> findById(int id){
    Book b = context.selectFrom(BOOK).where(BOOK.ID.eq(id)).fetchOneInto(Book.class);
    return (ObjectUtils.isEmpty(b)) ? Optional.empty() : Optional.of(b);
  }
  @Override
  public boolean deleteById(int id) {
    return context.delete(BOOK)
      .where(BOOK.ID.eq(id))
      .execute() == 1;
  }
}





28 Kasım 2022 Pazartesi

CGLIB Kullanımı - Kullanmayın Deprecate Edildi

Giriş
Açıklaması şöyle
The cglib library is widely used and well-known in many frameworks, but it has been deprecated recently.
Açıklaması şöyle. Yani JDK ile gelen Proxy sınıfının çok benzeri. Ancak CGLIB final metodlar'ı intercept edemiyor.
Enhancer: Similar to the Proxy in the JDK way. It uses create() method to create a proxy instance on which you make a call

MethodInterceptor: Similar to InvocationHandler in the JDK way. The intercept() method will be called when you make a call on the proxy instance
Örnek
Elimizde şöyle bir Interceptor olsun
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class UserServiceCglibInterceptor implements MethodInterceptor {
  private Object realObj;

  public UserServiceCglibInterceptor(Object realObject) {
    super();
    this.realObj = realObject;
  }

  @Override
  public Object intercept(Object o, Method method, Object[] args, MethodProxy proxy) throws Throwable {
    Object result = null;
    if (args != null && args.length > 0 && args[0] instanceof User) {
      User user = (User)args[0];
      ...
      result = method.invoke(realObj, args);
    }
    return result;
  }
}
Şöyle yaparız
import org.springframework.cglib.proxy.Enhancer;

UserService target = new UserServiceImpl();
MethodInterceptor interceptor = new UserServiceCglibInterceptor(target);

Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(target.getClass());
enhancer.setCallback(interceptor);
UserService proxy = (UserService) enhancer.create();
proxy.addUser(user);



27 Kasım 2022 Pazar

SpringBootLogging Logstash Kullanımı

Maven
Şu satırı dahil ederiz
<dependency>
  <groupId>biz.paluch.logging</groupId>
  <artifactId>logstash-gelf</artifactId>
  <version>1.11.2</version>
</dependency>
Örnek - Log4J
Şöyle yaparız
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Liquid Technologies Online Tools 1.0 (https://www.liquid-technologies.com) -->
<Configuration status="WARN"
               monitorInterval="30"
               shutdownHook="disable">
  <Properties>
    <Property name="baseDir">$${env:HOME}/logs</Property>
    <Property name="applicationName">my-application</Property>
  </Properties>
  <Appenders>
    <RollingFile
       name="RollingFile"
       fileName="${baseDir}/${applicationName}.log"
       filePattern="${baseDir}/${applicationName}.%d{yyyy-MM-dd}-%i.log">
      <PatternLayout pattern="%-5p|%d{ISO8601}{GMT}|%X{token}|%c{1}|%X{Principal}|%m%ex%n" />
      <Policies>
        <OnStartupTriggeringPolicy />
        <SizeBasedTriggeringPolicy size="20 MB" />
        <TimeBasedTriggeringPolicy />
      </Policies>
      <DefaultRolloverStrategy max="10">
        <Delete basePath="${baseDir}">
          <IfFileName glob="${applicationName}.*.log">
            <IfAny>
              <IfAccumulatedFileSize exceeds="200 MB" />
              <IfAccumulatedFileCount exceeds="10" />
            </IfAny>
          </IfFileName>
        </Delete>
      </DefaultRolloverStrategy>
      <RegexFilter regex=".*@ConfigurationProperties.*"
                   onMatch="DENY"
                   onMismatch="ACCEPT" />
    </RollingFile>
    <Gelf name="gelf"
          host="tcp:${env:LOGSTASH_PROXY}"
          port="12201"
          version="1.0"
          extractStackTrace="true"
          filterStackTrace="true"
          mdcProfiling="true"
          includeFullMdc="true"
          maximumMessageSize="8192"
          originHost="%host"
          ignoreExceptions="true">
      <Field name="timestamp"
             pattern="%d{dd MMM yyyy HH:mm:ss,SSS}" />
      <Field name="level"
             pattern="%level" />
      <Field name="simpleClassName"
             pattern="%C{1}" />
      <Field name="className"
             pattern="%C" />
      <Field name="server.simple"
             pattern="%host{simple}" />
      <Field name="server.fqdn"
             pattern="%host{fqdn}" />
      <Field name="application"
             literal="${applicationName}" />
    </Gelf>
  </Appenders>
  <Loggers>
    <Root level="WARN">
      <AppenderRef ref="RollingFile" />
      <AppenderRef ref="gelf" />
    </Root>
    <Logger name="org.springframework"
            level="WARN" />
    <Logger name="com.my.app"
            level="INFO" />
  </Loggers>
</Configuration>
Açıklaması şöyle
In this file we are, in fact, defining two appenders: one sends the logs directly to the Elastic Stack and the other one is a typical file appender that rolls the files quite aggressively. Why? Because if the Elastic Stack fails for some reason, we can still access the logs, stored in the files, for some time, before they are overwritten.
Logstash alanları için açıklama şöyle
· Timestamp: essential

· Log level: essential

· Simple class name producing the log: I prefer it over the lengthy fully qualified name

· Class name: I send it, just in case, but I’ve never used it so I have it filtered out in Logstash.

· Hostname and simple hostname: The same as with class name, simple name is usually good enough and is shorter.

· Application name so I can filter easily and also so I can have different settings in Elastic Stack depending on the application

· Thanks to includeFullMdc=”true”, all the fields added to Log4J Mapped Diagnostic Context (MDC) will be added as fields to the log. This feature is very useful as, for example, it means that the token-per-request ID described in the entry “Spring Boot: Setting a unique ID per request” is added automatically, isn’t it cool?


SpringBootLogging application.properties - Basit logging Ayarları

Giriş
Basit loglama ayarlarını belirtmek için application.properties kullanır. Daha karmaşık şeyler için logback.xml kullanılabilir. Açıklaması şöyle
If you need to apply customizations to logback beyond those that can be achieved with application.properties, you’ll need to add a standard logback configuration file. You can add a logback.xml file to the root of your classpath for logback to find. You can also use logback-spring.xml if you want to use the Spring Boot Logback extensions.
Örneğin rolling file appender kullanmayı ben bulamadım bu yüzden logback.xml dosyasını src/main/resources altına ekledim.

Varsayılan Log Formatı
Açıklaması şöyle. Yani varsayılan çıktıda her satırda toplam 7 tane şey var.
Spring Boot preconfigures it with patterns and ANSI colors to make the standard output more readable.

The default log output from Spring Boot resembles the following example:

The following items are output:

I. Date and Time: Millisecond precision and easily sortable.
II. Log Level: ERROR, WARN, INFO, DEBUG, or TRACE.
III. Process ID.
IV. A — — separator to distinguish the start of actual log messages.
V. Thread name: Enclosed in square brackets (may be truncated for console output).
VI. Logger name: This is usually the source class name (often abbreviated).
VII. The log message.

The following table describes the mapping of log levels to colors: Level Color

FATAL: Red
ERROR: Red
WARN: Yellow
INFO: Green
DEBUG: Green
TRACE: Green
logging.pattern.console ve logging.pattern.file kullanılır

Örnek - Log Format Customization
Şöyle yaparız
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %logger{36} - %msg%n
Açıklaması şöyle
This pattern includes the timestamp, logger name (up to 36 characters), and the actual message.

logging.file Alanı - Log Dosyasının Yolu
Örnek
Şöyle yaparız
logging.file.path=.
Örnek
Şöyle yaparız
logging.file=/home/ubuntu/spring-boot-app.log
Örnek
Şöyle yaparız
logging.file.name=myapp.log
Örnek - Log File Rotation
Şöyle yaparız
logging.file.max-size=10MB
logging.file.max-history=10
logging.level Alanı 
Örnek - Root Loglama Seviyesini Ayarlamak
Şöyle yaparız
logging.level.root=DEBUG
Örnek - Loglama Patern'i Değiştirmek
Şöyle yaparız.
logging.level.root=info
logging.path=path
logging.file=${logging.path}/log.log
logging.pattern.file=
  %d{dd-MM-yyyy HH:mm:ss.SSS} [%thread] %-5level %logger{36}.%M - %msg%n
logging.pattern.console=
  %d{dd-MM-yyyy HH:mm:ss.SSS} [%thread] %-5level %logger{36}.%M - %n%highlight%msg%n
Spring Data Sınıfları
Örnek
Spring data sınıflarının loglama seviyesi için şöyle yaparız.
#logging
logging.level.org.springframework.data=debug
logging.level.=error
Örnek
Şöyle yaparız.
logging.level.org.springframework.security=ERROR
logging.level.com.test=DEBUG
Spring Web Sınıfları
Şöyle yaparız
logging.level.org.springframework.web=INFO

24 Kasım 2022 Perşembe

SpringAOP AopUtils Sınıfı

Giriş
Şu satırı dahil ederiz.
import org.springframework.aop.support.AopUtils;
isAopProxy metodu
Belirtilen nesne JDK dynamic proxy veya CGLIB proxy ise true döner

Örnek
Şöyle yaparız. Burada BatteryRepository bir JpaRepository nesnesi. Sonuç true olacaktır
@Autowired
private BatteryRepository batteryRepository;

@Test
void testBatteryServiceIsProxy() {
  boolean isProxy = AopUtils.isAopProxy(batteryRepository);
  Assertions.assertTrue(isProxy);
}

21 Kasım 2022 Pazartesi

SpringBoot CLI - Spring Initializr Yerine Kullanılabilir

Giriş
Spring Initializr ile yapılan şeyi komut satırından yapabilmesi sağlar. SpringShell'de daha farklı bir şeydir

--dependencies seçeneği
Örnek
Şöyle yaparız. Burada SDKMan ile önce kurulum yapılıyor
$ sdk install springboot
$ spring --version
Spring CLI v2.7.1

$ spring init --list

$ spring init --dependencies=web,data-jpa,postgresql --java-version=17 --build=gradle \ spring-boot-cli
Using service at https://start.spring.io
Project extracted to '/home/mohzen/dev/spring-boot-cli'
--name seçeneği
Proje ismi belirtilir.
Örnek
Şöyle yaparız
spring init --name=log4j2-demo --dependencies=web log4j2-demo





18 Kasım 2022 Cuma

Bean Lifecycle

1. Spring LifeCycle
Şeklen şöyle. Bu şekil SpringBoot değil, Spring bakış açısına sahip

1. Instantiate by container

2. Populate Attributes
Açıklaması şöyle. XML ile property set edilince kullanılır
Spring injects dependencies into the bean attributes according to BeanDefinition.
3. Inject Aware interfaces 
Açıklaması şöyle
Spring detects if the object implements any xxAware interfaces and injects the corresponding instances into the bean.
Sınıfım bazı Spring arayüzlerinden kalıtır. Örneğin şunlar. Aware arayüzleri arasında bir sıralama var sanırım

4. BeanPostProcessor
Açıklaması şöyle
If you want to customize some logic before the beans are actually used, you can implement it through the BeanPostProcessor interface, which mainly provides two methods:

- postProcessBeforeInitialization (Object bean, String beanName)
- postProcessAfterInitialization (Object bean, String beanName)
BeanPostProcessor sınıfından kalıtan ayrı bir sınıf yazılır. Bazı sınıflar hazır geliyor. Örneğin

5. InitializingBean and init-method
Açıklaması şöyle
InitializingBean interface only has one function:

afterPropertiesSet()
We can also configure init-method in bean to define the method name for execution.
Sınıfım  InitializingBean arayüzünü gerçekleştirir. Bence @PostConstruct gibi düşünülebilir.

6. BeanPostProcessor
postProcessAfterInitialization (Object bean, String beanName) metodu çağrılır
BeanPostProcessor sınıfından kalıtan ayrı bir sınıf yazılır

7. Ready for use
Bean artık hazırdır

8. DisposableBean and destroy-method
Bean artık yok edilecektir. Açıklaması şöyle. Sınıfım DisposableBean arayüzünü gerçekleştirilir
Same as init-method, we can use DisposableBean and destroy-method to appoint a method for destruction.
Örnek
Elimizde şöyle bir kod olsun.
public class PersonBean implements BeanNameAware, BeanFactoryAware, InitializingBean,
DisposableBean {
  private Integer id;
  private String name;

  public PersonBean(){
    System.out.println("1. I am alive!");
  }

  public Integer getId() {
    return id;
  }

  public void setId(Integer id) {
    this.id = id;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
    System.out.println("2. Populate attribute: my name is " + name);
  }

  @Override
  public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
    System.out.println("4. Invoking BeanFactoryAware#setBeanFactory method");
  }

  @Override
  public void setBeanName(String s) {
    System.out.println("3. Invoking BeanNameAware#setBeanName method");
  }

  public void init(){
    System.out.println("7. Customized init method");
  }

  public void myDestroy(){
    System.out.println("10. Customized destroy method");
  }

  @Override
  public void destroy() throws Exception {
    System.out.println("9. DisposableBean#destroy method");
  }

  @Override
  public void afterPropertiesSet() throws Exception {
    System.out.println("6. InitializingBean#afterPropertiesSet method");
  }
}
Ayrı bir BeanPostProcessor sınıfı olsun.
public class MyBeanPostProcessor implements BeanPostProcessor {

  @Override
  public Object postProcessBeforeInitialization(Object bean, String beanName)
  throws BeansException {
    System.out.println("5. BeanPostProcessor#postProcessBeforeInitialization method");
    return bean;
  }

  @Override
  public Object postProcessAfterInitialization(Object bean, String beanName)
  throws BeansException {
    System.out.println("8. BeanPostProcessor#postProcessAfterInitialization method");
    return bean;
  }
}
Elimizde şöyle bir XML olsun
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean name="myBeanPostProcessor" class="com.dntech.demo.config.MyBeanPostProcessor"/>
    <bean name="personBean" class="com.dntech.demo.dao.PersonBean"
          init-method="init" destroy-method="myDestroy">
        <property name="id" value="123456"/>
        <property name="name" value="dntech"/>
    </bean>
</beans>
public static void main(String[] args){
  ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
  PersonBean personBean = (PersonBean) context.getBean("personBean");
  ((ClassPathXmlApplicationContext) context).destroy();
}
Çıktı şöyle
1. I am alive!
2. Populate attribute: my name is dntech
3. Invoking BeanNameAware#setBeanName method
4. Invoking BeanFactoryAware#setBeanFactory method
5. BeanPostProcessor#postProcessBeforeInitialization method
6. InitializingBean#afterPropertiesSet method
7. Custom init method
8. BeanPostProcessor#postProcessAfterInitialization method
9. DisposableBean#destroy method
10. Customized destroy method
2. SpringBoot LifeCycle
Bir başka şekil şöyle. Bu şekil SpringBoot bakış açısına sahip. Burada Instantiate by container + Populate Attributes yerine Inject Dependencies denilmiş. Kasıt şu, constructor injection veya field injection varsa bunlar yapılır. Ayrıca

Bean yaratılırken @PostConstruct anostayonu initMethod'dan önce çağrılır. 
Örnek
Elimizde şöyle bir kod olsun
@Bean(initMethod = "initMethod", destroyMethod = "destroyMethod")
public Author author() {
  return new Author("Aplha, Engineer", "alphaengineer@gmail.com");
}

@Getter
@Setter
@NoArgsConstructor
public class Author implements BeanNameAware, BeanClassLoaderAware, 
  ApplicationContextAware, InitializingBean, DisposableBean {
 
 public Author(String fullName, String email) {
  System.out.println("constructor");
  ...
 }

 @PostConstruct
 public void init() {
  System.out.println("@PostConstruct annotated method");
 }

 @PreDestroy
 public void predestroy() {
  System.out.println("@PreDestroy annotated method");
 }

 @Override
 public void destroy() {
  System.out.println("destroy method from DisposableBean interface");
 }

 public void initMethod() {
  System.out.println("initMethod method");
 }

 public void destroyMethod() {
  System.out.println("destroyMethod method");
 }

 @Override
 public void setBeanName(String name) {
  System.out.println("setBeanName from BeanNameAware interface");
 }

 @Override
 public void setBeanClassLoader(ClassLoader classLoader) {
  System.out.println("setBeanClassLoader from BeanClassLoaderAware interface");
 }

 @Override
 public void setApplicationContext(ApplicationContext applicationContext) 
 throws BeansException {
  System.out.println("setApplicationContext from ApplicationContextAware interface");
 }

 @Override
 public void afterPropertiesSet() throws Exception {
  System.out.println("afterPropertiesSet from InitializingBean interface");
 }
}
Çıktı şöyle
constructor
setBeanName from BeanNameAware interface
setBeanClassLoader from BeanClassLoaderAware interface
setApplicationContext method ApplicationContextAware interface @PostConstruct annotated method afterPropertiesSet method InitializingBean interface initMethod method ... @PreDestroy annotated method destroy method from DisposableBean interface destroyMethod method





13 Kasım 2022 Pazar

Multi-staged Docker + layertools ve SpringBoot

Giriş
Burada amaç üretilen fat jar'ı katmanlara bölmek ve bunları layer olarak Docker image'a kopyalamak. Açıklaması şöyle
This uber jar can easily reach 400MB or more. This means that for every new build, even for the simplest code change in our application:

- A new 400MB layer will be created
- The layer will be uploaded to your OCI registry
- When Kubernetes pulls the latest image to the node that runs the container, the entire 400 MB layer will need to be pulled.

This will happen on every single code change and rebuild of the image. In reality, only a very small sliver of compiled code has changed. Dockerfile treats each new line as a new layer, so it would make a lot more sense to put the third-party dependencies in their own layer and have our custom code in its own layer. Spring boot provides a way to explode the jar into layers from 2.3 onwards.
Fat jar'ı katmanlara bölmek için komut şöyle. Eğer sadece listelemek istersek extract yerine list yaparız
RUN java -Djarmode=layertools -jar /home/app/target/*.jar extract
Ortaya çıkan layer'lar şöyle
Dependencies — the Spring Boot and other frameworks’ release-based dependencies. These only change when we upgrade to a Spring Boot version or the third-party framework version.
Spring Boot Loader — this is the code that loads our Spring Boot app into the JVM to manage the bean lifecycle, among other things. This also rarely changes.
Snapshot Dependencies — these dependencies are changing much more often. It’s possible that on each new build it would require us to pull the latest snapshot. As a result, this layer is the closest to our application code.
Application — This is our application code from src/main/java (in the case of maven).
Eğer custom layer yaratmak istersek layers.xml dosyası tanımlarız. Şöyle yaparız
<layers xmlns="http://www.springframework.org/schema/boot/layers" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
  https://www.springframework.org/schema/boot/layers/layers-2.7.xsd">
  <application>
    <into layer="spring-boot-loader">
      <include>org/springframework/boot/loader/**</include>
    </into>
    <into layer="application" />
  </application>
  <dependencies>
    <into layer="snapshot-dependencies">
      <include>*:*:*SNAPSHOT</include>
    </into>
    <into layer="custom-dependencies">
      <include>com.company:*</include>
    </into>
    <into layer="dependencies" />
  </dependencies>
  <layerOrder>
    <layer>dependencies</layer>
    <layer>spring-boot-loader</layer>
    <layer>snapshot-dependencies</layer>
    <layer>custom-dependencies</layer>
    <layer>application</layer>
  </layerOrder>
</layers>
Şöyle yaparız
<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <configuration>
    <excludes>
      <exclude>
        <groupId>org.projectlombok</groupId>
        <artifacdId>lombok</artifacdId>
      </exclude>
    </excludes>
    <layers>
      <enabled>true</enabled>
      <configuration>${project.basedir}/src/layers.xml</configuration>
    </layers>
  </configuration>
</plugin>
Örnek
Şöyle yaparız
#Multi Stage Docker Build

#STAGE 1 - Build the layered jar
#Use Maven base image 
FROM maven-3.8.3-openjdk-17 AS builder
COPY src /home/app/src
COPY pom.xml /home/app
#Build an uber jar
RUN mvn -f /home/app/pom.xml package
WORKDIR /home/app/target
#Extract the uber jar into layers
RUN java -Djarmode=layertools -jar /home/app/target/*.jar extract

#STAGE 2 - Use the layered jar to run Spring Boot app
#Use OpenJDK17 base image
FROM openjdk:17-alpine
USER root
#Copy individual layers one by one 
COPY --from=builder /home/app/target/dependencies/ ./
#Add this to fix a bug which happens during sequential copy commands
RUN true
COPY --from=builder /home/app/target/spring-boot-loader/ ./
RUN true
COPY --from=builder /home/app/target/snapshot-dependencies/ ./
RUN true
COPY --from=builder /home/app/target/custom-dependencies/ ./
RUN true
COPY --from=builder /home/app/target/application/ ./
#Expose port on which Spring Boot app will run
EXPOSE 8080
#Switch to non root user
USER 1001
#Start Spring Boot app
ENTRYPOINT ["java","org.springframework.boot.loader.JarLauncher"]
Örnek - adoptopenjdk/maven-openjdk11
Şöyle yaparız
# STAGE 1
FROM adoptopenjdk/maven-openjdk11 as builder
WORKDIR application
COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
COPY src src
RUN ./mvnw install -DskipTests
RUN java -Djarmode=layertools -jar target/rest-service-0.0.1-SNAPSHOT.jar extract

# STAGE 2
FROM adoptopenjdk:11-jre-hotspot
WORKDIR application
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
Açıklaması şöyle. Bu yöntemin özelliği Docker cache özelliğinin de devreye girmesiyle, derleme zamanının çok hızlanması
This approach uses a maven image in Stage 1. Therefore, you do not need to have maven installed on your machine to run the maven build commands.

This approach uses a jre base image in Stage 2. Furthermore it uses layers as opposed to a fat jar.

There are four layers consisting of the following:
  • java dependencies
  • Spring Boot loader
  • snapshot dependencies
  • application code

These four dependencies are organized in order of least likely to change. Naturally, the application code is ordered last since it changes the most often. Therefore, when application code changes, only the java files that have changed need to be compiled and added to the new image during a build. The docker build engine can use the cached layers created from previous build to reuse the other layers. 
Örnek -  openjdk:17-alpine
Şöyle yaparız. Burada dockerfile-maven-plugin kullanılıyor.  maven install komutu ile önce jar yapılandırılıyor. En son olarak ta Docker dosyası çalıştırılıyor
FROM openjdk:17-alpine as builder

WORKDIR application

ARG JAR_FILE=target/*.jar

COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract

FROM openjdk:17-alpine
WORKDIR application
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
Örnek - eclipse-temurin:17.0.4_8-jdk-alpine
Şöyle yaparız
# Making here use of a docker multi-stage build
# https://docs.docker.com/develop/develop-images/multistage-build/

# Build-time container
FROM eclipse-temurin:17.0.4_8-jdk-alpine as builder
ARG JAR_FILE
WORKDIR application
COPY $JAR_FILE application.jar
COPY build_application.sh ./
RUN sh build_application.sh

# Run-time container
FROM alpine:3.16.2

ARG APP_NAME=app
ARG USER=exie
ARG GROUP=party
ARG LOG_FOLDER=/srv/app/logs

ENV APP=$APP_NAME \
    JAVA_HOME=/opt/java \
    PATH="${JAVA_HOME}/bin:${PATH}"

## Adding programs for operation
RUN apk update && \
    apk --no-cache add dumb-init curl jq jattach && \
    rm -rf /var/cache/apk/*

WORKDIR /srv/$APP

COPY run_application.sh /etc

RUN addgroup -S $GROUP && \
    adduser -S -D -H $USER -G $GROUP && \
    mkdir -p $LOG_FOLDER && \
    chgrp $GROUP $LOG_FOLDER && \
    chmod g+rwx $LOG_FOLDER && \
    chmod +x /etc/run_application.sh

## Application-specif created JRE
COPY --from=builder /opt/java-runtime $JAVA_HOME

## Spring Boot Layers
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./

USER $USER
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
CMD ["/etc/run_application.sh"]
build.sh şöyle
#!/bin/sh

JAR_FILE=application.jar
if [ ! -f "$JAR_FILE" ]; then
    echo "build.error: application jar is missing!"
    exit 1
fi

jar xf application.jar
REQUIRED_JAVA_MODULES="$(jdeps \
                            --print-module-deps \
                            --ignore-missing-deps \
                            --recursive \
                            --multi-release 17 \
                            --class-path="./BOOT-INF/lib/*" \
                            --module-path="./BOOT-INF/lib/*" \
                            ./application.jar)"

if [ -z "$REQUIRED_JAVA_MODULES" ]; then
    echo "build.error: required java modules are not determined!"
    exit 1
fi

jlink \
  --no-header-files \
  --no-man-pages \
  --compress 2 \
  --add-modules "${REQUIRED_JAVA_MODULES}" \
  --output /opt/java-runtime

java -Djarmode=layertools -jar application.jar extract

exit 0
run_application.sh şöyle
#!/bin/sh

JAVA_OPTS="-Dfile.encoding=UTF-8 -Duser.timezone=UTC -XX:NativeMemoryTracking=summary -XX:+HeapDumpOnOutOfMemoryError"
PORT="8080"

cd /srv/"$APP" || exit
/opt/java/bin/java $JAVA_OPTS org.springframework.boot.loader.JarLauncher --bind 0.0.0.0:$PORT