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。
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 物件。
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 在很多地方都設定了,最後以誰為準?常用的幾個來源,由高到低:
--shortener.base-url=...)SHORTENER_BASE_URL=...)application-prod.yml)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 不進版控」的精神完全一致,只是換了一種實現方式。
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());
}
}
好處一次列出來:
codeLength 是 int,yml 裡填成 abc 啟動就會失敗,不會跑到一半才炸@Validated 之後,Day06 學的 @NotBlank、@Min 這些驗證註解也能用在設定上,設定不合法直接拒絕啟動shortener.* 設定都在一個 record 裡,IDE 可以直接「尋找使用處」,不用搜尋字串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。