🎯 為什麼需要多環境配置?
在現代軟體開發中,應用程式通常需要在多個環境中運行:
📋 常見環境類型與挑戰
開發環境 (Development)
- 開發人員本地機器
- 使用本地資料庫或輕量級資料庫
- 詳細的日誌輸出便於除錯
- 不需要 Redis 等快取服務
測試環境 (Staging/UAT)
- 模擬生產環境的配置
- 使用獨立的測試資料庫
- 啟用效能監控
- 測試與第三方服務的整合
生產環境 (Production)
- 正式對外服務的環境
- 高可用性資料庫叢集
- 啟用 Redis 快取提升效能
- 嚴格的安全性與日誌管理
🎯 核心需求分析
不同環境需要不同的配置:
- 資料庫連接:開發環境用本地 MySQL,生產環境用 RDS
- 快取服務:開發環境不用 Redis,生產環境必須啟用
- 日誌級別:開發環境 DEBUG,生產環境 INFO/WARN
- 安全設定:開發環境寬鬆,生產環境嚴格
🏗️ Spring Boot 多環境配置架構
🔧 Profile 機制原理
Spring Boot 使用 Profile 機制來管理不同環境的配置:
1src/main/resources/
2├── application.yml # 基礎配置(所有環境共用)
3├── application-dev.yml # 開發環境專屬配置
4├── application-stage.yml # 測試環境專屬配置
5└── application-prod.yml # 生產環境專屬配置
配置優先級順序:
1. application-{profile}.yml (最高優先級)
2. application.yml (基礎配置)
3. 環境變數
4. 命令列參數 (最高優先級,可覆蓋所有)
🎨 配置文件結構設計
基礎配置檔案 - application.yml
1# ============================================
2# 基礎配置 - 所有環境共用
3# ============================================
4spring:
5 application:
6 name: employee-management-system
7
8 # JPA 基礎配置
9 jpa:
10 show-sql: false
11 properties:
12 hibernate:
13 format_sql: true
14 jdbc:
15 batch_size: 25
16
17 # 檔案上傳限制
18 servlet:
19 multipart:
20 max-file-size: 5MB
21 max-request-size: 5MB
22
23# Server 基礎配置
24server:
25 port: 8080
26 servlet:
27 context-path: /api
28
29# Actuator 監控端點
30management:
31 endpoints:
32 web:
33 exposure:
34 include: health,info,metrics
35 endpoint:
36 health:
37 show-details: when-authorized
38
39# 應用程式基本資訊
40info:
41 app:
42 name: ${spring.application.name}
43 version: 1.0.0
44 description: Employee Management System
開發環境配置 - application-dev.yml
1# ============================================
2# 開發環境配置
3# ============================================
4spring:
5 # H2 記憶體資料庫(快速啟動,無需安裝)
6 datasource:
7 url: jdbc:h2:mem:devdb
8 driver-class-name: org.h2.Driver
9 username: sa
10 password:
11 hikari:
12 maximum-pool-size: 5
13 minimum-idle: 2
14
15 # JPA 開發設定
16 jpa:
17 hibernate:
18 ddl-auto: create-drop # 每次啟動重建資料表
19 show-sql: true # 顯示 SQL 語句
20 properties:
21 hibernate:
22 format_sql: true # SQL 格式化
23
24 # H2 Console 啟用(方便查看資料庫)
25 h2:
26 console:
27 enabled: true
28 path: /h2-console
29
30 # Redis 停用(開發環境不需要)
31 cache:
32 type: simple # 使用簡單的記憶體快取
33
34 # 開發環境 CORS 設定(允許前端跨域)
35 web:
36 cors:
37 allowed-origins: "http://localhost:3000,http://localhost:8080"
38 allowed-methods: "*"
39 allowed-headers: "*"
40
41# 日誌配置
42logging:
43 level:
44 root: INFO
45 com.employee.system: DEBUG # 應用程式詳細日誌
46 org.springframework.web: DEBUG # Spring Web 詳細日誌
47 org.springframework.security: DEBUG
48 org.hibernate.SQL: DEBUG # SQL 語句日誌
49 org.hibernate.type.descriptor.sql.BasicBinder: TRACE # SQL 參數值
50 pattern:
51 console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
52
53# JWT 開發配置(較短的有效期便於測試)
54jwt:
55 secret: dev-secret-key-change-in-production
56 expiration: 3600000 # 1 小時
57 refresh-expiration: 86400000 # 1 天
58
59# 檔案上傳路徑
60app:
61 upload:
62 dir: ./uploads/dev
測試環境配置 - application-stage.yml
1# ============================================
2# 測試環境配置
3# ============================================
4spring:
5 # 測試用 MySQL 資料庫
6 datasource:
7 url: jdbc:mysql://${DB_HOST:localhost}:3306/${DB_NAME:employee_stage}?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
8 driver-class-name: com.mysql.cj.jdbc.Driver
9 username: ${DB_USER:stage_user}
10 password: ${DB_PASSWORD:stage_password}
11 hikari:
12 maximum-pool-size: 10
13 minimum-idle: 5
14 connection-timeout: 20000
15 idle-timeout: 300000
16 leak-detection-threshold: 60000
17
18 # JPA 測試設定
19 jpa:
20 hibernate:
21 ddl-auto: validate # 驗證資料表結構,不自動建立
22 show-sql: false
23 properties:
24 hibernate:
25 format_sql: false
26
27 # Redis 部分啟用(測試快取功能)
28 redis:
29 host: ${REDIS_HOST:localhost}
30 port: ${REDIS_PORT:6379}
31 password: ${REDIS_PASSWORD:}
32 lettuce:
33 pool:
34 max-active: 8
35 max-idle: 8
36 min-idle: 2
37
38 cache:
39 type: redis
40 redis:
41 time-to-live: 600000 # 10 分鐘快取
42
43# 日誌配置(平衡詳細度與效能)
44logging:
45 level:
46 root: INFO
47 com.employee.system: INFO
48 org.springframework.web: INFO
49 org.springframework.security: WARN
50 org.hibernate.SQL: DEBUG
51 file:
52 name: ./logs/stage/application.log
53 max-size: 10MB
54 max-history: 7
55
56# JWT 測試配置
57jwt:
58 secret: ${JWT_SECRET:stage-secret-key-please-change}
59 expiration: 7200000 # 2 小時
60 refresh-expiration: 604800000 # 7 天
61
62# 檔案上傳配置
63app:
64 upload:
65 dir: ${UPLOAD_DIR:/app/uploads/stage}
66 cors:
67 allowed-origins: ${CORS_ORIGINS:http://stage.example.com}
生產環境配置 - application-prod.yml
1# ============================================
2# 生產環境配置
3# ============================================
4spring:
5 # 生產 MySQL 資料庫(使用環境變數)
6 datasource:
7 url: jdbc:mysql://${DB_HOST}:${DB_PORT:3306}/${DB_NAME}?useSSL=true&requireSSL=true&serverTimezone=UTC
8 driver-class-name: com.mysql.cj.jdbc.Driver
9 username: ${DB_USER}
10 password: ${DB_PASSWORD}
11 hikari:
12 maximum-pool-size: 20 # 較大的連接池
13 minimum-idle: 10
14 connection-timeout: 30000
15 idle-timeout: 600000
16 max-lifetime: 1800000
17 leak-detection-threshold: 60000
18
19 # JPA 生產設定
20 jpa:
21 hibernate:
22 ddl-auto: validate # 僅驗證,絕不自動修改
23 show-sql: false # 不顯示 SQL(效能考量)
24 properties:
25 hibernate:
26 format_sql: false
27 jdbc:
28 batch_size: 50 # 批次處理提升效能
29 order_inserts: true
30 order_updates: true
31
32 # Redis 完整啟用(生產環境必須)
33 redis:
34 host: ${REDIS_HOST}
35 port: ${REDIS_PORT:6379}
36 password: ${REDIS_PASSWORD}
37 ssl: true # 啟用 SSL
38 timeout: 2000ms
39 lettuce:
40 pool:
41 max-active: 20
42 max-idle: 10
43 min-idle: 5
44 max-wait: 2000ms
45 shutdown-timeout: 100ms
46
47 cache:
48 type: redis
49 redis:
50 time-to-live: 1800000 # 30 分鐘快取
51 cache-null-values: false # 不快取 null 值
52
53 # 安全的 CORS 設定
54 web:
55 cors:
56 allowed-origins: ${CORS_ORIGINS} # 必須從環境變數設定
57 allowed-methods: GET,POST,PUT,DELETE
58 allowed-headers: Authorization,Content-Type
59 allow-credentials: true
60 max-age: 3600
61
62# 生產日誌配置(僅記錄重要資訊)
63logging:
64 level:
65 root: WARN
66 com.employee.system: INFO
67 org.springframework.web: WARN
68 org.springframework.security: WARN
69 org.hibernate.SQL: WARN
70 file:
71 name: /var/log/employee-system/application.log
72 max-size: 100MB
73 max-history: 30
74 pattern:
75 file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
76
77# JWT 生產配置(強安全性)
78jwt:
79 secret: ${JWT_SECRET} # 必須從環境變數或密鑰管理服務讀取
80 expiration: 3600000 # 1 小時
81 refresh-expiration: 2592000000 # 30 天
82
83# 檔案上傳配置
84app:
85 upload:
86 dir: ${UPLOAD_DIR:/app/uploads/prod}
87 max-size: 5242880 # 5MB
88 allowed-extensions: jpg,jpeg,png,pdf
89
90# Actuator 安全配置
91management:
92 endpoints:
93 web:
94 exposure:
95 include: health,info # 僅暴露必要端點
96 endpoint:
97 health:
98 show-details: never # 不暴露詳細健康資訊
99
100# Server 生產配置
101server:
102 port: ${SERVER_PORT:8080}
103 error:
104 include-stacktrace: never # 不暴露堆疊追蹤
105 include-message: never # 不暴露錯誤訊息
106 tomcat:
107 threads:
108 max: 200 # 最大執行緒數
109 min-spare: 10
110 max-connections: 8192 # 最大連接數
111 accept-count: 100
💻 Java 程式碼實作
🔧 配置類別設計
1. 資料庫配置類別 - DatabaseConfig.java
1package com.employee.system.config;
2
3import com.zaxxer.hikari.HikariConfig;
4import com.zaxxer.hikari.HikariDataSource;
5import lombok.extern.slf4j.Slf4j;
6import org.springframework.beans.factory.annotation.Value;
7import org.springframework.context.annotation.Bean;
8import org.springframework.context.annotation.Configuration;
9import org.springframework.context.annotation.Profile;
10
11import javax.sql.DataSource;
12
13/**
14 * 資料庫配置類別
15 * 根據不同環境提供不同的資料庫配置
16 */
17@Configuration
18@Slf4j
19public class DatabaseConfig {
20
21 @Value("${spring.datasource.url}")
22 private String jdbcUrl;
23
24 @Value("${spring.datasource.username}")
25 private String username;
26
27 @Value("${spring.datasource.password}")
28 private String password;
29
30 @Value("${spring.datasource.driver-class-name}")
31 private String driverClassName;
32
33 /**
34 * 開發環境資料源(H2 記憶體資料庫)
35 */
36 @Bean
37 @Profile("dev")
38 public DataSource devDataSource() {
39 log.info("====================================");
40 log.info("初始化開發環境資料源 (H2 Database)");
41 log.info("JDBC URL: {}", jdbcUrl);
42 log.info("====================================");
43
44 HikariConfig config = new HikariConfig();
45 config.setJdbcUrl(jdbcUrl);
46 config.setUsername(username);
47 config.setPassword(password);
48 config.setDriverClassName(driverClassName);
49
50 // 開發環境較小的連接池
51 config.setMaximumPoolSize(5);
52 config.setMinimumIdle(2);
53 config.setConnectionTimeout(20000);
54
55 return new HikariDataSource(config);
56 }
57
58 /**
59 * 測試環境資料源(MySQL)
60 */
61 @Bean
62 @Profile("stage")
63 public DataSource stageDataSource() {
64 log.info("====================================");
65 log.info("初始化測試環境資料源 (MySQL)");
66 log.info("JDBC URL: {}", jdbcUrl);
67 log.info("====================================");
68
69 HikariConfig config = new HikariConfig();
70 config.setJdbcUrl(jdbcUrl);
71 config.setUsername(username);
72 config.setPassword(password);
73 config.setDriverClassName(driverClassName);
74
75 // 測試環境中等連接池
76 config.setMaximumPoolSize(10);
77 config.setMinimumIdle(5);
78 config.setConnectionTimeout(20000);
79 config.setLeakDetectionThreshold(60000);
80
81 return new HikariDataSource(config);
82 }
83
84 /**
85 * 生產環境資料源(MySQL with 優化配置)
86 */
87 @Bean
88 @Profile("prod")
89 public DataSource prodDataSource() {
90 log.info("====================================");
91 log.info("初始化生產環境資料源 (MySQL Production)");
92 log.info("JDBC URL: {}", maskPassword(jdbcUrl));
93 log.info("====================================");
94
95 HikariConfig config = new HikariConfig();
96 config.setJdbcUrl(jdbcUrl);
97 config.setUsername(username);
98 config.setPassword(password);
99 config.setDriverClassName(driverClassName);
100
101 // 生產環境較大的連接池
102 config.setMaximumPoolSize(20);
103 config.setMinimumIdle(10);
104 config.setConnectionTimeout(30000);
105 config.setIdleTimeout(600000);
106 config.setMaxLifetime(1800000);
107 config.setLeakDetectionThreshold(60000);
108
109 // 生產環境額外配置
110 config.addDataSourceProperty("cachePrepStmts", "true");
111 config.addDataSourceProperty("prepStmtCacheSize", "250");
112 config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
113 config.addDataSourceProperty("useServerPrepStmts", "true");
114
115 return new HikariDataSource(config);
116 }
117
118 /**
119 * 遮蔽密碼資訊(安全性)
120 */
121 private String maskPassword(String url) {
122 if (url.contains("password=")) {
123 return url.replaceAll("password=[^&]*", "password=****");
124 }
125 return url;
126 }
127}
2. Redis 配置類別 - RedisConfig.java
1package com.employee.system.config;
2
3import com.fasterxml.jackson.annotation.JsonAutoDetect;
4import com.fasterxml.jackson.annotation.PropertyAccessor;
5import com.fasterxml.jackson.databind.ObjectMapper;
6import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
7import lombok.extern.slf4j.Slf4j;
8import org.springframework.beans.factory.annotation.Value;
9import org.springframework.cache.CacheManager;
10import org.springframework.cache.annotation.EnableCaching;
11import org.springframework.context.annotation.Bean;
12import org.springframework.context.annotation.Configuration;
13import org.springframework.context.annotation.Profile;
14import org.springframework.data.redis.cache.RedisCacheConfiguration;
15import org.springframework.data.redis.cache.RedisCacheManager;
16import org.springframework.data.redis.connection.RedisConnectionFactory;
17import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
18import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
19import org.springframework.data.redis.core.RedisTemplate;
20import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
21import org.springframework.data.redis.serializer.RedisSerializationContext;
22import org.springframework.data.redis.serializer.StringRedisSerializer;
23
24import java.time.Duration;
25import java.util.HashMap;
26import java.util.Map;
27
28/**
29 * Redis 快取配置
30 * 開發環境停用,測試/生產環境啟用
31 */
32@Configuration
33@EnableCaching
34@Slf4j
35public class RedisConfig {
36
37 @Value("${spring.redis.host:localhost}")
38 private String redisHost;
39
40 @Value("${spring.redis.port:6379}")
41 private int redisPort;
42
43 @Value("${spring.redis.password:}")
44 private String redisPassword;
45
46 /**
47 * 開發環境:使用簡單的記憶體快取(不需要 Redis)
48 */
49 @Bean
50 @Profile("dev")
51 public CacheManager devCacheManager() {
52 log.info("====================================");
53 log.info("開發環境:使用簡單記憶體快取(無 Redis)");
54 log.info("====================================");
55 return new org.springframework.cache.concurrent.ConcurrentMapCacheManager();
56 }
57
58 /**
59 * 測試/生產環境:使用 Redis
60 */
61 @Bean
62 @Profile({"stage", "prod"})
63 public LettuceConnectionFactory redisConnectionFactory() {
64 log.info("====================================");
65 log.info("初始化 Redis 連接");
66 log.info("Redis Host: {}", redisHost);
67 log.info("Redis Port: {}", redisPort);
68 log.info("====================================");
69
70 RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
71 config.setHostName(redisHost);
72 config.setPort(redisPort);
73
74 if (redisPassword != null && !redisPassword.isEmpty()) {
75 config.setPassword(redisPassword);
76 }
77
78 return new LettuceConnectionFactory(config);
79 }
80
81 /**
82 * Redis Template 配置
83 */
84 @Bean
85 @Profile({"stage", "prod"})
86 public RedisTemplate<String, Object> redisTemplate(
87 RedisConnectionFactory connectionFactory) {
88
89 RedisTemplate<String, Object> template = new RedisTemplate<>();
90 template.setConnectionFactory(connectionFactory);
91
92 // Jackson 序列化配置
93 Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
94 new Jackson2JsonRedisSerializer<>(Object.class);
95
96 ObjectMapper objectMapper = new ObjectMapper();
97 objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
98 objectMapper.activateDefaultTyping(
99 LaissezFaireSubTypeValidator.instance,
100 ObjectMapper.DefaultTyping.NON_FINAL
101 );
102
103 jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
104
105 // String 序列化
106 StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
107
108 // Key 使用 String 序列化
109 template.setKeySerializer(stringRedisSerializer);
110 template.setHashKeySerializer(stringRedisSerializer);
111
112 // Value 使用 JSON 序列化
113 template.setValueSerializer(jackson2JsonRedisSerializer);
114 template.setHashValueSerializer(jackson2JsonRedisSerializer);
115
116 template.afterPropertiesSet();
117
118 return template;
119 }
120
121 /**
122 * Redis Cache Manager 配置
123 */
124 @Bean
125 @Profile({"stage", "prod"})
126 public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
127 log.info("====================================");
128 log.info("初始化 Redis Cache Manager");
129 log.info("====================================");
130
131 // 預設快取配置
132 RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
133 .entryTtl(Duration.ofMinutes(30)) // 預設 30 分鐘過期
134 .serializeKeysWith(
135 RedisSerializationContext.SerializationPair.fromSerializer(
136 new StringRedisSerializer()))
137 .serializeValuesWith(
138 RedisSerializationContext.SerializationPair.fromSerializer(
139 new Jackson2JsonRedisSerializer<>(Object.class)))
140 .disableCachingNullValues(); // 不快取 null 值
141
142 // 不同快取的個別配置
143 Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
144
145 // 員工資料快取:1 小時
146 cacheConfigurations.put("employees",
147 defaultConfig.entryTtl(Duration.ofHours(1)));
148
149 // 部門資料快取:2 小時(較少變動)
150 cacheConfigurations.put("departments",
151 defaultConfig.entryTtl(Duration.ofHours(2)));
152
153 // 使用者資料快取:30 分鐘
154 cacheConfigurations.put("users",
155 defaultConfig.entryTtl(Duration.ofMinutes(30)));
156
157 return RedisCacheManager.builder(connectionFactory)
158 .cacheDefaults(defaultConfig)
159 .withInitialCacheConfigurations(cacheConfigurations)
160 .transactionAware()
161 .build();
162 }
163}
3. 環境感知服務類別 - EnvironmentService.java
1package com.employee.system.service;
2
3import lombok.extern.slf4j.Slf4j;
4import org.springframework.beans.factory.annotation.Autowired;
5import org.springframework.beans.factory.annotation.Value;
6import org.springframework.core.env.Environment;
7import org.springframework.stereotype.Service;
8
9import javax.annotation.PostConstruct;
10import java.util.Arrays;
11
12/**
13 * 環境資訊服務
14 * 提供當前運行環境的資訊
15 */
16@Service
17@Slf4j
18public class EnvironmentService {
19
20 @Autowired
21 private Environment environment;
22
23 @Value("${spring.datasource.url:N/A}")
24 private String datasourceUrl;
25
26 @Value("${spring.redis.host:N/A}")
27 private String redisHost;
28
29 @Value("${spring.cache.type:N/A}")
30 private String cacheType;
31
32 /**
33 * 應用程式啟動時顯示環境資訊
34 */
35 @PostConstruct
36 public void displayEnvironmentInfo() {
37 String[] activeProfiles = environment.getActiveProfiles();
38 String profile = activeProfiles.length > 0 ? activeProfiles[0] : "default";
39
40 log.info("========================================");
41 log.info("🚀 應用程式環境資訊");
42 log.info("========================================");
43 log.info("當前環境: {}", profile.toUpperCase());
44 log.info("資料庫 URL: {}", maskSensitiveInfo(datasourceUrl));
45 log.info("快取類型: {}", cacheType);
46 log.info("Redis 主機: {}", redisHost);
47 log.info("========================================");
48 }
49
50 /**
51 * 檢查是否為開發環境
52 */
53 public boolean isDevelopment() {
54 return Arrays.asList(environment.getActiveProfiles()).contains("dev");
55 }
56
57 /**
58 * 檢查是否為測試環境
59 */
60 public boolean isStaging() {
61 return Arrays.asList(environment.getActiveProfiles()).contains("stage");
62 }
63
64 /**
65 * 檢查是否為生產環境
66 */
67 public boolean isProduction() {
68 return Arrays.asList(environment.getActiveProfiles()).contains("prod");
69 }
70
71 /**
72 * 檢查 Redis 是否啟用
73 */
74 public boolean isRedisEnabled() {
75 return "redis".equalsIgnoreCase(cacheType);
76 }
77
78 /**
79 * 取得當前環境名稱
80 */
81 public String getCurrentEnvironment() {
82 String[] profiles = environment.getActiveProfiles();
83 return profiles.length > 0 ? profiles[0] : "default";
84 }
85
86 /**
87 * 遮蔽敏感資訊
88 */
89 private String maskSensitiveInfo(String info) {
90 if (info.contains("password=")) {
91 info = info.replaceAll("password=[^&]*", "password=****");
92 }
93 return info;
94 }
95}
4. Service 層使用快取 - EmployeeService.java
1package com.employee.system.service;
2
3import com.employee.system.dto.EmployeeDto;
4import com.employee.system.entity.Employee;
5import com.employee.system.repository.EmployeeRepository;
6import lombok.extern.slf4j.Slf4j;
7import org.springframework.beans.factory.annotation.Autowired;
8import org.springframework.cache.annotation.CacheEvict;
9import org.springframework.cache.annotation.CachePut;
10import org.springframework.cache.annotation.Cacheable;
11import org.springframework.stereotype.Service;
12import org.springframework.transaction.annotation.Transactional;
13
14import java.util.List;
15import java.util.stream.Collectors;
16
17/**
18 * 員工服務類別
19 * 展示如何使用快取註解
20 */
21@Service
22@Slf4j
23@Transactional
24public class EmployeeService {
25
26 @Autowired
27 private EmployeeRepository employeeRepository;
28
29 @Autowired
30 private EnvironmentService environmentService;
31
32 /**
33 * 根據 ID 查詢員工(使用快取)
34 * 開發環境:記憶體快取
35 * 測試/生產環境:Redis 快取
36 */
37 @Cacheable(value = "employees", key = "#id", unless = "#result == null")
38 public EmployeeDto getEmployeeById(Long id) {
39 log.info("從資料庫查詢員工 ID: {} (環境: {})",
40 id, environmentService.getCurrentEnvironment());
41
42 Employee employee = employeeRepository.findById(id)
43 .orElseThrow(() -> new RuntimeException("員工不存在: " + id));
44
45 return convertToDto(employee);
46 }
47
48 /**
49 * 查詢所有員工(使用快取)
50 */
51 @Cacheable(value = "employees", key = "'all'")
52 public List<EmployeeDto> getAllEmployees() {
53 log.info("從資料庫查詢所有員工 (環境: {})",
54 environmentService.getCurrentEnvironment());
55
56 return employeeRepository.findAll()
57 .stream()
58 .map(this::convertToDto)
59 .collect(Collectors.toList());
60 }
61
62 /**
63 * 更新員工資料(更新快取)
64 */
65 @CachePut(value = "employees", key = "#id")
66 public EmployeeDto updateEmployee(Long id, EmployeeDto employeeDto) {
67 log.info("更新員工 ID: {} (環境: {})",
68 id, environmentService.getCurrentEnvironment());
69
70 Employee employee = employeeRepository.findById(id)
71 .orElseThrow(() -> new RuntimeException("員工不存在: " + id));
72
73 // 更新邏輯...
74 employee.setFirstName(employeeDto.getFirstName());
75 employee.setLastName(employeeDto.getLastName());
76 employee.setEmail(employeeDto.getEmail());
77
78 Employee updated = employeeRepository.save(employee);
79
80 // 清除所有員工的快取
81 evictAllEmployeesCache();
82
83 return convertToDto(updated);
84 }
85
86 /**
87 * 刪除員工(清除快取)
88 */
89 @CacheEvict(value = "employees", key = "#id")
90 public void deleteEmployee(Long id) {
91 log.info("刪除員工 ID: {} (環境: {})",
92 id, environmentService.getCurrentEnvironment());
93
94 employeeRepository.deleteById(id);
95
96 // 清除所有員工的快取
97 evictAllEmployeesCache();
98 }
99
100 /**
101 * 清除所有員工快取
102 */
103 @CacheEvict(value = "employees", key = "'all'")
104 public void evictAllEmployeesCache() {
105 log.info("清除所有員工快取 (環境: {})",
106 environmentService.getCurrentEnvironment());
107 }
108
109 /**
110 * Entity 轉 DTO
111 */
112 private EmployeeDto convertToDto(Employee employee) {
113 return EmployeeDto.builder()
114 .id(employee.getId())
115 .firstName(employee.getFirstName())
116 .lastName(employee.getLastName())
117 .email(employee.getEmail())
118 .build();
119 }
120}
🐳 Docker 容器化部署
📦 Dockerfile 設計
多階段建置 Dockerfile
1# ============================================
2# Stage 1: Build Stage (Maven 編譯)
3# ============================================
4FROM maven:3.8.6-eclipse-temurin-17 AS build
5
6WORKDIR /app
7
8# 複製 pom.xml 並下載依賴(利用 Docker 快取)
9COPY pom.xml .
10RUN mvn dependency:go-offline -B
11
12# 複製原始碼並編譯
13COPY src ./src
14RUN mvn clean package -DskipTests -B
15
16# ============================================
17# Stage 2: Runtime Stage (執行環境)
18# ============================================
19FROM eclipse-temurin:17-jre-alpine
20
21WORKDIR /app
22
23# 建立非 root 使用者(安全性)
24RUN addgroup -g 1001 appuser && \
25 adduser -D -u 1001 -G appuser appuser
26
27# 複製編譯好的 JAR 檔案
28COPY --from=build /app/target/*.jar app.jar
29
30# 建立日誌和上傳目錄
31RUN mkdir -p /app/logs /app/uploads && \
32 chown -R appuser:appuser /app
33
34# 切換到非 root 使用者
35USER appuser
36
37# 暴露埠號
38EXPOSE 8080
39
40# 健康檢查
41HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
42 CMD wget --no-verbose --tries=1 --spider http://localhost:8080/api/actuator/health || exit 1
43
44# 啟動指令(使用環境變數指定 Profile)
45ENTRYPOINT ["java", \
46 "-Djava.security.egd=file:/dev/./urandom", \
47 "-Dspring.profiles.active=${SPRING_PROFILE:prod}", \
48 "-jar", \
49 "app.jar"]
🎯 Docker Compose 配置
完整的多環境 Docker Compose 配置
docker-compose.dev.yml - 開發環境
1version: '3.8'
2
3services:
4 # Spring Boot 應用(開發環境)
5 app:
6 build:
7 context: .
8 dockerfile: Dockerfile
9 container_name: employee-system-dev
10 environment:
11 - SPRING_PROFILE=dev
12 ports:
13 - "8080:8080"
14 volumes:
15 - ./logs:/app/logs
16 - ./uploads:/app/uploads
17 networks:
18 - employee-network
19 restart: unless-stopped
20 healthcheck:
21 test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/api/actuator/health"]
22 interval: 30s
23 timeout: 10s
24 retries: 3
25
26networks:
27 employee-network:
28 driver: bridge
docker-compose.stage.yml - 測試環境
1version: '3.8'
2
3services:
4 # MySQL 資料庫
5 mysql:
6 image: mysql:8.0
7 container_name: employee-mysql-stage
8 environment:
9 MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
10 MYSQL_DATABASE: ${DB_NAME:-employee_stage}
11 MYSQL_USER: ${DB_USER:-stage_user}
12 MYSQL_PASSWORD: ${DB_PASSWORD}
13 ports:
14 - "3306:3306"
15 volumes:
16 - mysql_stage_data:/var/lib/mysql
17 - ./sql/init:/docker-entrypoint-initdb.d
18 networks:
19 - employee-network
20 restart: unless-stopped
21 healthcheck:
22 test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
23 timeout: 10s
24 retries: 5
25
26 # Redis 快取
27 redis:
28 image: redis:7-alpine
29 container_name: employee-redis-stage
30 command: redis-server --requirepass ${REDIS_PASSWORD}
31 ports:
32 - "6379:6379"
33 volumes:
34 - redis_stage_data:/data
35 networks:
36 - employee-network
37 restart: unless-stopped
38 healthcheck:
39 test: ["CMD", "redis-cli", "ping"]
40 interval: 10s
41 timeout: 3s
42 retries: 5
43
44 # Spring Boot 應用(測試環境)
45 app:
46 build:
47 context: .
48 dockerfile: Dockerfile
49 container_name: employee-system-stage
50 environment:
51 - SPRING_PROFILE=stage
52 - DB_HOST=mysql
53 - DB_PORT=3306
54 - DB_NAME=${DB_NAME:-employee_stage}
55 - DB_USER=${DB_USER:-stage_user}
56 - DB_PASSWORD=${DB_PASSWORD}
57 - REDIS_HOST=redis
58 - REDIS_PORT=6379
59 - REDIS_PASSWORD=${REDIS_PASSWORD}
60 - JWT_SECRET=${JWT_SECRET}
61 ports:
62 - "8080:8080"
63 depends_on:
64 mysql:
65 condition: service_healthy
66 redis:
67 condition: service_healthy
68 volumes:
69 - ./logs:/app/logs
70 - ./uploads:/app/uploads
71 networks:
72 - employee-network
73 restart: unless-stopped
74 healthcheck:
75 test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/api/actuator/health"]
76 interval: 30s
77 timeout: 10s
78 retries: 3
79
80volumes:
81 mysql_stage_data:
82 driver: local
83 redis_stage_data:
84 driver: local
85
86networks:
87 employee-network:
88 driver: bridge
docker-compose.prod.yml - 生產環境
1version: '3.8'
2
3services:
4 # MySQL 資料庫(生產配置)
5 mysql:
6 image: mysql:8.0
7 container_name: employee-mysql-prod
8 environment:
9 MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
10 MYSQL_DATABASE: ${DB_NAME}
11 MYSQL_USER: ${DB_USER}
12 MYSQL_PASSWORD: ${DB_PASSWORD}
13 ports:
14 - "3306:3306"
15 volumes:
16 - mysql_prod_data:/var/lib/mysql
17 - ./sql/init:/docker-entrypoint-initdb.d
18 - ./mysql/conf:/etc/mysql/conf.d # 自定義 MySQL 配置
19 command:
20 - --character-set-server=utf8mb4
21 - --collation-server=utf8mb4_unicode_ci
22 - --max_connections=500
23 - --innodb_buffer_pool_size=2G
24 networks:
25 - employee-network
26 restart: always
27 healthcheck:
28 test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
29 timeout: 10s
30 retries: 5
31
32 # Redis 快取(生產配置)
33 redis:
34 image: redis:7-alpine
35 container_name: employee-redis-prod
36 command: >
37 redis-server
38 --requirepass ${REDIS_PASSWORD}
39 --maxmemory 512mb
40 --maxmemory-policy allkeys-lru
41 --save 900 1
42 --save 300 10
43 --save 60 10000
44 ports:
45 - "6379:6379"
46 volumes:
47 - redis_prod_data:/data
48 - ./redis/redis.conf:/usr/local/etc/redis/redis.conf # 自定義 Redis 配置
49 networks:
50 - employee-network
51 restart: always
52 healthcheck:
53 test: ["CMD", "redis-cli", "--pass", "${REDIS_PASSWORD}", "ping"]
54 interval: 10s
55 timeout: 3s
56 retries: 5
57
58 # Spring Boot 應用(生產環境)
59 app:
60 build:
61 context: .
62 dockerfile: Dockerfile
63 container_name: employee-system-prod
64 environment:
65 - SPRING_PROFILE=prod
66 - DB_HOST=mysql
67 - DB_PORT=3306
68 - DB_NAME=${DB_NAME}
69 - DB_USER=${DB_USER}
70 - DB_PASSWORD=${DB_PASSWORD}
71 - REDIS_HOST=redis
72 - REDIS_PORT=6379
73 - REDIS_PASSWORD=${REDIS_PASSWORD}
74 - JWT_SECRET=${JWT_SECRET}
75 - UPLOAD_DIR=/app/uploads/prod
76 - CORS_ORIGINS=${CORS_ORIGINS}
77 - JAVA_OPTS=-Xms512m -Xmx2048m -XX:+UseG1GC -XX:MaxGCPauseMillis=200
78 ports:
79 - "8080:8080"
80 depends_on:
81 mysql:
82 condition: service_healthy
83 redis:
84 condition: service_healthy
85 volumes:
86 - ./logs:/var/log/employee-system
87 - uploads_prod_data:/app/uploads/prod
88 networks:
89 - employee-network
90 restart: always
91 healthcheck:
92 test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/api/actuator/health"]
93 interval: 30s
94 timeout: 10s
95 retries: 3
96 start_period: 60s
97 deploy:
98 resources:
99 limits:
100 cpus: '2'
101 memory: 2048M
102 reservations:
103 cpus: '1'
104 memory: 512M
105
106 # Nginx 反向代理
107 nginx:
108 image: nginx:alpine
109 container_name: employee-nginx-prod
110 ports:
111 - "80:80"
112 - "443:443"
113 volumes:
114 - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
115 - ./nginx/ssl:/etc/nginx/ssl:ro
116 - uploads_prod_data:/var/www/uploads:ro
117 depends_on:
118 - app
119 networks:
120 - employee-network
121 restart: always
122
123volumes:
124 mysql_prod_data:
125 driver: local
126 redis_prod_data:
127 driver: local
128 uploads_prod_data:
129 driver: local
130
131networks:
132 employee-network:
133 driver: bridge
🔐 環境變數管理
.env.dev - 開發環境變數
1# 開發環境配置
2SPRING_PROFILE=dev
.env.stage - 測試環境變數
1# 測試環境配置
2SPRING_PROFILE=stage
3
4# 資料庫配置
5DB_ROOT_PASSWORD=stage_root_password
6DB_NAME=employee_stage
7DB_USER=stage_user
8DB_PASSWORD=stage_db_password
9
10# Redis 配置
11REDIS_PASSWORD=stage_redis_password
12
13# JWT 配置
14JWT_SECRET=stage_jwt_secret_key_minimum_256_bits
15
16# CORS 配置
17CORS_ORIGINS=http://stage-frontend.example.com
.env.prod - 生產環境變數
1# 生產環境配置
2SPRING_PROFILE=prod
3
4# 資料庫配置(使用強密碼)
5DB_ROOT_PASSWORD=<strong-root-password>
6DB_NAME=employee_prod
7DB_USER=prod_user
8DB_PASSWORD=<strong-db-password>
9
10# Redis 配置
11REDIS_PASSWORD=<strong-redis-password>
12
13# JWT 配置(使用密鑰管理服務)
14JWT_SECRET=<strong-jwt-secret-minimum-256-bits>
15
16# CORS 配置
17CORS_ORIGINS=https://app.example.com,https://www.example.com
18
19# 檔案上傳配置
20UPLOAD_DIR=/app/uploads/prod
21
22# Java 記憶體配置
23JAVA_OPTS=-Xms1024m -Xmx2048m -XX:+UseG1GC
🚀 Docker 部署指令
開發環境啟動
1# 使用開發環境配置啟動
2docker-compose -f docker-compose.dev.yml --env-file .env.dev up -d
3
4# 查看日誌
5docker-compose -f docker-compose.dev.yml logs -f app
6
7# 停止
8docker-compose -f docker-compose.dev.yml down
測試環境啟動
1# 使用測試環境配置啟動
2docker-compose -f docker-compose.stage.yml --env-file .env.stage up -d
3
4# 查看所有服務狀態
5docker-compose -f docker-compose.stage.yml ps
6
7# 查看特定服務日誌
8docker-compose -f docker-compose.stage.yml logs -f app
9
10# 重新建置並啟動
11docker-compose -f docker-compose.stage.yml up -d --build
12
13# 停止並刪除資料卷(慎用)
14docker-compose -f docker-compose.stage.yml down -v
生產環境啟動
1# 使用生產環境配置啟動
2docker-compose -f docker-compose.prod.yml --env-file .env.prod up -d
3
4# 檢查健康狀態
5docker-compose -f docker-compose.prod.yml ps
6
7# 查看資源使用情況
8docker stats
9
10# 查看應用程式日誌(不使用 -f 避免阻塞)
11docker-compose -f docker-compose.prod.yml logs --tail=100 app
12
13# 執行資料庫備份
14docker exec employee-mysql-prod mysqldump -u root -p${DB_ROOT_PASSWORD} ${DB_NAME} > backup.sql
15
16# 滾動更新(零停機)
17docker-compose -f docker-compose.prod.yml up -d --no-deps --build app
18
19# 停止(保留資料卷)
20docker-compose -f docker-compose.prod.yml down
🔄 Docker Build 參數化
使用 Build Args 指定環境
1# 建置開發環境映像
2docker build \
3 --build-arg SPRING_PROFILE=dev \
4 -t employee-system:dev \
5 .
6
7# 建置測試環境映像
8docker build \
9 --build-arg SPRING_PROFILE=stage \
10 -t employee-system:stage \
11 .
12
13# 建置生產環境映像
14docker build \
15 --build-arg SPRING_PROFILE=prod \
16 -t employee-system:prod \
17 .
18
19# 執行特定環境的容器
20docker run -d \
21 --name employee-system-prod \
22 -p 8080:8080 \
23 --env-file .env.prod \
24 employee-system:prod
📊 監控與驗證
🔍 驗證配置載入
建立監控端點 - ConfigController.java
1package com.employee.system.controller;
2
3import com.employee.system.service.EnvironmentService;
4import lombok.AllArgsConstructor;
5import lombok.Data;
6import org.springframework.beans.factory.annotation.Autowired;
7import org.springframework.beans.factory.annotation.Value;
8import org.springframework.core.env.Environment;
9import org.springframework.web.bind.annotation.GetMapping;
10import org.springframework.web.bind.annotation.RequestMapping;
11import org.springframework.web.bind.annotation.RestController;
12
13import java.util.Arrays;
14import java.util.HashMap;
15import java.util.Map;
16
17/**
18 * 配置資訊監控端點
19 * 僅在開發/測試環境啟用
20 */
21@RestController
22@RequestMapping("/api/config")
23public class ConfigController {
24
25 @Autowired
26 private Environment environment;
27
28 @Autowired
29 private EnvironmentService environmentService;
30
31 @Value("${spring.datasource.url:N/A}")
32 private String datasourceUrl;
33
34 @Value("${spring.cache.type:N/A}")
35 private String cacheType;
36
37 /**
38 * 取得當前配置資訊
39 */
40 @GetMapping("/info")
41 public ConfigInfo getConfigInfo() {
42 // 生產環境不暴露配置資訊
43 if (environmentService.isProduction()) {
44 throw new RuntimeException("生產環境不允許存取配置資訊");
45 }
46
47 ConfigInfo info = new ConfigInfo();
48 info.setEnvironment(environmentService.getCurrentEnvironment());
49 info.setActiveProfiles(Arrays.asList(environment.getActiveProfiles()));
50 info.setDatasourceUrl(maskSensitiveInfo(datasourceUrl));
51 info.setCacheType(cacheType);
52 info.setRedisEnabled(environmentService.isRedisEnabled());
53
54 return info;
55 }
56
57 /**
58 * 健康檢查端點
59 */
60 @GetMapping("/health")
61 public Map<String, Object> healthCheck() {
62 Map<String, Object> health = new HashMap<>();
63 health.put("status", "UP");
64 health.put("environment", environmentService.getCurrentEnvironment());
65 health.put("database", "UP");
66 health.put("cache", environmentService.isRedisEnabled() ? "Redis" : "Simple");
67
68 return health;
69 }
70
71 /**
72 * 遮蔽敏感資訊
73 */
74 private String maskSensitiveInfo(String info) {
75 if (info.contains("password=")) {
76 info = info.replaceAll("password=[^&]*", "password=****");
77 }
78 return info;
79 }
80
81 @Data
82 @AllArgsConstructor
83 private static class ConfigInfo {
84 private String environment;
85 private java.util.List<String> activeProfiles;
86 private String datasourceUrl;
87 private String cacheType;
88 private boolean redisEnabled;
89
90 public ConfigInfo() {}
91 }
92}
📈 測試不同環境
測試腳本 - test-environments.sh
1#!/bin/bash
2
3# 顏色定義
4GREEN='\033[0;32m'
5YELLOW='\033[1;33m'
6RED='\033[0;31m'
7NC='\033[0m' # No Color
8
9echo -e "${YELLOW}========================================${NC}"
10echo -e "${YELLOW}Spring Boot 多環境配置測試${NC}"
11echo -e "${YELLOW}========================================${NC}"
12
13# 測試開發環境
14echo -e "\n${GREEN}1. 測試開發環境 (dev)${NC}"
15docker-compose -f docker-compose.dev.yml --env-file .env.dev up -d
16sleep 10
17
18echo -e "檢查配置..."
19curl -s http://localhost:8080/api/config/info | jq '.'
20
21echo -e "檢查健康狀態..."
22curl -s http://localhost:8080/api/config/health | jq '.'
23
24docker-compose -f docker-compose.dev.yml down
25
26# 測試測試環境
27echo -e "\n${GREEN}2. 測試測試環境 (stage)${NC}"
28docker-compose -f docker-compose.stage.yml --env-file .env.stage up -d
29sleep 30
30
31echo -e "檢查配置..."
32curl -s http://localhost:8080/api/config/info | jq '.'
33
34echo -e "檢查 Redis 連接..."
35docker exec employee-redis-stage redis-cli -a stage_redis_password ping
36
37docker-compose -f docker-compose.stage.yml down
38
39# 測試生產環境
40echo -e "\n${GREEN}3. 測試生產環境 (prod)${NC}"
41docker-compose -f docker-compose.prod.yml --env-file .env.prod up -d
42sleep 30
43
44echo -e "檢查健康狀態..."
45curl -s http://localhost:8080/api/actuator/health | jq '.'
46
47echo -e "檢查 Redis 連接..."
48docker exec employee-redis-prod redis-cli -a ${REDIS_PASSWORD} ping
49
50docker-compose -f docker-compose.prod.yml down
51
52echo -e "\n${YELLOW}========================================${NC}"
53echo -e "${YELLOW}所有環境測試完成!${NC}"
54echo -e "${YELLOW}========================================${NC}"
🎯 最佳實踐與注意事項
✅ 配置管理最佳實踐
1. 安全性
- ❌ 絕對不要將敏感資訊(密碼、金鑰)寫在配置檔案中
- ✅ 必須使用環境變數或密鑰管理服務
- ✅ 生產環境使用 AWS Secrets Manager 或 HashiCorp Vault
- ✅
.env檔案加入.gitignore
2. 配置分離
- ✅ 基礎配置放在
application.yml - ✅ 環境特定配置放在
application-{profile}.yml - ✅ 敏感配置使用環境變數
- ✅ 使用
@Value和@ConfigurationProperties注入配置
3. 資料庫管理
- ✅ 開發環境使用 H2/SQLite 記憶體資料庫
- ✅ 測試環境使用獨立的測試資料庫
- ✅ 生產環境使用 RDS 或資料庫叢集
- ✅ 使用 Flyway/Liquibase 管理資料庫版本
4. 快取策略
- ✅ 開發環境不使用 Redis(減少依賴)
- ✅ 測試環境使用 Redis 測試快取功能
- ✅ 生產環境使用 Redis Cluster(高可用)
- ✅ 設定合理的快取過期時間
⚠️ 常見陷阱與解決方案
問題 1:Profile 沒有正確載入
1# 解決方案:明確指定 Profile
2java -jar app.jar --spring.profiles.active=prod
3
4# Docker 環境
5docker run -e SPRING_PROFILES_ACTIVE=prod app:latest
問題 2:環境變數沒有生效
1# 錯誤寫法
2spring:
3 datasource:
4 url: jdbc:mysql://localhost:3306/db
5
6# 正確寫法(使用環境變數)
7spring:
8 datasource:
9 url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:db}
問題 3:Redis 連接失敗
1// 解決方案:使用 @ConditionalOnProperty 條件化 Bean
2@Configuration
3@ConditionalOnProperty(name = "spring.cache.type", havingValue = "redis")
4public class RedisConfig {
5 // Redis 配置...
6}
問題 4:Docker 容器內連接資料庫失敗
1# 錯誤:使用 localhost
2DB_HOST=localhost
3
4# 正確:使用 Docker Compose 服務名稱
5DB_HOST=mysql
🎉 總結
📊 多環境配置架構優勢
1. 開發效率提升
- 開發人員無需安裝 MySQL、Redis 等服務
- 使用 H2 記憶體資料庫快速啟動
- 詳細的日誌輸出便於除錯
2. 測試環境隔離
- 獨立的測試資料庫避免污染生產資料
- 模擬生產環境配置提前發現問題
- 支援自動化測試和持續整合
3. 生產環境安全
- 敏感資訊完全隔離
- 效能優化配置(連接池、快取)
- 完整的監控和日誌管理
4. 維護成本降低
- 統一的配置管理方式
- 清晰的環境區分
- 容易的新環境建立
🚀 延伸學習
進階主題:
- Spring Cloud Config:集中化配置管理
- Kubernetes ConfigMap:容器編排環境配置
- AWS Parameter Store:雲端配置管理
- Vault Integration:密鑰管理整合
相關資源:
💡 核心要點回顧
- ✅ 使用 Spring Profile 管理不同環境配置
- ✅ 敏感資訊必須使用環境變數
- ✅ 開發環境簡化配置,生產環境強化安全
- ✅ Docker Compose 統一管理容器化部署
- ✅ Redis 快取視環境需求啟用/停用
- ✅ 完整的健康檢查和監控機制
透過本文的完整指南,你可以建立一個專業、安全、易維護的 Spring Boot 多環境配置架構,無論是本地開發、測試環境還是生產部署,都能游刃有餘!
🔗 相關資源
| 資源 | 連結 |
|---|---|
| 📂 完整範例程式碼 | GitHub - SpringPlayground |
| 📖 Spring Boot 文檔 | 官方文檔 |
| 🐳 Docker 文檔 | Docker 官方文檔 |
| 📚 相關文章 | Spring Boot 系列文章 |
