2025/03/17

SpringBoot3 - Spring Boot 啟動過程

啟動入口

@SpringBootApplication
public class Demo1Application {

    public static void main(String[] args) {
        SpringApplication.run(Demo1Application.class);
    }

}

@SpringBootApplication 裡面就包含了

  • @SpringBootConfiguration

  • @EnableAutoConfiguration

    啟用應用的自動設定功能

  • @ComponentScan

    自動掃描 sub packages 裡面的 Spring Component


啟動 application

SpringApplication.run(Demo1Application.class);

也可以拆開

SpringApplication springApplication = new SpringApplication(Demo1Application.class);
springApplication.run(args);

也可以用 SpringApplicationBuilder

new SpringApplicationBuilder()
    .sources(Parent.class)
    .child(Demo1Application.class)
    .banerMode(Banner.Mode.OFF)
    .run(args);

jar 啟動

application 的 main 通常是在 console 啟動時被呼叫,如果打包成 jar,就會是另一種狀況

project 可用 mvn package 打包成 jar

|____org
| |____springframework
| | |____boot
| | | |____loader
| | | | |____ref
| | | | |____net
| | | | |____jarmode
| | | | |____launch
| | | | |____jar
| | | | |____zip
| | | | |____nio
| | | | |____log
|____META-INF
| |____MANIFEST.MF
| |____maven
| | |____tw.com.test
| | | |____demo1
| | | | |____pom.xml
| | | | |____pom.properties
| |____services
| | |____java.nio.file.spi.FileSystemProvider
|____BOOT-INF
| |____classes
| | |____tw
| | | |____com
| | | | |____test
| | | | | |____demo1
| | | | | | |____Demo1Application.class
| | |____application.yml
| |____layers.idx
| |____classpath.idx
| |____lib
  • BOOT-INF: 啟動 application 所需要的 classes

  • META-INF: application打包的相關描述檔案

  • org: spring boot 啟動所需要的引導類別


META-INF/MENIFEST.MF

Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.4.2
Build-Jdk-Spec: 17
Implementation-Title: demo1
Implementation-Version: 0.0.1-SNAPSHOT
Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: tw.com.test.demo1.Demo1Application
Spring-Boot-Version: 3.3.5
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx

Main-Class: 啟動的引導類別

Start-Class: application 啟動類別


啟動的 log 可透過設定關閉

spring:
  main:
    log-startup-info: false

或是

SpringApplication springApplication = new SpringApplication(Demo1Application.class);
springApplication.setLogStartupInfo(false);
springApplication.run(args);

啟動失敗分析

由 FailureAnalyzer interface 處理

ex: PortInUseFailureAnalyzer 可處理重複使用 TCP Port 的 exception

可以自訂一個 PortInUseFailureAnalyzer

package tw.com.test.demo1;

import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.boot.web.server.PortInUseException;

public class PortInUseFailureAnalyzer extends AbstractFailureAnalyzer<PortInUseException> {
    @Override
    protected FailureAnalysis analyze(Throwable rootFailure, PortInUseException cause) {
        return new FailureAnalysis(cause.getPort()+"PortInUse", "check "+cause.getPort(), cause);
    }
}

然後在 META-INF/spring.factories

org.springframework.boot.diagnostics.FailureAnalyzer=\
  tw.com.test.demo1.PortInUseFailureAnalyzer

也可以自訂自己的 applicaton exception,以及對應的 FailureAnalyzer

啟動 Banner

Text to ASCII Art Generator (TAAG) 這是將 text 轉換為 ascii 文字的網頁工具。

將要列印的 application 文字,轉換為 ascii 後,儲存到 /resources/banner.txt。啟動 application 時,就會看到 banner 已經換掉了。

以下是一些相關設定

# banner.txt encoding
spring.banner.charset=UTF-8
# banner.txt filepath
spring.banner.location=classpath:banner.txt
# console/log/off
spring.main.banner-mode=console

啟動事件與 listener

spring-boot-3.3.5.jar

META-INF/spring.factories 裡面註冊的 listener,這些都實作了 Spring 的 ApplicationListener interface

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.env.EnvironmentPostProcessorApplicationListener

啟動事件的順序

  1. ApplicationStartingEvent

    spring application 啟動前發送

  2. ApplicationEnvironmentPreparedEvent

    已知要在 context 使用 spring 的 Environment 時,在 context 建立之前發送

  3. ApplicationContextInitializedEvent

    ApplicationContext 準備好了,ApplicationContextInitializer 已被呼叫,在 bean 載入前發送

  4. ApplicationPreparedEvent

    在 context refresh 前,且在 bean 定義被載入後發送

  5. ApplicationStartedEvent

    在 Spring context refresh 以後,在 application / CLI runner 呼叫前發送

  6. AvailabilityChangeEvent

    跟著上一個事件後發送,ReadinessState.CORRECT,代表 application 已處於活動狀態

  7. ApplicationReadyEvent

    在 application / CLI runner 呼叫後發送

  8. AvailabilityChangeEvent

    跟著上一個事件後發送,ReadinessState.ACCEPTING_TRAFFIC,代表 application 已經準備接收 request

  9. ApplicationFailedEvent

    在啟動異常時發送

以上只有 SpringApplication 發出的 SpringApplicationEvents 事件。

以下事件會在 ApplicationPreparedEvent 後,ApplicationStartedEvent 前發送

  1. WebServerInitializedEvent

    WebServer 啟動後發送。包含 ServletWebServerInitialziedEvent, ReactiveWebServerInitializedEvent

  2. ContextRefreshedEvent

    在 ApplicationContext refresh 後發送


自訂事件 listener

Demo1ApplicationListener.java

package tw.com.test.demo1;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.boot.availability.ReadinessState;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class Demo1ApplicationListener implements ApplicationListener<AvailabilityChangeEvent> {
    @Override
    public void onApplicationEvent(AvailabilityChangeEvent event) {
        log.info("got event: {}, state: {}", event, event.getState());
        if( ReadinessState.ACCEPTING_TRAFFIC == event.getState() ) {
            log.info("applcation is started!");
        }
    }
}

啟動測試

2024-11-15T16:26:43.269+08:00  INFO 4595 --- [           main] t.c.test.demo1.Demo1ApplicationListener  : got event: org.springframework.boot.availability.AvailabilityChangeEvent[source=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@5674e1f2, started on Fri Nov 15 16:26:40 CST 2024], state: CORRECT
2024-11-15T16:26:43.273+08:00  INFO 4595 --- [           main] t.c.test.demo1.Demo1ApplicationListener  : got event: org.springframework.boot.availability.AvailabilityChangeEvent[source=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@5674e1f2, started on Fri Nov 15 16:26:40 CST 2024], state: ACCEPTING_TRAFFIC
2024-11-15T16:26:43.273+08:00  INFO 4595 --- [           main] t.c.test.demo1.Demo1ApplicationListener  : applcation is started!

Runner

在 application 啟動前,執行一些 code

  • ApplicationRunner

  • CommandLineRunner

@Component 或是 lambda 的 @Bean兩種寫法

package tw.com.test.demo1;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

@SpringBootApplication
@Slf4j
public class Demo1Application {

    public static void main(String[] args) {
        SpringApplication.run(Demo1Application.class);
    }

    @Component
    public class Demo1ApplicationRunner implements ApplicationRunner {
        @Override
        public void run(ApplicationArguments args) throws Exception {
            log.info("ApplicationRunner runner args={}", args);
        }
    }
//    @Bean
//    public ApplicationRunner applicationRunner() {
//        return new Demo1ApplicationRunner();
//    }

    @Bean
    public ApplicationRunner applicationRunner2() {
        return (args) -> {
            log.info("CommandLineRunner2 args={}", args);
        };
    }

    @Bean
    public CommandLineRunner commandLineRunner() {
        return (args) -> {
            log.info("CommandLineRunner args={}", args);
        };
    }
}

執行結果

2024-11-15T16:57:46.247+08:00  INFO 4793 --- [           main] tw.com.test.demo1.Demo1Application       : CommandLineRunner args={}
2024-11-15T16:57:46.248+08:00  INFO 4793 --- [           main] tw.com.test.demo1.Demo1Application       : CommandLineRunner2 args=org.springframework.boot.DefaultApplicationArguments@d535a3d
2024-11-15T16:57:46.249+08:00  INFO 4793 --- [           main] tw.com.test.demo1.Demo1Application       : ApplicationRunner runner args=org.springframework.boot.DefaultApplicationArguments@d535a3d

2025/03/10

SpringBoot3 - Spring Boot Starter

starter 包含 自動設定及相關 library

官方都是以 spring-boot-starter-* 命名。第三方要以 *-spring-boot-starter 命名,ex: druid-spring-boot-starter

分類

application

name desc
spring-boot-starter 核心 starter,含自動設定、log 及支援 YAML
spring-boot-starter-amqp Spring AMQP, Rabbit MQ
spring-boot-starter-aop Spring AOP
spring-boot-starter-artemis Apache Artemis,支援 JMS 的 MQ
spring-boot-starter-batch Spring Batch
spring-boot-starter-cache Spring Cache
spring-boot-starter-data-cassandra Cassandra + Spring Data Cassandra
spring-boot-starter-data-cassandra-reactive Cassandra + Spring Data Cassandra Reactive
spring-boot-starter-data-couchbase Couchbase + Spring Data Couchbase
spring-boot-starter-data-couchbase-reactive Couchbase + Spring Data Couchbase Reactive
spring-boot-starter-data-elasticsearch Elasticsearch + Spring Data Elasticsearch
spring-boot-starter-data-jdbc Spring Data JDBC
spring-boot-starter-data-jpa Spring Data JPA + Hibernate
spring-boot-starter-data-ldap Spring Data LDAP
spring-boot-starter-data-mongodb MongoDB + Spring Data MongoDB
spring-boot-starter-data-mongodb-reactive MongoDB + Spring Data MongoDB Reactive
spring-boot-starter-data-neo4j Neo4J + Spring Data Neo4J
spring-boot-starter-data-r2dbc Spring Data R2DBC
spring-boot-starter-data-redis Redis + Spring Data Redis + Lettuce
spring-boot-starter-data-redis-reactive Redis + Spring Data Redis Reactive + Lettuce Client
spring-boot-starter-data-rest Spring Data REST + Spring Data repositories,輸出 REST
spring-boot-starter-freemarker 以 FreeMarker View 建立 Spring Web application
spring-boot-starter-graphql Spring GraphQL
spring-boot-starter-grovvy-templates Groovy View 建立 Spring Web application
spring-boot-starter-hateoas Spring MVC + Spring HATEOAS 建立 RESTful Web application
spring-boot-starter-integration Spring Integration
spring-boot-starter-jdbc JDBC + HikariCP connection pool
spring-boot-starter-jersey JAX-RS + Jersey 建立 RESTful Web application,可替代 spring-boot-starter-web
spring-boot-starter-jooq jOOQ 存取 SQL database。可替代 spring-boot-starter-data-jpa 或 spring-boot-starter-jdbc
spring-boot-starter-json 讀寫 JSON
spring-boot-starter-mail Java Mail + Spring Mail Sender
spring-boot-starter-mustache 以 Mustache view 建立 Web Application
spring-boot-starter-oauth2-client Spring Security's OAuth2/OpenID 客戶端連線
spring-boot-starter-oauth2-resource-server Spring Security's OAuth2 資源伺服器
spring-boot-starter-quartz Quartz
spring-boot-starter-rsocket RSocket client and server
spring-boot-starter-security Spring Security
spring-boot-starter-test JUnit Jupiter + Hamcrest + Mockito
spring-boot-starter-thymeleaf Thymeleaf View 建立 MVC web application
spring-boot-starter-validation Java Bean Validation + Hibernate Validator
spring-boot-starter-web Spring MVC 建立 RESTful Web application,以 Tomcat 為內嵌伺服器
spring-boot-starter-web-services Spring Web Services
spring-boot-starter-webflux Spring Reactive Web 建立 WebFlux application
spring-boot-starter-websocket Spring WebSocket

如果官方沒有的 starter,可使用第三方自制的 Spring Boot Starter,ex: Dubbo, ZooKeeper, MyBatis

production

name desc
spring-boot-starter-actuator Sprign Boot Actuator,正式環境的監控與應用管理

technical

可排除或替換預設的技術套件

name desc
spring-boot-starter-jetty 以 Jetty 為 servlet container,替代 spring-boot-starter-tomcat
spring-boot-starter-log4j2 log4j2,替代 spring-boot-starter-logging
spring-boot-starter-logging Logback
spring-boot-starter-reactor-netty 以 Netty 作為內嵌的 reactive http server
spring-boot-starter-tomcat 以 Tomcat 為內嵌的 servlet container,這是預設的,也被用在 spring-boot-starter-web
spring-boot-starter-undertow 以 Undertow 作為內嵌的 servlet container,可替代 spring-boot-starter-tomcat

ex: 使用 Jetty 替代 tomcat

修改pom.xml

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <!-- exclude tomcat -->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jetty</artifactId>
            <!-- <artifactId>spring-boot-starter-undertow</artifactId> -->
        </dependency>

自動設定

所有自動設定的類別都是由 spring-boot-autoconfigure 模組提供的

ref: # 深入理解自動配置原理之@SpringApplcation


MailSender

spring-boot-start-mail 提供了

  • org.springframework.mail.javamail.JavaMailSender

  • org.springframework.mail.javamail.JavaMailSenderImpl

另外有一個自動設定類別

  • org.springframework.boot.autoconfigure.mail.MailSnederAutoConfiguration

該類別被註冊到這個檔案裡面,檔案內容就是自動設定類別的字串

/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

pom.xml

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>

application.yml

spring:
  mail:
    host: smtp.gmail.com
    username: service@larzio.com
    password: XXXXXX
    properties:
      "[mail.smtp.socketFactory.class]": javax.net.ssl.SSLSocketFactory
      "[mail.smtp.socketFactory.fallback]": false
      "[mail.smtp.socketFactory.port]": 465
      "[mail.smtp.connectiontimeout]": 5000
      "[mail.smtp.timeout]": 3000
      "[mail.smtp.writetimeout]": 5000

mail:
  from: service@test.com
  fromname: service
  bcc:
  subject: Spring Boot Mail Test

Demo1Application.java

package tw.com.test.demo1;

import lombok.RequiredArgsConstructor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.web.bind.annotation.RestController;

@EnableConfigurationProperties({MailProperties.class})
@RequiredArgsConstructor
@SpringBootApplication
@RestController
public class Demo1Application {
    public static void main(String[] args) {
        SpringApplication.run(Demo1Application.class);
    }
}

MailProperties.java

package tw.com.test.demo1;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Data
@ConfigurationProperties(prefix = "mail")
public class MailProperties {
    private String from;
    private String fromname;
    private String bcc;
    private String subject;

}

EmailController.java

package tw.com.test.demo1;

import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.io.UnsupportedEncodingException;

@Slf4j
@RequiredArgsConstructor
@RestController
public class EmailController {
    private final JavaMailSender javaMailSender;
    private final MailProperties mailProperties;

    @RequestMapping("/sendSimpleEmail")
    @ResponseBody
    public boolean sendSimpleEmail(@RequestParam("email") String email, @RequestParam("text") String text) {
        try {
            SimpleMailMessage msg = createSimpleMsg(email, text);
            javaMailSender.send(msg);
        } catch (Exception ex) {
            log.error("Error:", ex);
            return false;
        }
        return true;
    }

    @RequestMapping("/sendMimeEmail")
    @ResponseBody
    public boolean sendMimeEmail(@RequestParam("email") String email, @RequestParam("text") String text) {
        try {
            MimeMessage msg = createMimeMsg(email, text, "java.png");
            javaMailSender.send(msg);
        } catch (Exception ex) {
            log.error("Error:", ex);
            return false;
        }
        return true;
    }

    /**
     * @param email
     * @param text
     * @param attachmentClassPathFilename
     * @return
     * @throws MessagingException
     * @throws UnsupportedEncodingException
     */
    private MimeMessage createMimeMsg(String email, String text, String attachmentClassPathFilename) throws MessagingException, UnsupportedEncodingException {
        MimeMessage msg = javaMailSender.createMimeMessage();
        MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(msg, true);
        mimeMessageHelper.setFrom(mailProperties.getFrom(), mailProperties.getFromname());
        mimeMessageHelper.setTo(email);
        if (!mailProperties.getBcc().equals("")) {
            mimeMessageHelper.setBcc(mailProperties.getBcc());
        }
        mimeMessageHelper.setSubject(mailProperties.getSubject());
        mimeMessageHelper.setText(text);
        mimeMessageHelper.addAttachment(attachmentClassPathFilename, new ClassPathResource(attachmentClassPathFilename));
        return msg;
    }

    /**
     * @param email
     * @param text
     * @return
     */
    private SimpleMailMessage createSimpleMsg(String email, String text) {
        SimpleMailMessage msg = new SimpleMailMessage();
        msg.setFrom(mailProperties.getFrom());
        msg.setTo(email);
        if (!mailProperties.getBcc().equals("")) {
            msg.setBcc(mailProperties.getBcc());
        }
        msg.setSubject(mailProperties.getSubject());
        msg.setText(text);
        return msg;
    }

}

測試

http://localhost:8080/sendMimeEmail?email=charley@maxkit.com.tw&text=hello

http://localhost:8080/sendSimpleEmail?email=charley@maxkit.com.tw&text=hello

2025/03/03

SpringBoot3 Configuration

Spring Boot 3 的相關設定

設定類別

spring 3.0 以前,都是使用 XML 設定檔。3.0 以後,是透過 @Configuration 註解的類別做設定。

@SpringBootApplication 裡面包含了 @SpringBootConfiguration,該設定可代替 @Configuration

example: @Bean 就類似以前 xml 的 <bean>

@SpringBootConfiguration
public class MainConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

SprintBoot 可使用一個 @SpringBootConfiguration或是 @Configuration 類別做設定,但也可以區分不同功能的設定,然後再使用 @Import 彙整在一起。

@SpringBootConfiguration
@Import({Config1.class, Config2.class})
public class MainConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

但如果設定類別都在類別掃描路徑上,可以用 @ComponentScan 掃描所有 packages。因 @SpringBootConfiguration 包含了 @ComponentScan,Application 以下的所有 packages。


也可以用 @ImportResource 匯入 XML 設定檔

設定檔

Spring Boot 的主要設定檔在 application.properties

如果有使用 Spring Cloud,有另一個 bootstrap.properties,該設定檔的優先權比 application.properties 高。

  • bootstrap 由 ApplicationContext 載入,優先權高

  • bootstrap 的設定不能被覆寫

設定檔可以用 .properties 或是 .yml (yaml 格式)

server.port = 8090
server.servlet.contextPath = /test

可使用 @PropertySource 匯入

.yml

server:
  port: 8090
  servlet:
    contextPath: /test

可用 YamlPropertiesFactoryBean 轉換為 Properties,或是用 YamlMapFactoryBean 轉換為 Map。也可以用 YamlPropertySourceLoader 在入為 PropertySource


@ConfigurationProperties 支援兩種格式的設定檔


設定綁定

所有已載入 Spring 的設定,都可以用 Environment 取得

@Autowired
private Environment env;

//
String getProperties(String key);

使用 @Value

使用 @Value

  1. ${ property : default_value }

  2. #{ obj.property ?: default_value }

example:

application.properties

jdbc.driverClass=com.mysql

在類別中,可用以下方式取得

    @Value("${jdbc.driverClass}")
    private String driver;

也可以用 @PropertySource

@Data
@Component
@PropertySource(value={"/config/db.properties"})
public class DBProperties {
    @Value("${jdbc.driverClass}")
    private String driverClass;
}

映射到類別、Constructor

可直接以 @ConfigurationProperties 將設定映射到一個類別

ex:

application.yaml

demo1:
  name: test
  users:
    - user1
    - user2
  params:
    place : userplace

demo2:
  name: test2
  age: 11
  birthday: 2024/11/11 11:11:11

demo3:
  name: test3
  age: 22

Demo1.properties

package tw.com.test.demo1;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.util.List;
import java.util.Map;

@Data
@ConfigurationProperties(prefix="demo1")
public class Demo1Properties {
    private String name;
    private List<String> users;
    private Map<String, String> params;
}

Demo2Properties.java

package tw.com.test.demo1;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.ConstructorBinding;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.format.annotation.DateTimeFormat;

import java.util.Date;

@Data
@NoArgsConstructor
@ConfigurationProperties(prefix="demo2")
public class Demo2Properties {
    private String name;
    private int age;
    private Date birthday;

    @ConstructorBinding
    public Demo2Properties(String name,
                           @DefaultValue("1") int age,
//                           @DateTimeFormat(pattern = "yyyy/MM/dd HH:mm:ss") Date birthday,
                           @JsonFormat(pattern = "yyyy/MM/dd HH:mm:ss", timezone="GMT+8") Date birthday) {
        this.name = name;
        this.age = age;
        this.birthday = birthday;
    }
}

Demo1Application.java

package tw.com.test.demo1;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
@RequiredArgsConstructor
@EnableConfigurationProperties(value = {Demo1Properties.class, Demo2Properties.class})
@Slf4j
public class Demo1Application {

    private final Demo1Properties demo1Properties;
    private final Demo2Properties demo2Properties;

    public static void main(String[] args) {
        SpringApplication.run(Demo1Application.class, args);
    }

    @Bean
    public CommandLineRunner commandLineRunner() {
        return (args) -> {
            log.info("demo1 properties: {}", demo1Properties);
            log.info("demo2 properties: {}", demo2Properties);
        };
    }
}

CommandLineRunner 的用途是,在 CLI 啟動 application 後,會被呼叫的 method

執行結果

demo1 properties: Demo1Properties(name=test, users=[user1, user2], params={place=userplace})
demo2 properties: Demo2Properties(name=test2, age=11, birthday=Mon Nov 11 11:11:11 CST 2024)

Bean 綁定

@ConfigurationProperties 除了用在 class,也可以用在 @Bean

MainConfig.java

@SpringBootConfiguration
public class MainConfig {
    @Bean
    @ConfigurationProperties(prefix="demo3")
    public Demo3Properties demo3Properties() {
        return new Demo3Properties();
    }
}

Demo3Properties.java

package tw.com.test.demo1;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class Demo3Properties {
    private String name;
    private int age;
}

Demo1Application 不需要寫在 @EnableConfigurationProperties

@SpringBootApplication
@RequiredArgsConstructor
@EnableConfigurationProperties(value = {Demo1Properties.class, Demo2Properties.class})
@Slf4j
public class Demo1Application {

    private final Demo1Properties demo1Properties;
    private final Demo2Properties demo2Properties;
    private final Demo3Properties demo3Properties;

    public static void main(String[] args) {
        SpringApplication.run(Demo1Application.class, args);
    }

    @Bean
    public CommandLineRunner commandLineRunner() {
        return (args) -> {
            log.info("demo1 properties: {}", demo1Properties);
            log.info("demo2 properties: {}", demo2Properties);
            log.info("demo3 properties: {}", demo3Properties);
        };
    }
}

執行結果

demo3 properties: Demo3Properties(name=test3, age=22)

設定類別掃描

在 Application 加上 @ConfigurationPropertiesScan 可掃描該 package 以下的所有設定類別,如果要掃描特定package,就加上 basePackages

@SpringBootApplicaiton
@RequiredArgsConstructor
@ConfigurationPropertiesScan
public class Application {

}

設定驗證

使用 @ConfigurationProperties 可利用 JSR-303 javax.validation 驗證設定值

pom.xml 要加上套件

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
</dependency>

在設定欄位上加上 @NotNull

@Data
@Validated
@NoArgsConstructor
@ConfigurationProperties(prefix="demo2")
public class Demo2Properties {
    @NotNull
    private String name;

    private int age;
    private Date birthday;

外部設定

設定來源

  • properties

  • YAML

  • 環境變數

  • CLI 參數

設定的優先等級

由低到高

  1. 預設參數 SpringApplication.setDefaultProperties

  2. @PropertySource 綁定的設定

  3. 套用 application 檔案的參數

  4. 設定了 random.* 的參數

  5. 系統環境變數

  6. Java System Properties

  7. java:comp/env 的 JNDI 參數

  8. ServletContext 初始化參數

  9. ServletConfig 初始化參數

  10. 來自 SPRING_APPLICATION_JSON 的參數

  11. CLI 參數

  12. 單元測試裡面的參數

  13. 使用 @TestPropertySource 綁定的設定

  14. Devtools 全域設定參數,來自 $HOME/.config/spring-boot

多個設定檔案的優先等級,低到高

  1. jar 裡面的設定檔

  2. 指定了 profile 的設定檔,ex: application-dev.properties (jar 裡面)

  3. application 設定檔 (jar 外面)

  4. 指定了 profile 的設定檔,ex: application-dev.properties (jar 外面)


CLI 參數,是啟動時,用 -- 開頭的參數

ex:

java -jar application.jar --server.port=8090

用 maven 啟動

mvn spring-boot:run -Dspring-boot.run.jvmArguments='-Dserver.port=8090'

如果不希望把CLI 參數增加到 spring 環境中,可以禁用

public static void main(String[] args) {
    SpringApplication.setAddCommandLineProperties(false);
    SpringApplication.run(Application.class, args);
}

匯入設定檔

可利用 spring.config.import 指定匯入設定檔的路徑

spring:
  config:
    import:
      - optional:classpath:/config/app.yml

random

RandomVlauePropertySource 可用來注入亂數值,可以產生 int, long, uuid, string

ex:

demo1:
  age: ${random.int[10,100]}
  security:
    security-key: ${random.value}
    security-code: ${random.uuid}

多個設定環境

有可能遇到 正式、測試、開發環境不同的設定

yaml 用 --- 區隔

ex:

spring:
  application:
    name: "Demo1"
---
spring:
  application:
    name: "Production1"
  config:
    activate:
      on-cloud-platform: "kubernates"

properties

spring.application.name=Demo1
#---
spring.application.name=Production1
config.activate.on-cloud-platform=kubernates

以下設定檔,可限制只在 dev 或是 test 環境使用

  • spring.config.activate.on-profile

  • spring.config.activate.on-cloud-platform

spring:
  profiles:
    active: dev
---
spring:
  config:
    activate:
      on-profile: "dev|test"

Profile

profile可以分離設定

如果不指定時,就會使用預設的 Profiledefault,可以修改預設 profile

spring:
  profiles:
    default: dev

啟用 profile

啟用 dev, test

spring:
  profiles:
    active: dev, test

也可以在程式裡面啟用

    public static void main(String[] args) {
        SpringApplication springApplication = new SpringApplication(Demo1Application.class);
        springApplication.setAdditionalProfiles("dev", "test");
        springApplication.run(args);
    }

profile 可啟用設定跟 bean,可用在 @Component, @Configuration, @ConfigurationProperties

ex: 以下設定只有在啟用 "main" 時,才會載入這個設定

@Profile("main")
@SpringBootConfiguration
public class MainConfig {
}

yaml

spring:
  profiles:
    default: dev
    active: dev, main

@Profile 用在 @ConfigurationProperties 時要注意

  • 如果是用掃描的註冊方式,@Profile 可直接用在 @ConfigurationProperties

  • 如果是用 @EditConfigurationProperties,則要把 @Profile 用在 @EditConfigurationProperties 設定類別上


切換 Profile

spring.profiles.active 設定啟用的 profile

但還是能在 CLI 參數調整,因為 CLI 參數優先權比設定檔高

java -jar application.jar --spring.profiles.active=test
mvn spring-boot:run -Dapp.profiles=test

如果不希望被替代,可改用 include

spring:
  profiles:
    default: dev
    active: dev, main
    # include 的 profile 不會被覆蓋
    include:
      - dev
      - main

profile 分組

在 yaml 將 profile 分組,當 main 啟用時, main1, main2 也會被啟用

spring:
  profiles:
    default: dev
    active: dev, main
    # include 的 profile 不會被覆蓋
    include:
      - dev
      - main
    group:
      main:
        - main1
        - main2
spring:
  config:
    active:
      on-profile: main1
---
spring:
  config:
    active:
      on-profile: main2

指定 profile 的設定檔

application-${profile}

ex:

application.yml

application-dev.yml

application-test.yml

application-main.yml

active: dev, main優先順序 default > dev > main


使用限制

  • application.profiles.default

    不指定 profile 的預設 profile

  • application.profiles.active

    啟用的 profiles

  • application.profiles.include

    要包含的 profiles,不會被 CLI 參數覆蓋

  • application.profiles.group

    分組

這些參數不能用在多個設定環境跟指定 profile 設定檔

ex: 以下是錯誤用法

在 application.yml

---
---
spring:
  config:
    activate:
      on-profile: main2
  profiles:
    active: main

在 application-dev.yml 使用 application.profiles.active

spring:
  profiles:
    active: main

設定加密

Spring Boot 沒有提供標準加解密的方法,但有提供 EnvironmentPostProcessor interface,可在 application 啟動前控制 spring 環境

spring 加解密的方法:

  • 使用第三方設定中心,支援自動解密~~~~

  • 使用自訂加解密

  • 使用 Jasypt Spring Boot

第三方設定

Spring Cloud Config

需要加解密的內容,前面會加上 {cipher}

自訂加解密

模仿 spring cloud 的方法

spring:
  datasource:
  username: '{cipher}XXXXXXXX'

用以下方式解密

@Bean
public DataSource dataSource() {
    DataSource dataSource = new DruidDataSource();
    String username = this.getUsername();
    if( username.startWith("{cipher}") ) {
        username = Encrypt.decrypt( username, this.getKey() );
    }
    dataSource.setUsername(username);
    //...
    return dataSource;
}

使用 Jasypt Spring Boot

ref: GitHub - ulisesbocchio/jasypt-spring-boot: Jasypt integration for Spring boot

ref: Spring boot 開發 - 使用 Jasypt 進行加密 | roi's blog

可針對 Spring Boot 專案中的屬性,提供加解密的方案

設定升級

當 Spring Boot 更新版本後,某些設定參數可能已經被修改或刪除,在 pom 加上這個 library 就能自動分析。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-properties-migrator</artifactId>
        </dependency>

在啟動後,就可在 console 看到需要調整的參數

  1. 在程式啟動後,再加入 Spring 的設定參數,這個自動分析不支援。ex: @PropertySource 載入的設定

  2. 設定修改完成後,要將此 library 移除