iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
Software Development

從 Laravel 到 Spring Boot:30 天打造縮網址服務系列 第 11 篇

Day11 - 設定管理 application.yml/Profile vs Laravel .env/config

  • 分享至 

  • xImage
  •  

Laravel 怎麼管設定:兩層結構

Laravel 的設定分成兩層,職責很清楚:

# .env(不進版控,每個環境各一份)
APP_ENV=local
DB_HOST=127.0.0.1
DB_PASSWORD=secret
SHORTENER_BASE_URL=http://localhost:8000
// config/shortener.php(進版控,決定設定的「形狀」跟預設值)
return [
    'base_url' => env('SHORTENER_BASE_URL', 'http://localhost:8000'),
    'code_length' => 6,
];

// 程式碼裡使用
config('shortener.base_url');

.env 只放「每個環境不一樣、或是機密」的值,config/*.php 負責把這些值組織成有結構的設定,程式碼一律透過 config() 取值,不直接呼叫 env()——因為跑過 php artisan config:cache 之後,.env 不會再被讀取,在 config 檔以外的地方呼叫 env() 會直接拿到 null。

Spring Boot:application.yml 一檔搞定形狀跟預設值

Spring Boot 把 Laravel 那兩層合進一份 application.yml(Day04 專案骨架裡就有這個檔案,Day08 也在裡面設定過 Flyway):

spring:
  application:
    name: url-shortener
  datasource:
    url: ${DB_URL:jdbc:postgresql://localhost:5432/shortener}
    username: ${DB_USERNAME:shortener}
    password: ${DB_PASSWORD:}
  jpa:
    hibernate:
      ddl-auto: validate
  flyway:
    enabled: true
    locations: classpath:db/migration

shortener:
  base-url: ${SHORTENER_BASE_URL:http://localhost:8080}
  code-length: 6

${DB_PASSWORD:} 這個語法的意思是「讀環境變數 DB_PASSWORD,沒有的話用冒號後面的預設值」,效果跟 Laravel 的 env('DB_PASSWORD', '') 一模一樣。差別在 Spring Boot 讀的是作業系統真正的環境變數,不是某個 .env 檔案——本機開發可以在 IDE 的執行設定裡填,部署到 Raspberry Pi 時(Day28)則交給 Docker Compose 注入。

shortener: 這一段是我們自己定義的設定,Spring Boot 不認識它,但完全沒關係——任何自訂的 key 都能放,等一下會把它綁定成 Java 物件。

Profile:一份共用設定 + 各環境的差異

Laravel 用 APP_ENV 標記現在是什麼環境,但設定檔本身只有一份,環境差異全靠 .env 的值不同。Spring Boot 的 Profile 多做了一件事:依照啟用的 Profile,額外載入對應的設定檔。

src/main/resources/
├── application.yml          ← 所有環境共用
├── application-dev.yml      ← 啟用 dev 時額外載入
└── application-prod.yml     ← 啟用 prod 時額外載入
# application-dev.yml
spring:
  jpa:
    show-sql: true          # 本機開發想看 Hibernate 產生的 SQL
logging:
  level:
    com.example.shortener: DEBUG
# application-prod.yml
shortener:
  base-url: ${SHORTENER_BASE_URL}   # 正式環境不給預設值,沒設定就啟動失敗
logging:
  level:
    root: WARN

啟用 Profile 的方式:

# 環境變數(部署時最常用)
SPRING_PROFILES_ACTIVE=prod java -jar url-shortener.jar

# 或是命令列參數
java -jar url-shortener.jar --spring.profiles.active=prod

Profile 設定檔裡的值會蓋過 application.yml 裡同一個 key 的值,沒寫到的 key 則沿用共用設定。所以 application-prod.yml 只需要寫「跟預設不一樣的部分」,不用整份複製一遍——這點比 Laravel 每個環境都要維護一份完整 .env 更省事。

注意 application-prod.yml 裡刻意把 base-url 的預設值拿掉:正式環境忘記設定 SHORTENER_BASE_URL 的話,應用程式會在啟動時就失敗,而不是上線後才發現產出的短網址全部指向 localhost。又是一個「錯得早」的例子(Day02、Day07 都提過這種思維)。

誰蓋過誰:設定值的優先順序

同一個 key 在很多地方都設定了,最後以誰為準?常用的幾個來源,由高到低:

  1. 命令列參數(--shortener.base-url=...)
  2. 作業系統環境變數(SHORTENER_BASE_URL=...)
  3. Profile 設定檔(application-prod.yml)
  4. 預設設定檔(application.yml)

第 2 點有個 Spring Boot 很貼心的設計叫 Relaxed Binding:就算 yml 裡沒有寫 ${...},環境變數也能照規則自動蓋過對應的 key——規則是「點換成底線、- 直接拿掉、全部大寫」。所以 SPRING_DATASOURCE_PASSWORD 可以直接蓋過 spring.datasource.password,不用在 yml 裡預先寫佔位符。

這裡有個小雷:照這個規則,shortener.base-url 對應的環境變數是 SHORTENER_BASEURL(- 被拿掉),不是直覺會寫的 SHORTENER_BASE_URL——後者會被解讀成 shortener.base.url,對不上。這也是為什麼前面的 yml 刻意明確寫出 ${SHORTENER_BASE_URL:...}:用佔位符自己指定環境變數名稱,就不用記這條轉換規則,名稱也跟 Laravel 習慣的寫法一致。

這代表一個很實用的部署模式:application.yml 放合理的預設值進版控,機密跟環境差異全部用環境變數從外面蓋,跟 Laravel「config/*.php 進版控、.env 不進版控」的精神完全一致,只是換了一種實現方式。

還 Day05 的債:把 short.ly 抽成設定

從 Day05 開始,組短網址的地方一直是寫死的:

return new LinkResponse(saved.getCode(), "https://short.ly/" + saved.getCode());

本機開發時產出的短網址指向一個根本不存在的網域,上線後又要改程式碼——這正是設定檔該處理的事。

做法一:@Value(快,但容易散落各處)

@Service
class LinkService {

    @Value("${shortener.base-url}")
    private String baseUrl;
    ...
}

能動,但 @Value 的問題跟 Laravel 在 config 檔以外呼叫 env() 很像:key 是字串,打錯字編譯不會報錯;專案變大之後,同一個設定可能被好幾個類別各自用 @Value 讀,要找「這個設定到底被誰用」得全專案搜尋字串。

做法二:@ConfigurationProperties(推薦)

把整段 shortener: 設定綁定成一個 record:

@ConfigurationProperties(prefix = "shortener")
@Validated
record ShortenerProperties(
    @NotBlank String baseUrl,
    @Min(4) @Max(16) int codeLength
) {}

在進入點加上 @ConfigurationPropertiesScan,讓 Spring 掃描並註冊這些設定類別:

@SpringBootApplication
@ConfigurationPropertiesScan
public class UrlShortenerApplication { ... }

接著就能像任何 Bean 一樣用建構子注入(Day04 講過的 DI):

@Service
class LinkService {

    private final LinkRepository linkRepository;
    private final ShortenerProperties properties;

    LinkService(LinkRepository linkRepository, ShortenerProperties properties) {
        this.linkRepository = linkRepository;
        this.properties = properties;
    }

    private LinkResponse toResponse(Link link) {
        return new LinkResponse(link.getCode(), properties.baseUrl() + "/" + link.getCode());
    }
}

好處一次列出來:

  1. 有型別:codeLength 是 int,yml 裡填成 abc 啟動就會失敗,不會跑到一半才炸
  2. 能驗證:加了 @Validated 之後,Day06 學的 @NotBlank、@Min 這些驗證註解也能用在設定上,設定不合法直接拒絕啟動
  3. 集中管理:所有 shortener.* 設定都在一個 record 裡,IDE 可以直接「尋找使用處」,不用搜尋字串
  4. yml 有自動補全:Day02 裝的 Spring Boot Tools 外掛會認得這個類別,在 application.yml 打 shortener. 就會跳出提示(要加 spring-boot-configuration-processor 依賴產生 metadata)

對照 Laravel,ShortenerProperties 扮演的就是 config/shortener.php 的角色,差別在 Laravel 的 config 是陣列、取值靠字串 key;Spring 的是有型別的物件,取值靠方法呼叫,打錯字編譯期就會發現。

順帶一提,這裡順手把 Day09 散落在 create()、findByCode() 兩處的轉換邏輯收成一個 toResponse() 方法——Day09 說過「等轉換邏輯變複雜再抽出 Mapper」,現在還不到那個程度,先收成一個私有方法就夠了。

本機想用 .env 怎麼辦

習慣 Laravel 的話,本機開發還是會想要一個 .env 檔集中放密碼。Spring Boot 可以用 spring.config.import 引入,把它當成 properties 格式讀:

# application-dev.yml
spring:
  config:
    import: optional:file:.env[.properties]

optional: 表示檔案不存在也不會報錯,[.properties] 告訴 Spring 用 properties 格式解析這個沒有副檔名的檔案。記得把 .env 加進 .gitignore——這點跟 Laravel 完全一樣,機密永遠不進版控。

小結

Laravel Spring Boot
.env 作業系統環境變數(本機可用 spring.config.import 引入 .env)
config/*.php application.yml
env('KEY', 'default') ${KEY:default}
APP_ENV spring.profiles.active
每個環境一份完整 .env application-{profile}.yml 只寫差異
config('shortener.base_url') 注入 ShortenerProperties,呼叫 baseUrl()
php artisan config:cache 沒有對應概念,設定在啟動時就讀進記憶體(長駐進程,Day02 提過)

最後一行值得多說一句:Laravel 需要 config:cache 是因為 PHP 每個請求都重新啟動,每次重新解析一堆 config 檔很浪費;Spring Boot 是長駐進程,設定只在啟動時讀一次,本來就不需要快取。同一個問題,因為執行模型不同,一邊需要額外的指令解決,另一邊則根本不存在這個問題。

明天 Day12 要進到 Day05 那張請求生命週期圖裡還沒展開的部分:Filter 跟 Interceptor,對照 Laravel 的 Middleware。


上一篇
Day10 - 例外處理與統一錯誤回應 vs Laravel Exception Handler
下一篇
Day12 - Middleware/Interceptor/Filter vs Laravel Middleware
系列文
從 Laravel 到 Spring Boot:30 天打造縮網址服務 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言