程式寫作指引
目錄
前言
本指引旨在幫助開發團隊撰寫高品質、可維護且安全的程式碼。無論您是剛入行的新進開發人員,還是經驗豐富的資深工程師,都可以透過這份指引提升程式設計技能,並確保專案的長期成功。
適用技術棧
- 前端:Vue 3.x、Angular、Tailwind CSS、TypeScript
- 後端:Spring Boot 3.x、Java 17+
- 其他:Maven、Git、JUnit 5、SonarQube
核心原則
- 可讀性優先:程式碼是給人讀的,其次才是給機器執行的
- 安全第一:始終考慮安全性,遵循 OWASP 最佳實踐
- 效能意識:在不犧牲可讀性的前提下,追求最佳效能
- 可維護性:寫出易於修改和擴展的程式碼
- 測試友好:設計時就考慮可測試性
1. 程式碼風格與命名規範
1.1 Java 命名規範
類別命名
- 使用 PascalCase(首字母大寫的駝峰命名法)
- 名稱應具描述性,避免縮寫
// ✅ 良好示例
public class UserService {
// ...
}
public class PaymentProcessor {
// ...
}
// ❌ 避免使用
public class UsrSvc {
// ...
}方法命名
- 使用 camelCase(小駝峰命名法)
- 動詞開頭,清楚表達方法功能
// ✅ 良好示例
public void calculateTotalAmount() { }
public boolean isValidUser() { }
public String getUserDisplayName() { }
// ❌ 避免使用
public void calc() { }
public boolean valid() { }變數命名
- 使用 camelCase
- 避免單字母變數(除了迴圈計數器)
// ✅ 良好示例
private String userName;
private BigDecimal totalAmount;
private List<Order> activeOrders;
// ❌ 避免使用
private String n;
private BigDecimal amt;常數命名
- 使用 UPPER_SNAKE_CASE
- 所有字母大寫,底線分隔
// ✅ 良好示例
public static final String DEFAULT_ENCODING = "UTF-8";
public static final int MAX_RETRY_ATTEMPTS = 3;
private static final Logger LOGGER = LoggerFactory.getLogger(UserService.class);1.2 TypeScript/JavaScript 命名規範
變數與函式
// ✅ 良好示例
const userAccountBalance = 1000;
const isUserAuthenticated = true;
function calculateMonthlyPayment(principal: number, rate: number): number {
// ...
}
// ❌ 避免使用
const bal = 1000;
const auth = true;
function calc(p: number, r: number): number { }介面與型別
// ✅ 良好示例
interface UserProfile {
id: string;
name: string;
email: string;
}
type PaymentMethod = 'credit-card' | 'bank-transfer' | 'digital-wallet';
// Vue 元件命名
const UserProfileCard = defineComponent({
name: 'UserProfileCard'
});1.3 程式碼格式化
Java 格式化規則
// ✅ 良好示例:適當的空格和縮排
public class OrderService {
private final PaymentProcessor paymentProcessor;
private final OrderRepository orderRepository;
public OrderService(PaymentProcessor paymentProcessor,
OrderRepository orderRepository) {
this.paymentProcessor = paymentProcessor;
this.orderRepository = orderRepository;
}
public Order processOrder(OrderRequest request) {
if (request == null || request.getItems().isEmpty()) {
throw new IllegalArgumentException("Order request cannot be null or empty");
}
Order order = createOrder(request);
Payment payment = paymentProcessor.processPayment(order.getTotalAmount());
if (payment.isSuccessful()) {
order.setStatus(OrderStatus.CONFIRMED);
return orderRepository.save(order);
} else {
order.setStatus(OrderStatus.FAILED);
throw new PaymentException("Payment processing failed");
}
}
}TypeScript 格式化規則
// ✅ 良好示例
export class UserService {
private readonly apiClient: ApiClient;
constructor(apiClient: ApiClient) {
this.apiClient = apiClient;
}
async getUserProfile(userId: string): Promise<UserProfile> {
try {
const response = await this.apiClient.get(`/users/${userId}`);
return this.mapToUserProfile(response.data);
} catch (error) {
logger.error('Failed to fetch user profile', { userId, error });
throw new UserServiceError('Unable to retrieve user profile');
}
}
private mapToUserProfile(data: any): UserProfile {
return {
id: data.id,
name: data.fullName,
email: data.emailAddress
};
}
}1.4 實務案例與注意事項
案例:重構不良命名
// ❌ 重構前
public class DataMgr {
public List<Object> getData(String id) {
// ...
}
}
// ✅ 重構後
public class CustomerDataManager {
public List<CustomerRecord> getCustomerRecords(String customerId) {
// ...
}
}注意事項:
- 統一使用英文命名,避免中英混雜
- 避免使用具有誤導性的名稱
- 保持命名風格在整個專案中的一致性
- 使用領域相關的術語,提高程式碼的表達力
- 定期檢視和重構命名,確保其仍然準確描述功能
2. 註解與文件撰寫
2.1 JavaDoc 註解規範
類別註解
/**
* 用戶服務類,負責處理用戶相關的業務邏輯
*
* <p>此類提供用戶註冊、登入、資料更新等核心功能,
* 並確保所有操作符合安全性和資料完整性要求。</p>
*
* @author 開發團隊
* @version 1.2.0
* @since 2024-01-15
* @see UserRepository
* @see UserValidator
*/
@Service
public class UserService {
// ...
}方法註解
/**
* 根據用戶 ID 獲取用戶詳細資訊
*
* @param userId 用戶唯一識別碼,不能為 null 或空字串
* @return 用戶詳細資訊,如果用戶不存在則返回 null
* @throws IllegalArgumentException 當 userId 為 null 或空字串時拋出
* @throws UserServiceException 當資料庫連接失敗或其他系統錯誤時拋出
*
* @example
* <pre>
* UserService userService = new UserService();
* UserProfile profile = userService.getUserProfile("12345");
* if (profile != null) {
* System.out.println("用戶名稱: " + profile.getName());
* }
* </pre>
*/
public UserProfile getUserProfile(String userId) {
if (StringUtils.isBlank(userId)) {
throw new IllegalArgumentException("User ID cannot be null or empty");
}
try {
return userRepository.findById(userId)
.map(this::mapToUserProfile)
.orElse(null);
} catch (Exception e) {
logger.error("Failed to retrieve user profile for ID: {}", userId, e);
throw new UserServiceException("Unable to retrieve user profile", e);
}
}2.2 TypeScript JSDoc 註解
/**
* 用戶資料管理服務
*
* 提供前端用戶資料的 CRUD 操作,包含資料驗證、格式化等功能
*
* @example
* ```typescript
* const userService = new UserDataService();
* const userData = await userService.fetchUserData('12345');
* ```
*/
export class UserDataService {
/**
* 獲取用戶資料
*
* @param userId - 用戶 ID
* @param options - 查詢選項
* @param options.includeProfile - 是否包含詳細資料
* @param options.timeout - 請求超時時間(毫秒)
* @returns Promise 包含用戶資料
*
* @throws {@link NetworkError} 網路連接失敗時拋出
* @throws {@link ValidationError} 參數驗證失敗時拋出
*/
async fetchUserData(
userId: string,
options: {
includeProfile?: boolean;
timeout?: number;
} = {}
): Promise<UserData> {
// 實作內容
}
}2.3 程式碼內註解最佳實踐
解釋複雜邏輯
public BigDecimal calculateCompoundInterest(BigDecimal principal,
BigDecimal rate,
int periods) {
// 使用複利公式: A = P(1 + r)^n
// 其中 A = 最終金額, P = 本金, r = 利率, n = 期數
BigDecimal onePlusRate = BigDecimal.ONE.add(rate);
BigDecimal compoundFactor = onePlusRate.pow(periods);
return principal.multiply(compoundFactor);
}解釋業務規則
public boolean isEligibleForLoan(User user, BigDecimal requestedAmount) {
// 根據銀行政策,用戶必須滿足以下條件才能申請貸款:
// 1. 年齡在 20-65 歲之間
// 2. 信用評分 >= 600
// 3. 月收入 >= 貸款金額的 1/60 (假設 5 年期)
if (user.getAge() < 20 || user.getAge() > 65) {
return false;
}
if (user.getCreditScore() < 600) {
return false;
}
BigDecimal requiredIncome = requestedAmount.divide(new BigDecimal("60"));
return user.getMonthlyIncome().compareTo(requiredIncome) >= 0;
}TODO 和 FIXME 註解
public class OrderProcessor {
public void processOrder(Order order) {
validateOrder(order);
// TODO: 實作庫存檢查邏輯
// 預計完成時間: 2024-02-01
// 負責人: John Doe
calculateOrderTotal(order);
// FIXME: 這裡的稅率計算可能有問題
// 需要重新檢視稅率查詢邏輯
// Bug ID: #12345
applyTaxes(order);
}
}2.4 API 文件撰寫
Spring Boot Controller 註解
@RestController
@RequestMapping("/api/v1/users")
@Api(tags = "用戶管理", description = "用戶資料的 CRUD 操作")
public class UserController {
@PostMapping
@ApiOperation(
value = "創建新用戶",
notes = "創建新的用戶帳戶,包含基本資料驗證"
)
@ApiResponses({
@ApiResponse(code = 201, message = "用戶創建成功"),
@ApiResponse(code = 400, message = "請求參數無效"),
@ApiResponse(code = 409, message = "用戶已存在")
})
public ResponseEntity<UserResponse> createUser(
@ApiParam(value = "用戶創建請求", required = true)
@Valid @RequestBody CreateUserRequest request
) {
// 實作內容
}
}2.5 Vue 元件註解
<template>
<!-- 用戶資料卡片元件 -->
<div class="user-profile-card">
<div class="avatar-section">
<!-- 用戶頭像,支援預設圖片和上傳功能 -->
<img :src="userAvatar" :alt="userAltText" />
</div>
<div class="info-section">
<!-- 用戶基本資訊顯示區域 -->
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</div>
</div>
</template>
<script setup lang="ts">
/**
* 用戶資料卡片元件
*
* 顯示用戶的基本資訊,包含頭像、姓名、email 等
* 支援響應式設計和暗色主題
*
* @example
* ```vue
* <UserProfileCard
* :user="currentUser"
* :show-actions="true"
* @edit-clicked="handleEdit"
* />
* ```
*/
interface Props {
/** 用戶資料物件 */
user: UserProfile;
/** 是否顯示操作按鈕 */
showActions?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
showActions: false
});
// 計算用戶頭像 URL,如果沒有則使用預設圖片
const userAvatar = computed(() => {
return props.user.avatar || '/images/default-avatar.png';
});
</script>2.6 實務案例與注意事項
案例:改善註解品質
// ❌ 糟糕的註解
public class Calculator {
// 加法
public int add(int a, int b) {
return a + b; // 返回結果
}
}
// ✅ 改善後的註解
public class Calculator {
/**
* 執行兩個整數的加法運算
*
* @param a 第一個加數
* @param b 第二個加數
* @return 兩數之和
* @throws ArithmeticException 當結果超出 int 範圍時拋出
*/
public int add(int a, int b) {
// 檢查是否會發生整數溢位
if (a > 0 && b > Integer.MAX_VALUE - a) {
throw new ArithmeticException("Integer overflow");
}
if (a < 0 && b < Integer.MIN_VALUE - a) {
throw new ArithmeticException("Integer underflow");
}
return a + b;
}
}注意事項:
- 避免冗餘註解:不要為顯而易見的程式碼添加註解
- 保持註解更新:程式碼修改時,務必同步更新相關註解
- 使用標準格式:遵循 JavaDoc 或 JSDoc 標準格式
- 關注為什麼而非什麼:解釋程式碼的目的和原因,而非具體做法
- 使用範例:對於複雜的 API,提供使用範例
- 多語言考量:如果是國際化專案,考慮使用英文註解
3. 錯誤處理與日誌紀錄
3.1 例外處理最佳實踐
Java 例外處理規範
/**
* 用戶服務例外處理示例
*/
@Service
public class UserService {
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
/**
* 創建用戶帳戶
*/
public UserAccount createUserAccount(CreateUserRequest request) {
try {
// 1. 輸入驗證
validateCreateUserRequest(request);
// 2. 業務邏輯處理
UserAccount account = buildUserAccount(request);
// 3. 資料持久化
UserAccount savedAccount = userRepository.save(account);
logger.info("User account created successfully: {}", savedAccount.getId());
return savedAccount;
} catch (ValidationException e) {
logger.warn("User creation failed due to validation error: {}", e.getMessage());
throw new BadRequestException("Invalid user data: " + e.getMessage(), e);
} catch (DuplicateUserException e) {
logger.warn("Attempted to create duplicate user: {}", request.getEmail());
throw new ConflictException("User already exists", e);
} catch (DataAccessException e) {
logger.error("Database error during user creation: {}", e.getMessage(), e);
throw new ServiceUnavailableException("Unable to create user account", e);
} catch (Exception e) {
logger.error("Unexpected error during user creation: {}", e.getMessage(), e);
throw new InternalServerErrorException("Internal server error", e);
}
}
/**
* 驗證創建用戶請求
*/
private void validateCreateUserRequest(CreateUserRequest request) {
if (request == null) {
throw new ValidationException("Request cannot be null");
}
if (StringUtils.isBlank(request.getEmail())) {
throw new ValidationException("Email is required");
}
if (!EmailValidator.isValid(request.getEmail())) {
throw new ValidationException("Invalid email format");
}
if (StringUtils.isBlank(request.getPassword()) ||
request.getPassword().length() < 8) {
throw new ValidationException("Password must be at least 8 characters");
}
}
}3.2 日誌記錄最佳實踐
日誌級別使用指南
// TRACE: 最詳細的信息,通常只在診斷問題時開啟
logger.trace("Entering method getUserById with parameter: {}", userId);
// DEBUG: 調試信息,開發和測試環境使用
logger.debug("Database query executed: {}", query);
// INFO: 一般信息,記錄重要的業務流程
logger.info("User {} successfully logged in", username);
// WARN: 警告信息,不影響系統運行但需要注意
logger.warn("User {} failed login attempt #{}", username, attemptCount);
// ERROR: 錯誤信息,系統出現錯誤但仍能繼續運行
logger.error("Failed to process payment for order {}: {}", orderId, e.getMessage(), e);
// FATAL: 嚴重錯誤,系統無法繼續運行(較少使用)
logger.fatal("Database connection lost, shutting down application");結構化日誌記錄
@Component
public class StructuredLogger {
private static final Logger logger = LoggerFactory.getLogger(StructuredLogger.class);
private final ObjectMapper objectMapper = new ObjectMapper();
public void logUserAction(String userId, String action, Object details) {
try {
Map<String, Object> logData = Map.of(
"timestamp", Instant.now(),
"userId", userId,
"action", action,
"details", details,
"source", "user-service"
);
logger.info("USER_ACTION: {}", objectMapper.writeValueAsString(logData));
} catch (Exception e) {
logger.error("Failed to log user action", e);
}
}
public void logPerformanceMetrics(String operation, long durationMs, boolean success) {
Map<String, Object> metrics = Map.of(
"timestamp", Instant.now(),
"operation", operation,
"duration_ms", durationMs,
"success", success
);
logger.info("PERFORMANCE_METRIC: {}", metrics);
}
}敏感資料遮蔽
@Component
public class DataMasker {
public static String maskEmail(String email) {
if (email == null || email.length() < 3) {
return "***";
}
int atIndex = email.indexOf('@');
if (atIndex <= 0) {
return "***";
}
String localPart = email.substring(0, atIndex);
String domain = email.substring(atIndex);
if (localPart.length() <= 2) {
return "*".repeat(localPart.length()) + domain;
}
return localPart.charAt(0) + "*".repeat(localPart.length() - 2) +
localPart.charAt(localPart.length() - 1) + domain;
}
public static String maskCreditCard(String cardNumber) {
if (cardNumber == null || cardNumber.length() < 4) {
return "****";
}
return "*".repeat(cardNumber.length() - 4) +
cardNumber.substring(cardNumber.length() - 4);
}
public static String maskPhone(String phone) {
if (phone == null || phone.length() < 4) {
return "****";
}
return "*".repeat(phone.length() - 4) +
phone.substring(phone.length() - 4);
}
}MDC (Mapped Diagnostic Context) 使用
@Component
public class RequestTrackingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 設置追蹤資訊
String traceId = UUID.randomUUID().toString();
String userId = extractUserId(httpRequest);
String clientIp = getClientIpAddress(httpRequest);
try {
MDC.put("traceId", traceId);
MDC.put("userId", userId);
MDC.put("clientIp", clientIp);
MDC.put("requestUri", httpRequest.getRequestURI());
chain.doFilter(request, response);
} finally {
// 清理 MDC
MDC.clear();
}
}
private String extractUserId(HttpServletRequest request) {
// 從 JWT token 或 session 中提取用戶ID
return "unknown";
}
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return request.getRemoteAddr();
}
}日誌配置最佳實踐
Log4j2 配置範例:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Properties>
<Property name="LOG_PATTERN">
%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} [%X{traceId}] [%X{userId}] - %msg%n
</Property>
<Property name="LOG_DIR">./logs</Property>
</Properties>
<Appenders>
<!-- 控制台輸出 -->
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="${LOG_PATTERN}"/>
</Console>
<!-- 應用程式日誌 -->
<RollingFile name="AppFile" fileName="${LOG_DIR}/application.log"
filePattern="${LOG_DIR}/application-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<SizeBasedTriggeringPolicy size="100MB"/>
<TimeBasedTriggeringPolicy />
</Policies>
<DefaultRolloverStrategy max="30"/>
</RollingFile>
<!-- 錯誤日誌 -->
<RollingFile name="ErrorFile" fileName="${LOG_DIR}/error.log"
filePattern="${LOG_DIR}/error-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="${LOG_PATTERN}"/>
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY"/>
<Policies>
<SizeBasedTriggeringPolicy size="50MB"/>
<TimeBasedTriggeringPolicy />
</Policies>
<DefaultRolloverStrategy max="30"/>
</RollingFile>
<!-- 審計日誌 -->
<RollingFile name="AuditFile" fileName="${LOG_DIR}/audit.log"
filePattern="${LOG_DIR}/audit-%d{yyyy-MM-dd}-%i.log.gz">
<JsonLayout complete="false" compact="true"/>
<Policies>
<SizeBasedTriggeringPolicy size="100MB"/>
<TimeBasedTriggeringPolicy />
</Policies>
<DefaultRolloverStrategy max="90"/>
</RollingFile>
</Appenders>
<Loggers>
<!-- 特定套件的日誌級別 -->
<Logger name="com.tutorial" level="DEBUG" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="AppFile"/>
</Logger>
<Logger name="org.springframework" level="INFO" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="AppFile"/>
</Logger>
<!-- 審計專用 Logger -->
<Logger name="AUDIT" level="INFO" additivity="false">
<AppenderRef ref="AuditFile"/>
</Logger>
<Root level="INFO">
<AppenderRef ref="Console"/>
<AppenderRef ref="AppFile"/>
<AppenderRef ref="ErrorFile"/>
</Root>
</Loggers>
</Configuration>效能考量
@Service
public class OptimizedLoggingService {
private static final Logger logger = LoggerFactory.getLogger(OptimizedLoggingService.class);
public void processLargeDataset(List<Data> dataset) {
// ✅ 使用 isDebugEnabled 避免不必要的字串串接
if (logger.isDebugEnabled()) {
logger.debug("Processing dataset with {} items: {}",
dataset.size(),
dataset.stream()
.map(Data::getId)
.collect(Collectors.toList()));
}
// ✅ 使用參數化日誌
logger.info("Started processing {} items", dataset.size());
// ❌ 避免字串串接
// logger.info("Started processing " + dataset.size() + " items");
long startTime = System.currentTimeMillis();
try {
// 處理資料...
logger.info("Successfully processed {} items in {} ms",
dataset.size(),
System.currentTimeMillis() - startTime);
} catch (Exception e) {
logger.error("Failed to process dataset: {}", e.getMessage(), e);
throw e;
}
}
}3.3 監控與告警設定
應用程式健康檢查
@Component
@HealthIndicator
public class CustomHealthIndicator implements HealthIndicator {
private final DatabaseHealthChecker databaseChecker;
private final ExternalServiceChecker serviceChecker;
@Override
public Health health() {
Health.Builder builder = Health.up();
// 檢查資料庫連接
if (!databaseChecker.isHealthy()) {
builder.down().withDetail("database", "Connection failed");
}
// 檢查外部服務
if (!serviceChecker.isHealthy()) {
builder.down().withDetail("external-service", "Service unavailable");
}
// 檢查記憶體使用量
MemoryUsage memoryUsage = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage();
double memoryUsedPercent = (double) memoryUsage.getUsed() / memoryUsage.getMax() * 100;
if (memoryUsedPercent > 90) {
builder.down().withDetail("memory", "High memory usage: " + memoryUsedPercent + "%");
}
return builder
.withDetail("timestamp", Instant.now())
.withDetail("memory-used-percent", memoryUsedPercent)
.build();
}
}自定義指標收集
@Component
public class MetricsCollector {
private final MeterRegistry meterRegistry;
private final Counter userRegistrationCounter;
private final Timer requestTimer;
private final Gauge activeUsersGauge;
public MetricsCollector(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.userRegistrationCounter = Counter.builder("user.registrations")
.description("Number of user registrations")
.register(meterRegistry);
this.requestTimer = Timer.builder("http.requests")
.description("HTTP request duration")
.register(meterRegistry);
this.activeUsersGauge = Gauge.builder("active.users")
.description("Number of active users")
.register(meterRegistry, this, MetricsCollector::getActiveUserCount);
}
public void recordUserRegistration(String userType) {
userRegistrationCounter.increment(Tags.of("type", userType));
}
public Timer.Sample startRequestTimer(String endpoint) {
return Timer.start(meterRegistry);
}
public void recordRequestTime(Timer.Sample sample, String endpoint, String status) {
sample.stop(Timer.builder("http.requests")
.tags("endpoint", endpoint, "status", status)
.register(meterRegistry));
}
private double getActiveUserCount() {
// 實際的活躍用戶計算邏輯
return 0.0;
}
}告警規則設定
@Component
public class AlertManager {
private static final Logger alertLogger = LoggerFactory.getLogger("ALERT");
private final NotificationService notificationService;
private final MetricsCollector metricsCollector;
@EventListener
public void handleHealthCheckFailed(HealthCheckFailedEvent event) {
Alert alert = Alert.builder()
.severity(Severity.HIGH)
.title("Health Check Failed")
.description("Service health check failed: " + event.getDetails())
.timestamp(Instant.now())
.build();
sendAlert(alert);
}
@Scheduled(fixedRate = 60000) // 每分鐘檢查一次
public void checkSystemMetrics() {
// 檢查記憶體使用率
MemoryUsage memory = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage();
double memoryUsage = (double) memory.getUsed() / memory.getMax() * 100;
if (memoryUsage > 85) {
Alert alert = Alert.builder()
.severity(memoryUsage > 95 ? Severity.CRITICAL : Severity.WARNING)
.title("High Memory Usage")
.description(String.format("Memory usage is %.2f%%", memoryUsage))
.timestamp(Instant.now())
.build();
sendAlert(alert);
}
// 檢查錯誤率
checkErrorRate();
}
private void checkErrorRate() {
// 檢查過去5分鐘的錯誤率
Counter errorCounter = meterRegistry.counter("http.requests", "status", "error");
Counter totalCounter = meterRegistry.counter("http.requests");
double errorRate = errorCounter.count() / totalCounter.count() * 100;
if (errorRate > 5) {
Alert alert = Alert.builder()
.severity(errorRate > 10 ? Severity.CRITICAL : Severity.WARNING)
.title("High Error Rate")
.description(String.format("Error rate is %.2f%%", errorRate))
.timestamp(Instant.now())
.build();
sendAlert(alert);
}
}
private void sendAlert(Alert alert) {
alertLogger.error("ALERT: {} - {} - {}",
alert.getSeverity(),
alert.getTitle(),
alert.getDescription());
// 發送通知到 Slack、Email 等
notificationService.sendAlert(alert);
}
}3.4 實務案例與注意事項
案例:敏感資訊的日誌記錄
// ❌ 錯誤做法:記錄敏感資訊
logger.info("User login: email={}, password={}", user.getEmail(), user.getPassword());
// ✅ 正確做法:遮蔽敏感資訊
logger.info("User login: email={}, passwordLength={}",
maskEmail(user.getEmail()),
user.getPassword().length());
private String maskEmail(String email) {
if (email == null || email.length() < 3) {
return "***";
}
int atIndex = email.indexOf('@');
if (atIndex <= 0) {
return "***";
}
return email.charAt(0) + "***" + email.substring(atIndex);
}注意事項:
- 永不記錄敏感資訊:密碼、身分證字號、信用卡號等
- 使用結構化日誌:便於後續分析和監控
- 適當的日誌級別:DEBUG < INFO < WARN < ERROR
- 包含足夠上下文:使用 MDC 或結構化資料
- 定期清理日誌:避免磁碟空間不足
- 監控日誌檔案:設置日誌警報和監控
4. 單元測試與測試驅動開發(TDD)
4.1 JUnit 5 測試規範
基本測試結構
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private EmailService emailService;
@InjectMocks
private UserService userService;
private User testUser;
private CreateUserRequest validRequest;
@BeforeEach
void setUp() {
testUser = User.builder()
.id("user123")
.email("test@example.com")
.name("Test User")
.build();
validRequest = CreateUserRequest.builder()
.email("test@example.com")
.name("Test User")
.password("securePassword123")
.build();
}
@Test
@DisplayName("應該成功創建用戶當提供有效資料時")
void shouldCreateUserSuccessfully_WhenValidDataProvided() {
// Given
when(userRepository.existsByEmail(validRequest.getEmail())).thenReturn(false);
when(userRepository.save(any(User.class))).thenReturn(testUser);
// When
User result = userService.createUser(validRequest);
// Then
assertThat(result).isNotNull();
assertThat(result.getEmail()).isEqualTo(validRequest.getEmail());
assertThat(result.getName()).isEqualTo(validRequest.getName());
verify(userRepository).existsByEmail(validRequest.getEmail());
verify(userRepository).save(argThat(user ->
user.getEmail().equals(validRequest.getEmail()) &&
user.getName().equals(validRequest.getName())
));
verify(emailService).sendWelcomeEmail(testUser);
}
@Test
@DisplayName("應該拋出例外當用戶已存在時")
void shouldThrowException_WhenUserAlreadyExists() {
// Given
when(userRepository.existsByEmail(validRequest.getEmail())).thenReturn(true);
// When & Then
assertThatThrownBy(() -> userService.createUser(validRequest))
.isInstanceOf(DuplicateUserException.class)
.hasMessage("User already exists with email: " + validRequest.getEmail());
verify(userRepository, never()).save(any(User.class));
verify(emailService, never()).sendWelcomeEmail(any(User.class));
}
@ParameterizedTest
@DisplayName("應該拋出驗證例外當提供無效 email 時")
@ValueSource(strings = {"", " ", "invalid-email", "@example.com", "test@"})
void shouldThrowValidationException_WhenInvalidEmailProvided(String invalidEmail) {
// Given
validRequest.setEmail(invalidEmail);
// When & Then
assertThatThrownBy(() -> userService.createUser(validRequest))
.isInstanceOf(ValidationException.class)
.hasMessageContaining("Invalid email");
}
@Test
@DisplayName("應該處理資料庫例外並重新拋出服務例外")
void shouldHandleDatabaseException_AndRethrowServiceException() {
// Given
when(userRepository.existsByEmail(anyString())).thenReturn(false);
when(userRepository.save(any(User.class)))
.thenThrow(new DataIntegrityViolationException("Database constraint violation"));
// When & Then
assertThatThrownBy(() -> userService.createUser(validRequest))
.isInstanceOf(UserServiceException.class)
.hasMessageContaining("Failed to create user")
.hasCauseInstanceOf(DataIntegrityViolationException.class);
}
}整合測試範例
@SpringBootTest
@Testcontainers
@Transactional
class UserServiceIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@Autowired
private UserService userService;
@Autowired
private UserRepository userRepository;
@Autowired
private TestEntityManager entityManager;
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Test
@DisplayName("完整用戶創建流程測試")
void shouldCompleteUserCreationFlow() {
// Given
CreateUserRequest request = CreateUserRequest.builder()
.email("integration@test.com")
.name("Integration Test User")
.password("securePassword123")
.build();
// When
User createdUser = userService.createUser(request);
entityManager.flush();
entityManager.clear();
// Then
assertThat(createdUser.getId()).isNotNull();
Optional<User> foundUser = userRepository.findById(createdUser.getId());
assertThat(foundUser).isPresent();
assertThat(foundUser.get().getEmail()).isEqualTo(request.getEmail());
assertThat(foundUser.get().getName()).isEqualTo(request.getName());
assertThat(foundUser.get().getCreatedAt()).isNotNull();
}
}4.2 TypeScript/Jest 測試規範
Vue 元件測試
import { mount } from '@vue/test-utils';
import { describe, it, expect, vi } from 'vitest';
import UserProfileCard from '@/components/UserProfileCard.vue';
import type { UserProfile } from '@/types/user';
describe('UserProfileCard', () => {
const mockUser: UserProfile = {
id: '123',
name: 'John Doe',
email: 'john@example.com',
avatar: 'https://example.com/avatar.jpg'
};
it('應該正確顯示用戶資訊', () => {
// Given
const wrapper = mount(UserProfileCard, {
props: { user: mockUser }
});
// Then
expect(wrapper.find('[data-testid="user-name"]').text()).toBe(mockUser.name);
expect(wrapper.find('[data-testid="user-email"]').text()).toBe(mockUser.email);
expect(wrapper.find('[data-testid="user-avatar"]').attributes('src')).toBe(mockUser.avatar);
});
it('應該在點擊編輯按鈕時發出事件', async () => {
// Given
const wrapper = mount(UserProfileCard, {
props: { user: mockUser, showActions: true }
});
// When
await wrapper.find('[data-testid="edit-button"]').trigger('click');
// Then
expect(wrapper.emitted('edit-clicked')).toBeTruthy();
expect(wrapper.emitted('edit-clicked')?.[0]).toEqual([mockUser.id]);
});
it('應該在用戶沒有頭像時顯示預設圖片', () => {
// Given
const userWithoutAvatar = { ...mockUser, avatar: '' };
const wrapper = mount(UserProfileCard, {
props: { user: userWithoutAvatar }
});
// Then
expect(wrapper.find('[data-testid="user-avatar"]').attributes('src'))
.toBe('/images/default-avatar.png');
});
});API 服務測試
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UserApiService } from '@/services/UserApiService';
import type { User } from '@/types/user';
// Mock fetch
global.fetch = vi.fn();
describe('UserApiService', () => {
let userService: UserApiService;
beforeEach(() => {
userService = new UserApiService('http://localhost:8080');
vi.resetAllMocks();
});
it('應該成功獲取用戶資料', async () => {
// Given
const mockUser: User = {
id: '123',
name: 'John Doe',
email: 'john@example.com'
};
(fetch as jest.MockedFunction<typeof fetch>).mockResolvedValueOnce({
ok: true,
json: async () => mockUser
} as Response);
// When
const result = await userService.getUser('123');
// Then
expect(result).toEqual(mockUser);
expect(fetch).toHaveBeenCalledWith(
'http://localhost:8080/users/123',
expect.objectContaining({
method: 'GET',
headers: expect.objectContaining({
'Content-Type': 'application/json'
})
})
);
});
it('應該在用戶不存在時拋出錯誤', async () => {
// Given
(fetch as jest.MockedFunction<typeof fetch>).mockResolvedValueOnce({
ok: false,
status: 404,
json: async () => ({ message: 'User not found' })
} as Response);
// When & Then
await expect(userService.getUser('999')).rejects.toThrow('User not found');
});
});4.3 測試驅動開發(TDD)流程
紅燈-綠燈-重構循環
// 1. 紅燈:先寫測試(會失敗)
@Test
@DisplayName("應該計算正確的複利金額")
void shouldCalculateCompoundInterestCorrectly() {
// Given
BigDecimal principal = new BigDecimal("1000");
BigDecimal annualRate = new BigDecimal("0.05"); // 5%
int years = 2;
// When
BigDecimal result = calculator.calculateCompoundInterest(principal, annualRate, years);
// Then
BigDecimal expected = new BigDecimal("1102.50");
assertThat(result).isEqualByComparingTo(expected);
}
// 2. 綠燈:寫最少的程式碼讓測試通過
public class Calculator {
public BigDecimal calculateCompoundInterest(BigDecimal principal,
BigDecimal annualRate,
int years) {
// 最簡單的實作讓測試通過
return new BigDecimal("1102.50");
}
}
// 3. 重構:改善程式碼品質
public class Calculator {
public BigDecimal calculateCompoundInterest(BigDecimal principal,
BigDecimal annualRate,
int years) {
if (principal == null || annualRate == null) {
throw new IllegalArgumentException("Principal and rate cannot be null");
}
if (years < 0) {
throw new IllegalArgumentException("Years must be non-negative");
}
// A = P(1 + r)^n
BigDecimal onePlusRate = BigDecimal.ONE.add(annualRate);
BigDecimal compoundFactor = onePlusRate.pow(years);
return principal.multiply(compoundFactor)
.setScale(2, RoundingMode.HALF_UP);
}
}4.4 測試覆蓋率與品質指標
Maven 配置(pom.xml)
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.8</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>4.5 實務案例與注意事項
案例:測試資料建構器模式
public class UserTestDataBuilder {
private String id = "default-id";
private String email = "test@example.com";
private String name = "Test User";
private UserStatus status = UserStatus.ACTIVE;
private LocalDateTime createdAt = LocalDateTime.now();
public static UserTestDataBuilder aUser() {
return new UserTestDataBuilder();
}
public UserTestDataBuilder withId(String id) {
this.id = id;
return this;
}
public UserTestDataBuilder withEmail(String email) {
this.email = email;
return this;
}
public UserTestDataBuilder withName(String name) {
this.name = name;
return this;
}
public UserTestDataBuilder withStatus(UserStatus status) {
this.status = status;
return this;
}
public UserTestDataBuilder inactive() {
this.status = UserStatus.INACTIVE;
return this;
}
public User build() {
return User.builder()
.id(id)
.email(email)
.name(name)
.status(status)
.createdAt(createdAt)
.build();
}
}
// 使用範例
@Test
void shouldDeactivateInactiveUser() {
// Given
User user = aUser().withEmail("test@example.com").inactive().build();
// When & Then
assertThatThrownBy(() -> userService.deactivateUser(user.getId()))
.isInstanceOf(InvalidOperationException.class);
}注意事項:
- 遵循 AAA 模式:Arrange(準備)、Act(執行)、Assert(驗證)
- 一個測試一個概念:每個測試方法只驗證一個行為
- 使用描述性測試名稱:清楚表達測試意圖
- 獨立的測試:測試之間不應該有依賴關係
- 快速執行:單元測試應該能快速執行
- 保持測試簡單:測試程式碼應該比產品程式碼更簡單
5. 安全性考量
5.1 輸入驗證與資料清理
基本輸入驗證
@RestController
@RequestMapping("/api/users")
@Validated
public class UserController {
@PostMapping
public ResponseEntity<UserResponse> createUser(
@Valid @RequestBody CreateUserRequest request) {
// ✅ 使用 Bean Validation 進行基本驗證
// ✅ @Valid 註解會自動觸發驗證
UserResponse response = userService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
}
// DTO 中的驗證註解
public class CreateUserRequest {
@NotBlank(message = "姓名不能為空")
@Size(min = 2, max = 50, message = "姓名長度必須在2-50字符之間")
@Pattern(regexp = "^[\\p{L}\\s]+$", message = "姓名只能包含字母和空格")
private String name;
@NotBlank(message = "電子郵件不能為空")
@Email(message = "電子郵件格式不正確")
@Size(max = 100, message = "電子郵件長度不能超過100字符")
private String email;
@NotBlank(message = "密碼不能為空")
@Size(min = 8, max = 128, message = "密碼長度必須在8-128字符之間")
@Pattern(
regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]+$",
message = "密碼必須包含大小寫字母、數字和特殊字符"
)
private String password;
// getters and setters...
}SQL 注入防護
@Repository
public class UserRepositoryImpl {
@Autowired
private JdbcTemplate jdbcTemplate;
// ✅ 使用參數化查詢防止SQL注入
public List<User> findUsersByStatus(String status) {
String sql = "SELECT * FROM users WHERE status = ?";
return jdbcTemplate.query(sql, new Object[]{status}, new UserRowMapper());
}
// ✅ 使用 JPA 查詢防止SQL注入
@Query("SELECT u FROM User u WHERE u.email = :email AND u.status = :status")
Optional<User> findByEmailAndStatus(@Param("email") String email,
@Param("status") UserStatus status);
// ❌ 避免字串拼接查詢
public List<User> findUsersBadExample(String status) {
String sql = "SELECT * FROM users WHERE status = '" + status + "'"; // 危險!
return jdbcTemplate.query(sql, new UserRowMapper());
}
}5.2 認證和授權
Spring Security 基本配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // 使用強度12的BCrypt
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.GET, "/api/users/**").hasRole("USER")
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
)
.logout(logout -> logout
.logoutUrl("/api/auth/logout")
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(1)
.maxSessionsPreventsLogin(false)
);
return http.build();
}
}5.3 XSS 攻擊防護
前端 XSS 防護
// XSS 防護工具函數
export class SecurityUtils {
/**
* HTML 編碼防止 XSS
*/
static escapeHtml(unsafe: string): string {
if (!unsafe) return unsafe;
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
/**
* 清理 URL 防止 JavaScript 協議
*/
static sanitizeUrl(url: string): string {
if (!url) return url;
const dangerous = /^(javascript|data|vbscript):/i;
if (dangerous.test(url)) {
return 'about:blank';
}
return url;
}
/**
* 驗證和清理用戶輸入
*/
static validateInput(input: string, maxLength: number = 1000): string {
if (!input) return '';
// 移除危險字符
let cleaned = input.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
cleaned = cleaned.replace(/javascript:/gi, '');
cleaned = cleaned.replace(/on\w+\s*=/gi, '');
// 長度限制
if (cleaned.length > maxLength) {
cleaned = cleaned.substring(0, maxLength);
}
return this.escapeHtml(cleaned);
}
}
// Vue 組件中的安全使用
export default defineComponent({
name: 'SafeContent',
setup() {
const userInput = ref('');
const safeContent = computed(() => {
return SecurityUtils.validateInput(userInput.value);
});
const handleSubmit = () => {
const cleanData = {
content: SecurityUtils.validateInput(userInput.value),
// 其他字段也進行相同處理
};
// 提交清理後的資料
api.post('/api/content', cleanData);
};
return {
userInput,
safeContent,
handleSubmit
};
}
});5.4 CSRF 攻擊防護
@Configuration
public class CsrfConfig {
@Bean
public CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setHeaderName("X-XSRF-TOKEN");
return repository;
}
}
// 前端 CSRF Token 處理
export class CsrfService {
static getToken(): string | null {
const metaTag = document.querySelector('meta[name="csrf-token"]');
return metaTag ? metaTag.getAttribute('content') : null;
}
static setupAxiosInterceptors() {
axios.interceptors.request.use((config) => {
const token = this.getToken();
if (token) {
config.headers['X-XSRF-TOKEN'] = token;
}
return config;
});
}
}5.5 敏感資料處理
資料加密
@Component
public class EncryptionService {
@Value("${app.encryption.key}")
private String encryptionKey;
private static final String ALGORITHM = "AES/GCM/NoPadding";
public String encrypt(String plainText) {
try {
SecretKeySpec keySpec = new SecretKeySpec(
Base64.getDecoder().decode(encryptionKey), "AES");
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] encryptedData = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encryptedData);
} catch (Exception e) {
throw new CryptographyException("加密失敗", e);
}
}
public String decrypt(String encryptedText) {
try {
SecretKeySpec keySpec = new SecretKeySpec(
Base64.getDecoder().decode(encryptionKey), "AES");
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] decryptedData = cipher.doFinal(Base64.getDecoder().decode(encryptedText));
return new String(decryptedData, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new CryptographyException("解密失敗", e);
}
}
}
// 自動加密實體字段
@Entity
public class UserProfile {
@Convert(converter = EncryptedStringConverter.class)
@Column(name = "id_number")
private String idNumber; // 身分證號碼 - 自動加密
@Convert(converter = EncryptedStringConverter.class)
@Column(name = "phone_number")
private String phoneNumber; // 電話號碼 - 自動加密
// 其他字段...
}
@Converter
public class EncryptedStringConverter implements AttributeConverter<String, String> {
@Autowired
private EncryptionService encryptionService;
@Override
public String convertToDatabaseColumn(String attribute) {
return attribute == null ? null : encryptionService.encrypt(attribute);
}
@Override
public String convertToEntityAttribute(String dbData) {
return dbData == null ? null : encryptionService.decrypt(dbData);
}
}5.6 安全標頭配置
@Configuration
public class SecurityHeadersConfig {
@Bean
public FilterRegistrationBean<SecurityHeadersFilter> securityHeadersFilter() {
FilterRegistrationBean<SecurityHeadersFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new SecurityHeadersFilter());
registration.addUrlPatterns("/*");
return registration;
}
}
public class SecurityHeadersFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
// Content Security Policy
httpResponse.setHeader("Content-Security-Policy",
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'");
// X-Frame-Options
httpResponse.setHeader("X-Frame-Options", "DENY");
// X-Content-Type-Options
httpResponse.setHeader("X-Content-Type-Options", "nosniff");
// X-XSS-Protection
httpResponse.setHeader("X-XSS-Protection", "1; mode=block");
// Strict-Transport-Security
httpResponse.setHeader("Strict-Transport-Security",
"max-age=31536000; includeSubDomains");
chain.doFilter(request, response);
}
}安全性注意事項:
- 永遠驗證輸入:不信任任何外部輸入
- 使用參數化查詢:防止 SQL 注入
- 適當的輸出編碼:防止 XSS 攻擊
- 實施認證和授權:確保適當的存取控制
- 記錄安全事件:監控可疑活動
- 定期安全審查:保持安全最佳實踐
6. Spring Boot 常用功能實踐
6.1 JWT (JSON Web Token) 認證授權
JWT 配置類
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors()
.and()
.csrf().disable()
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/users/**").hasRole("USER")
.requestMatchers(HttpMethod.POST, "/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.exceptionHandling(ex -> ex
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}JWT 工具類
@Component
public class JwtTokenUtil {
private static final String SECRET = "mySecretKey";
private static final int JWT_TOKEN_VALIDITY = 5 * 60 * 60; // 5小時
/**
* 從 token 中獲取用戶名
*/
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
/**
* 從 token 中獲取過期日期
*/
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
/**
* 獲取所有 claims
*/
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
}
/**
* 檢查 token 是否過期
*/
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
/**
* 為用戶生成 token
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
/**
* 創建 token
*/
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
/**
* 驗證 token
*/
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}6.2 Spring Data JPA 最佳實踐
實體類設計
@Entity
@Table(name = "users", indexes = {
@Index(name = "idx_user_email", columnList = "email"),
@Index(name = "idx_user_status", columnList = "status")
})
@EntityListeners(AuditingEntityListener.class)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 100)
@Email(message = "Email format is invalid")
private String email;
@Column(nullable = false, length = 50)
@Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
private String name;
@Column(nullable = false)
@JsonIgnore
private String password;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private UserStatus status = UserStatus.ACTIVE;
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@Version
private Long version;
// 一對多關聯:用戶擁有多個訂單
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JsonIgnore
private Set<Order> orders = new HashSet<>();
// 多對多關聯:用戶擁有多個角色
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
// 建構函數、getter、setter 省略...
}Repository 層設計
@Repository
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
/**
* 根據 email 查找用戶
*/
Optional<User> findByEmail(String email);
/**
* 根據狀態查找用戶(分頁)
*/
Page<User> findByStatus(UserStatus status, Pageable pageable);
/**
* 使用 JPQL 查詢:查找活躍用戶
*/
@Query("SELECT u FROM User u WHERE u.status = 'ACTIVE' AND u.createdAt >= :since")
List<User> findActiveUsersSince(@Param("since") LocalDateTime since);
/**
* 使用原生 SQL 查詢:複雜統計查詢
*/
@Query(value = """
SELECT u.status, COUNT(*) as count
FROM users u
WHERE u.created_at >= :startDate
GROUP BY u.status
""", nativeQuery = true)
List<Object[]> getUserStatistics(@Param("startDate") LocalDateTime startDate);
/**
* 自定義更新操作
*/
@Modifying
@Query("UPDATE User u SET u.status = :status WHERE u.id = :userId")
int updateUserStatus(@Param("userId") Long userId, @Param("status") UserStatus status);
/**
* 批次操作:批次更新用戶狀態
*/
@Modifying
@Query("UPDATE User u SET u.status = :newStatus WHERE u.id IN :userIds")
int batchUpdateStatus(@Param("userIds") List<Long> userIds,
@Param("newStatus") UserStatus newStatus);
}6.3 Spring Batch 批次處理
批次配置
@Configuration
@EnableBatchProcessing
public class BatchConfig {
@Autowired
private JobBuilderFactory jobBuilderFactory;
@Autowired
private StepBuilderFactory stepBuilderFactory;
@Autowired
private DataSource dataSource;
/**
* 用戶資料導入作業
*/
@Bean
public Job userImportJob() {
return jobBuilderFactory.get("userImportJob")
.incrementer(new RunIdIncrementer())
.listener(new JobCompletionNotificationListener())
.start(userImportStep())
.build();
}
/**
* 用戶資料導入步驟
*/
@Bean
public Step userImportStep() {
return stepBuilderFactory.get("userImportStep")
.<UserImportDto, User>chunk(100)
.reader(userItemReader())
.processor(userItemProcessor())
.writer(userItemWriter())
.faultTolerant()
.skip(Exception.class)
.skipLimit(10)
.listener(new UserImportStepListener())
.build();
}
/**
* CSV 檔案讀取器
*/
@Bean
public FlatFileItemReader<UserImportDto> userItemReader() {
return new FlatFileItemReaderBuilder<UserImportDto>()
.name("userItemReader")
.resource(new ClassPathResource("users.csv"))
.delimited()
.names("email", "name", "department", "role")
.targetType(UserImportDto.class)
.build();
}
/**
* 資料處理器
*/
@Bean
public ItemProcessor<UserImportDto, User> userItemProcessor() {
return new UserItemProcessor();
}
/**
* 資料庫寫入器
*/
@Bean
public JdbcBatchItemWriter<User> userItemWriter() {
return new JdbcBatchItemWriterBuilder<User>()
.itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>())
.sql("INSERT INTO users (email, name, password, status, created_at) " +
"VALUES (:email, :name, :password, :status, :createdAt)")
.dataSource(dataSource)
.build();
}
}6.4 Spring Cache 快取管理
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
/**
* 快取查詢結果
*/
@Cacheable(value = "users", key = "#userId")
public UserDto getUserById(Long userId) {
return userRepository.findById(userId)
.map(this::convertToDto)
.orElse(null);
}
/**
* 更新快取
*/
@CachePut(value = "users", key = "#result.id")
public UserDto updateUser(Long userId, UpdateUserRequest request) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException("User not found: " + userId));
user.setName(request.getName());
user.setEmail(request.getEmail());
User savedUser = userRepository.save(user);
return convertToDto(savedUser);
}
/**
* 刪除快取
*/
@CacheEvict(value = "users", key = "#userId")
public void deleteUser(Long userId) {
userRepository.deleteById(userId);
}
}6.5 Spring Boot 配置管理
# application.yml
spring:
profiles:
active: ${SPRING_PROFILES_ACTIVE:dev}
datasource:
url: ${DATABASE_URL:jdbc:postgresql://localhost:5432/myapp}
username: ${DATABASE_USERNAME:myapp}
password: ${DATABASE_PASSWORD:password}
hikari:
maximum-pool-size: ${DB_POOL_SIZE:20}
minimum-idle: ${DB_POOL_MIN_IDLE:5}
jpa:
hibernate:
ddl-auto: ${JPA_DDL_AUTO:validate}
show-sql: ${JPA_SHOW_SQL:false}
cache:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
# JWT 配置
jwt:
secret: ${JWT_SECRET:mySecretKey}
expiration: ${JWT_EXPIRATION:86400} # 24小時Spring Boot 開發注意事項:
- 使用 @Transactional 管理事務:確保資料一致性
- 適當的快取策略:避免過度快取或快取穿透
- 批次處理監控:記錄處理進度和錯誤
- JWT 安全性:使用強密鑰並適當設置過期時間
- 資料庫連線池優化:根據負載調整連線池大小
- 分頁查詢:避免一次載入大量資料
7. 資料庫設計與操作
7.1 資料庫設計原則
正規化設計
-- 第一正規化:消除重複的列
-- ❌ 錯誤設計
CREATE TABLE bad_orders (
id BIGINT PRIMARY KEY,
customer_name VARCHAR(100),
product1 VARCHAR(100),
product2 VARCHAR(100),
product3 VARCHAR(100)
);
-- ✅ 正確設計
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
customer_id BIGINT,
order_date TIMESTAMP,
total_amount DECIMAL(10,2)
);
CREATE TABLE order_items (
id BIGINT PRIMARY KEY,
order_id BIGINT,
product_id BIGINT,
quantity INT,
unit_price DECIMAL(10,2),
FOREIGN KEY (order_id) REFERENCES orders(id)
);Entity 設計最佳實踐
@Entity
@Table(name = "users", indexes = {
@Index(name = "idx_user_email", columnList = "email"),
@Index(name = "idx_user_created_at", columnList = "created_at")
})
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "email", unique = true, nullable = false, length = 255)
private String email;
@Column(name = "username", unique = true, nullable = false, length = 50)
private String username;
@Column(name = "password_hash", nullable = false)
private String passwordHash;
@Column(name = "first_name", length = 50)
private String firstName;
@Column(name = "last_name", length = 50)
private String lastName;
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false)
private UserStatus status = UserStatus.ACTIVE;
@CreationTimestamp
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
@Version
private Long version;
// 一對多關係
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Order> orders = new ArrayList<>();
// Getters and setters...
}
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false)
private OrderStatus status;
@Column(name = "total_amount", precision = 10, scale = 2)
private BigDecimal totalAmount;
@CreationTimestamp
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
// Getters and setters...
}資料庫約束設計
-- 主鍵約束
ALTER TABLE users ADD CONSTRAINT pk_users PRIMARY KEY (id);
-- 外鍵約束
ALTER TABLE orders ADD CONSTRAINT fk_orders_user_id
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
-- 唯一約束
ALTER TABLE users ADD CONSTRAINT uk_users_email UNIQUE (email);
ALTER TABLE users ADD CONSTRAINT uk_users_username UNIQUE (username);
-- 檢查約束
ALTER TABLE orders ADD CONSTRAINT chk_orders_total_amount
CHECK (total_amount >= 0);
-- 非空約束
ALTER TABLE users ALTER COLUMN email SET NOT NULL;
ALTER TABLE users ALTER COLUMN username SET NOT NULL;7.2 SQL 查詢優化
索引策略
-- 單欄索引
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_orders_created_at ON orders(created_at);
-- 複合索引(注意順序)
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
CREATE INDEX idx_orders_status_created ON orders(status, created_at);
-- 部分索引
CREATE INDEX idx_users_active_email ON users(email)
WHERE status = 'ACTIVE';
-- 函數索引
CREATE INDEX idx_users_lower_email ON users(LOWER(email));Repository 查詢優化
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// ✅ 使用索引的查詢
@Query("SELECT u FROM User u WHERE u.email = :email")
Optional<User> findByEmail(@Param("email") String email);
// ✅ 分頁查詢
@Query("SELECT u FROM User u WHERE u.status = :status ORDER BY u.createdAt DESC")
Page<User> findByStatus(@Param("status") UserStatus status, Pageable pageable);
// ✅ 投影查詢,只選擇需要的欄位
@Query("SELECT new com.tutorial.dto.UserSummaryDto(u.id, u.username, u.email) " +
"FROM User u WHERE u.status = :status")
List<UserSummaryDto> findUserSummaries(@Param("status") UserStatus status);
// ✅ JOIN FETCH 避免 N+1 問題
@Query("SELECT u FROM User u LEFT JOIN FETCH u.orders WHERE u.id = :id")
Optional<User> findByIdWithOrders(@Param("id") Long id);
// ✅ 批次查詢
@Query("SELECT u FROM User u WHERE u.id IN :ids")
List<User> findByIds(@Param("ids") List<Long> ids);
// ✅ 統計查詢
@Query("SELECT COUNT(u) FROM User u WHERE u.status = :status")
long countByStatus(@Param("status") UserStatus status);
// ✅ 原生查詢(複雜邏輯)
@Query(value = """
SELECT u.* FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.created_at >= :startDate
GROUP BY u.id
HAVING COUNT(o.id) >= :minOrderCount
""", nativeQuery = true)
List<User> findActiveCustomers(@Param("startDate") LocalDateTime startDate,
@Param("minOrderCount") int minOrderCount);
}查詢效能監控
@Component
public class QueryPerformanceInterceptor implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(QueryPerformanceInterceptor.class);
private static final long SLOW_QUERY_THRESHOLD_MS = 1000;
@Override
public boolean onLoad(Object entity, Serializable id, Object[] state,
String[] propertyNames, Type[] types) {
// 查詢開始時間記錄
return false;
}
@Override
public void onPostLoad(PostLoadEvent event) {
// 記錄查詢結束時間和效能指標
}
@EventListener
public void handleSlowQuery(SlowQueryEvent event) {
if (event.getDurationMs() > SLOW_QUERY_THRESHOLD_MS) {
logger.warn("Slow query detected: {} ms - Query: {}",
event.getDurationMs(),
event.getQuery());
}
}
}7.3 事務管理
事務配置
@Configuration
@EnableTransactionManagement
public class TransactionConfig {
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(emf);
return transactionManager;
}
@Bean
public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
TransactionTemplate template = new TransactionTemplate(transactionManager);
template.setTimeout(30); // 30秒逾時
return template;
}
}聲明式事務
@Service
@Transactional(readOnly = true)
public class OrderService {
private final OrderRepository orderRepository;
private final UserRepository userRepository;
private final PaymentService paymentService;
private final EmailService emailService;
// 寫操作事務
@Transactional(rollbackFor = Exception.class, timeout = 30)
public Order createOrder(CreateOrderRequest request) {
// 驗證用戶
User user = userRepository.findById(request.getUserId())
.orElseThrow(() -> new UserNotFoundException("User not found"));
// 創建訂單
Order order = new Order();
order.setUser(user);
order.setStatus(OrderStatus.PENDING);
order.setTotalAmount(request.getTotalAmount());
Order savedOrder = orderRepository.save(order);
// 處理付款
paymentService.processPayment(savedOrder);
// 發送確認郵件(非事務操作)
sendOrderConfirmationAsync(savedOrder);
return savedOrder;
}
// 只讀事務優化
@Transactional(readOnly = true)
public Page<Order> getUserOrders(Long userId, Pageable pageable) {
return orderRepository.findByUserIdOrderByCreatedAtDesc(userId, pageable);
}
// 程式化事務
@Autowired
private TransactionTemplate transactionTemplate;
public Order createOrderProgrammatic(CreateOrderRequest request) {
return transactionTemplate.execute(status -> {
try {
// 事務邏輯
return createOrderInternal(request);
} catch (Exception e) {
status.setRollbackOnly();
throw e;
}
});
}
// 非同步操作,使用新事務
@Async
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void sendOrderConfirmationAsync(Order order) {
try {
emailService.sendOrderConfirmation(order);
} catch (Exception e) {
logger.error("Failed to send order confirmation: {}", e.getMessage(), e);
}
}
}分散式事務處理
@Configuration
@EnableJtaTransactionManagement
public class DistributedTransactionConfig {
@Bean
public JtaTransactionManager transactionManager() {
return new JtaTransactionManager();
}
}
@Service
public class DistributedOrderService {
// 使用 JTA 事務管理多個資源
@Transactional
public void processOrderWithMultipleResources(Order order) {
// 更新主要資料庫
orderRepository.save(order);
// 更新分析資料庫
analyticsRepository.recordOrderEvent(order);
// 發送訊息到佇列
messageQueueService.sendOrderEvent(order);
}
}7.4 資料遷移策略
Flyway 遷移腳本
-- V1__Create_initial_tables.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
username VARCHAR(50) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
first_name VARCHAR(50),
last_name VARCHAR(50),
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_status ON users(status);
-- V2__Add_orders_table.sql
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
total_amount DECIMAL(10,2) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_created_at ON orders(created_at);
-- V3__Add_user_profile_fields.sql
ALTER TABLE users
ADD COLUMN phone VARCHAR(20),
ADD COLUMN birth_date DATE,
ADD COLUMN profile_image_url VARCHAR(500);資料遷移最佳實踐
@Component
public class DataMigrationService {
private final JdbcTemplate jdbcTemplate;
private final UserRepository userRepository;
@Value("${migration.batch-size:1000}")
private int batchSize;
// 大批量資料遷移
@Transactional
public void migrateLegacyUsers() {
String selectSql = """
SELECT id, email, username, password, created_date
FROM legacy_users
WHERE migrated = false
ORDER BY id
LIMIT ?
""";
String updateSql = """
UPDATE legacy_users
SET migrated = true
WHERE id IN (?)
""";
List<Long> processedIds = new ArrayList<>();
jdbcTemplate.query(selectSql, new Object[]{batchSize}, rs -> {
User user = new User();
user.setEmail(rs.getString("email"));
user.setUsername(rs.getString("username"));
user.setPasswordHash(hashPassword(rs.getString("password")));
user.setCreatedAt(rs.getTimestamp("created_date").toLocalDateTime());
userRepository.save(user);
processedIds.add(rs.getLong("id"));
});
if (!processedIds.isEmpty()) {
jdbcTemplate.update(updateSql, processedIds.toArray());
}
}
// 安全的欄位遷移
public void addUserProfileData() {
String sql = """
UPDATE users
SET phone = ?, birth_date = ?
WHERE id = ? AND phone IS NULL
""";
List<User> users = userRepository.findAll();
jdbcTemplate.batchUpdate(sql, users, batchSize,
(PreparedStatement ps, User user) -> {
ps.setString(1, generateDefaultPhone());
ps.setDate(2, Date.valueOf(LocalDate.now().minusYears(25)));
ps.setLong(3, user.getId());
});
}
}7.5 資料庫監控與維護
效能監控
@Component
public class DatabaseMonitor {
private final DataSource dataSource;
private final MeterRegistry meterRegistry;
@EventListener
@Async
public void handleSlowQuery(SlowQueryEvent event) {
// 記錄慢查詢指標
Timer.Sample sample = Timer.start(meterRegistry);
sample.stop(Timer.builder("database.slow.queries")
.tags("query", event.getQueryType())
.register(meterRegistry));
// 發送告警
if (event.getDurationMs() > 5000) {
alertService.sendSlowQueryAlert(event);
}
}
@Scheduled(fixedRate = 60000) // 每分鐘執行
public void monitorConnectionPool() {
try (Connection conn = dataSource.getConnection()) {
DatabaseMetaData metaData = conn.getMetaData();
// 監控連接池狀態
if (dataSource instanceof HikariDataSource) {
HikariDataSource hikariDS = (HikariDataSource) dataSource;
HikariPoolMXBean poolMBean = hikariDS.getHikariPoolMXBean();
Gauge.builder("database.connections.active")
.register(meterRegistry, poolMBean, HikariPoolMXBean::getActiveConnections);
Gauge.builder("database.connections.idle")
.register(meterRegistry, poolMBean, HikariPoolMXBean::getIdleConnections);
Gauge.builder("database.connections.total")
.register(meterRegistry, poolMBean, HikariPoolMXBean::getTotalConnections);
}
} catch (SQLException e) {
logger.error("Failed to monitor database connection pool", e);
}
}
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2點執行
public void performMaintenanceTasks() {
// 分析資料庫統計
updateTableStatistics();
// 清理過期資料
cleanupExpiredData();
// 檢查索引使用情況
analyzeIndexUsage();
}
private void updateTableStatistics() {
jdbcTemplate.execute("ANALYZE");
}
private void cleanupExpiredData() {
// 清理過期的session記錄
int deleted = jdbcTemplate.update("""
DELETE FROM user_sessions
WHERE last_accessed < ?
""", Timestamp.valueOf(LocalDateTime.now().minusDays(30)));
logger.info("Cleaned up {} expired user sessions", deleted);
}
private void analyzeIndexUsage() {
List<Map<String, Object>> indexStats = jdbcTemplate.queryForList("""
SELECT schemaname, tablename, indexname, idx_scan, idx_tup_read
FROM pg_stat_user_indexes
WHERE idx_scan = 0
""");
if (!indexStats.isEmpty()) {
logger.warn("Found {} unused indexes", indexStats.size());
indexStats.forEach(stat ->
logger.warn("Unused index: {}.{}.{}",
stat.get("schemaname"),
stat.get("tablename"),
stat.get("indexname")));
}
}
}備份策略
@Component
public class DatabaseBackupService {
@Value("${backup.directory}")
private String backupDirectory;
@Value("${backup.retention.days:30}")
private int retentionDays;
@Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1點執行
public void performDailyBackup() {
try {
String backupFileName = generateBackupFileName();
Path backupPath = Paths.get(backupDirectory, backupFileName);
// 執行備份
ProcessBuilder pb = new ProcessBuilder(
"pg_dump",
"-h", databaseHost,
"-U", databaseUser,
"-d", databaseName,
"-f", backupPath.toString(),
"--verbose"
);
pb.environment().put("PGPASSWORD", databasePassword);
Process process = pb.start();
int exitCode = process.waitFor();
if (exitCode == 0) {
logger.info("Database backup completed successfully: {}", backupFileName);
// 壓縮備份檔案
compressBackupFile(backupPath);
// 清理過期備份
cleanupOldBackups();
} else {
logger.error("Database backup failed with exit code: {}", exitCode);
}
} catch (Exception e) {
logger.error("Failed to perform database backup", e);
}
}
private void cleanupOldBackups() {
try {
LocalDateTime cutoffDate = LocalDateTime.now().minusDays(retentionDays);
Files.list(Paths.get(backupDirectory))
.filter(path -> path.toString().endsWith(".sql.gz"))
.filter(path -> {
try {
return Files.getLastModifiedTime(path)
.toInstant()
.isBefore(cutoffDate.atZone(ZoneId.systemDefault()).toInstant());
} catch (IOException e) {
return false;
}
})
.forEach(path -> {
try {
Files.delete(path);
logger.info("Deleted old backup: {}", path.getFileName());
} catch (IOException e) {
logger.error("Failed to delete old backup: {}", path.getFileName(), e);
}
});
} catch (IOException e) {
logger.error("Failed to cleanup old backups", e);
}
}
}8. 效能優化
7.1 Java 應用程式效能優化
JVM 調優
# 生產環境 JVM 參數範例
java -Xms2g -Xmx8g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:+UseStringDeduplication \
-XX:+OptimizeStringConcat \
-Djava.awt.headless=true \
-Dfile.encoding=UTF-8 \
-jar myapp.jar記憶體效能優化
/**
* 記憶體優化最佳實踐
*/
@Service
public class OptimizedUserService {
// ✅ 使用對象池減少對象創建
private final ObjectPool<StringBuilder> stringBuilderPool =
new GenericObjectPool<>(new StringBuilderPooledObjectFactory());
// ✅ 使用緩存避免重複計算
@Cacheable(value = "expensiveCalculations", key = "#input")
public BigDecimal performExpensiveCalculation(String input) {
// 複雜計算邏輯
return new BigDecimal("123.45");
}
/**
* 批次處理避免 N+1 查詢問題
*/
public List<UserDto> getUsersWithProfiles(List<Long> userIds) {
// ✅ 一次查詢所有用戶
List<User> users = userRepository.findAllById(userIds);
// ✅ 一次查詢所有配置檔
List<Long> foundUserIds = users.stream()
.map(User::getId)
.collect(Collectors.toList());
List<UserProfile> profiles = userProfileRepository.findByUserIdIn(foundUserIds);
// ✅ 建立映射關係
Map<Long, UserProfile> profileMap = profiles.stream()
.collect(Collectors.toMap(UserProfile::getUserId, Function.identity()));
// ✅ 組合結果
return users.stream()
.map(user -> {
UserDto dto = mapToDto(user);
dto.setProfile(profileMap.get(user.getId()));
return dto;
})
.collect(Collectors.toList());
}
/**
* 流式處理大量資料
*/
public void processLargeDataSet() {
// ✅ 使用 Stream 分頁處理,避免記憶體溢出
int pageSize = 1000;
int pageNumber = 0;
Page<User> userPage;
do {
Pageable pageable = PageRequest.of(pageNumber, pageSize);
userPage = userRepository.findAll(pageable);
userPage.getContent().parallelStream()
.forEach(this::processUser);
pageNumber++;
} while (userPage.hasNext());
}
/**
* 使用享元模式減少對象創建
*/
private static final Map<String, UserStatus> STATUS_CACHE = new HashMap<>();
static {
for (UserStatus status : UserStatus.values()) {
STATUS_CACHE.put(status.name(), status);
}
}
public UserStatus getStatusFromString(String statusName) {
return STATUS_CACHE.get(statusName);
}
}資料庫查詢優化
/**
* 資料庫效能優化實例
*/
@Repository
public interface OptimizedUserRepository extends JpaRepository<User, Long> {
// ✅ 使用索引提升查詢效能
@Query("SELECT u FROM User u WHERE u.email = :email AND u.status = 'ACTIVE'")
Optional<User> findActiveUserByEmail(@Param("email") String email);
// ✅ 避免 N+1 問題:使用 JOIN FETCH
@Query("SELECT u FROM User u LEFT JOIN FETCH u.orders o WHERE u.id = :userId")
Optional<User> findUserWithOrders(@Param("userId") Long userId);
// ✅ 分頁查詢:只查詢需要的欄位
@Query("SELECT new com.tutorial.dto.UserSummaryDto(u.id, u.name, u.email) " +
"FROM User u WHERE u.status = :status")
Page<UserSummaryDto> findUserSummaries(@Param("status") UserStatus status, Pageable pageable);
// ✅ 批次查詢避免迴圈查詢
@Query("SELECT u FROM User u WHERE u.id IN :userIds")
List<User> findUsersByIds(@Param("userIds") List<Long> userIds);
// ✅ 使用原生 SQL 進行複雜統計
@Query(value = """
SELECT DATE(created_at) as date, COUNT(*) as count
FROM users
WHERE created_at >= :startDate
GROUP BY DATE(created_at)
ORDER BY date
""", nativeQuery = true)
List<Object[]> getDailyUserCreationStats(@Param("startDate") LocalDateTime startDate);
}7.2 Spring Boot 效能調優
連線池優化
# application.yml
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
hikari:
# 連線池大小 = ((核心數 * 2) + 有效磁碟數)
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
leak-detection-threshold: 60000
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
# 批次處理優化
jdbc.batch_size: 50
order_inserts: true
order_updates: true
# 二級快取
cache.use_second_level_cache: true
cache.region.factory_class: org.hibernate.cache.jcache.JCacheRegionFactory非同步處理優化
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("Async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
@Service
public class AsyncUserService {
/**
* 非同步處理用戶註冊後續作業
*/
@Async("taskExecutor")
public CompletableFuture<Void> processUserRegistration(User user) {
try {
// 發送歡迎郵件
emailService.sendWelcomeEmail(user);
// 創建用戶配置檔
createDefaultUserProfile(user);
// 記錄審計日誌
auditService.logUserRegistration(user);
return CompletableFuture.completedFuture(null);
} catch (Exception e) {
logger.error("Failed to process user registration: {}", user.getId(), e);
return CompletableFuture.failedFuture(e);
}
}
}7.3 前端效能優化
Vue.js 效能優化
<template>
<div class="user-list">
<!-- ✅ 使用 v-show 而非 v-if 當需要頻繁切換時 -->
<div v-show="showFilters" class="filters">
<!-- 過濾器內容 -->
</div>
<!-- ✅ 使用 key 確保正確的重新渲染 -->
<div
v-for="user in visibleUsers"
:key="user.id"
class="user-card"
>
<!-- ✅ 使用計算屬性避免重複計算 -->
<span>{{ getUserDisplayName(user) }}</span>
</div>
<!-- ✅ 虛擬滾動處理大量資料 -->
<virtual-list
:data-source="allUsers"
:data-key="'id'"
:data-component="UserCard"
:keeps="30"
:estimate-size="80"
/>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch, onMounted } from 'vue';
import { debounce } from 'lodash-es';
interface User {
id: string;
firstName: string;
lastName: string;
email: string;
}
const props = defineProps<{
users: User[];
searchTerm: string;
}>();
// ✅ 使用 ref 進行響應式狀態管理
const showFilters = ref(false);
const loading = ref(false);
// ✅ 使用計算屬性快取複雜計算
const visibleUsers = computed(() => {
if (!props.searchTerm) return props.users;
const term = props.searchTerm.toLowerCase();
return props.users.filter(user =>
user.firstName.toLowerCase().includes(term) ||
user.lastName.toLowerCase().includes(term) ||
user.email.toLowerCase().includes(term)
);
});
// ✅ 記憶化函式避免重複計算
const getUserDisplayName = (user: User): string => {
return `${user.firstName} ${user.lastName}`;
};
// ✅ 使用 debounce 減少 API 呼叫
const debouncedSearch = debounce(async (searchTerm: string) => {
loading.value = true;
try {
await userStore.searchUsers(searchTerm);
} finally {
loading.value = false;
}
}, 300);
// ✅ 監聽器使用 debounce
watch(() => props.searchTerm, debouncedSearch);
// ✅ 懶載入和預載入
onMounted(() => {
// 預載入重要資源
userStore.preloadUserProfiles();
});
</script>
<style scoped>
/* ✅ 使用 CSS 動畫而非 JavaScript 動畫 */
.user-card {
transition: transform 0.2s ease-in-out;
}
.user-card:hover {
transform: translateY(-2px);
}
/* ✅ 使用 CSS Grid 進行高效佈局 */
.user-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
}
</style>資源載入優化
// 程式碼分割和懶載入
const UserDashboard = defineAsyncComponent({
loader: () => import('./UserDashboard.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorComponent,
delay: 200,
timeout: 3000
});
// 圖片懶載入和優化
export class ImageOptimizer {
static createOptimizedImage(src: string, options: ImageOptions): string {
const params = new URLSearchParams({
w: options.width?.toString() || '',
h: options.height?.toString() || '',
q: options.quality?.toString() || '80',
f: options.format || 'webp'
});
return `${src}?${params}`;
}
static preloadImages(urls: string[]): Promise<void[]> {
return Promise.all(
urls.map(url => new Promise<void>((resolve, reject) => {
const img = new Image();
img.onload = () => resolve();
img.onerror = reject;
img.src = url;
}))
);
}
}
// API 請求優化
export class ApiOptimizer {
private cache = new Map<string, { data: any; timestamp: number }>();
private pendingRequests = new Map<string, Promise<any>>();
async get<T>(url: string, options?: { ttl?: number }): Promise<T> {
const cacheKey = url;
const ttl = options?.ttl || 300000; // 5分鐘預設快取
// 檢查快取
const cached = this.cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.data;
}
// 檢查是否有進行中的請求
if (this.pendingRequests.has(cacheKey)) {
return this.pendingRequests.get(cacheKey)!;
}
// 發送新請求
const request = fetch(url).then(response => response.json());
this.pendingRequests.set(cacheKey, request);
try {
const data = await request;
this.cache.set(cacheKey, { data, timestamp: Date.now() });
return data;
} finally {
this.pendingRequests.delete(cacheKey);
}
}
}7.4 快取策略優化
多層快取架構
@Configuration
@EnableCaching
public class CacheOptimizationConfig {
@Bean
public CacheManager cacheManager() {
// L1: 本地快取 (Caffeine)
CaffeineCacheManager localCacheManager = new CaffeineCacheManager();
localCacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(5))
.recordStats());
// L2: 分散式快取 (Redis)
RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory())
.cacheDefaults(redisCacheConfiguration())
.build();
// 組合快取管理器
return new CompositeCacheManager(localCacheManager, redisCacheManager);
}
private RedisCacheConfiguration redisCacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.computePrefixWith(cacheName -> "myapp:cache:" + cacheName + ":");
}
}
@Service
public class OptimizedCacheService {
/**
* 快取預熱策略
*/
@EventListener(ApplicationReadyEvent.class)
public void warmUpCache() {
logger.info("Starting cache warm-up...");
// 預載入熱點資料
List<String> hotDataKeys = getHotDataKeys();
hotDataKeys.parallelStream().forEach(key -> {
try {
loadDataToCache(key);
} catch (Exception e) {
logger.warn("Failed to warm up cache for key: {}", key, e);
}
});
logger.info("Cache warm-up completed");
}
/**
* 智能快取更新
*/
@CacheEvict(value = "users", key = "#userId")
@CachePut(value = "users", key = "#result.id", condition = "#result != null")
public UserDto updateUserWithSmartCache(Long userId, UpdateUserRequest request) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException("User not found: " + userId));
// 更新用戶資料
updateUserData(user, request);
User savedUser = userRepository.save(user);
// 同時更新相關快取
invalidateRelatedCaches(userId);
return convertToDto(savedUser);
}
/**
* 批次快取載入
*/
public Map<Long, UserDto> getUsersBatch(List<Long> userIds) {
// 1. 先從快取獲取
Map<Long, UserDto> cached = new HashMap<>();
List<Long> uncachedIds = new ArrayList<>();
for (Long userId : userIds) {
UserDto cachedUser = cacheManager.getCache("users").get(userId, UserDto.class);
if (cachedUser != null) {
cached.put(userId, cachedUser);
} else {
uncachedIds.add(userId);
}
}
// 2. 批次查詢未快取的資料
if (!uncachedIds.isEmpty()) {
List<User> users = userRepository.findAllById(uncachedIds);
Map<Long, UserDto> fresh = users.stream()
.collect(Collectors.toMap(
User::getId,
this::convertToDto
));
// 3. 將新資料加入快取
fresh.forEach((id, dto) ->
cacheManager.getCache("users").put(id, dto));
cached.putAll(fresh);
}
return cached;
}
}7.5 監控和指標
效能監控配置
# application.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
metrics:
export:
prometheus:
enabled: true
distribution:
percentiles-histogram:
http.server.requests: true
percentiles:
http.server.requests: 0.5, 0.9, 0.95, 0.99
sla:
http.server.requests: 10ms,50ms,100ms,200ms,500ms自定義效能監控
@Component
public class PerformanceMonitor {
private final MeterRegistry meterRegistry;
private final Timer.Sample sample;
public PerformanceMonitor(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.sample = Timer.start(meterRegistry);
}
/**
* 監控方法執行時間
*/
@Around("@annotation(MonitorPerformance)")
public Object monitorExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
Timer.Sample sample = Timer.start(meterRegistry);
try {
Object result = joinPoint.proceed();
sample.stop(Timer.builder("method.execution.time")
.tag("method", methodName)
.tag("status", "success")
.register(meterRegistry));
return result;
} catch (Exception e) {
sample.stop(Timer.builder("method.execution.time")
.tag("method", methodName)
.tag("status", "error")
.register(meterRegistry));
throw e;
}
}
/**
* 監控資料庫查詢效能
*/
public void recordDatabaseQuery(String queryType, long executionTime) {
Timer.builder("database.query.time")
.tag("query.type", queryType)
.register(meterRegistry)
.record(executionTime, TimeUnit.MILLISECONDS);
}
/**
* 監控快取命中率
*/
public void recordCacheHit(String cacheName, boolean hit) {
Counter.builder("cache.requests")
.tag("cache", cacheName)
.tag("result", hit ? "hit" : "miss")
.register(meterRegistry)
.increment();
}
}7.6 效能測試
JMeter 測試腳本範例
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2">
<hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="User API Performance Test">
<elementProp name="TestPlan.arguments" elementType="Arguments" guiclass="ArgumentsPanel">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="TestPlan.user_define_classpath"></stringProp>
<boolProp name="TestPlan.functional_mode">false</boolProp>
<boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
</TestPlan>
<!-- 執行緒群組:模擬併發用戶 -->
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="User Load Test">
<stringProp name="ThreadGroup.num_threads">100</stringProp>
<stringProp name="ThreadGroup.ramp_time">30</stringProp>
<stringProp name="ThreadGroup.duration">300</stringProp>
<boolProp name="ThreadGroup.scheduler">true</boolProp>
</ThreadGroup>
</hashTree>
</jmeterTestPlan>7.7 效能優化檢查清單
後端效能檢查:
資料庫優化
- 適當的索引設計
- 查詢語句優化
- 連線池配置
- 避免 N+1 查詢問題
快取策略
- 多層快取架構
- 快取預熱機制
- 適當的失效策略
- 快取穿透保護
JVM 調優
- 堆記憶體大小配置
- 垃圾收集器選擇
- GC 參數調優
- 記憶體洩漏檢查
前端效能檢查:
程式碼優化
- 程式碼分割和懶載入
- 無用程式碼移除
- 資源壓縮
- 圖片優化
載入效能
- 首屏載入時間 < 2秒
- 資源預載入
- CDN 使用
- HTTP/2 支援
運行時效能
- 虛擬滾動實現
- 防抖和節流
- 記憶化計算
- 長列表優化
監控和報告:
關鍵指標監控
- 回應時間 (P95 < 200ms)
- 吞吐量監控
- 錯誤率 < 0.1%
- 資源使用率
持續優化
- 定期效能測試
- 效能基準建立
- 瓶頸識別和解決
- 效能回歸檢測
8. 版本控制
8.1 Git 工作流程規範
Git Flow 分支策略
# 主要分支
master/main # 生產環境分支,只包含穩定版本
develop # 開發主分支,整合所有功能
# 支援分支
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // 強度12
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable()) // 使用JWT時禁用CSRF
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authz -> authz
// 公開端點
.requestMatchers("/api/public/**", "/api/auth/login", "/api/auth/register").permitAll()
.requestMatchers("/actuator/health").permitAll()
.requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
// 需要認證的端點
.requestMatchers(HttpMethod.GET, "/api/users/profile").hasRole("USER")
.requestMatchers(HttpMethod.PUT, "/api/users/**").hasRole("USER")
// 管理員端點
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.DELETE, "/api/users/**").hasRole("ADMIN")
// 其他請求都需要認證
.anyRequest().authenticated()
)
.exceptionHandling(ex -> ex
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(customAccessDeniedHandler())
);
// 添加JWT過濾器
http.addFilterBefore(jwtRequestFilter(), UsernamePasswordAuthenticationFilter.class);
// 安全標頭
http.headers(headers -> headers
.frameOptions().deny()
.contentTypeOptions().and()
.httpStrictTransportSecurity(hsts -> hsts
.maxAgeInSeconds(31536000)
.includeSubdomains(true))
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(Arrays.asList("https://*.example.com"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", configuration);
return source;
}
}安全的用戶認證服務
@Service
public class SecureAuthenticationService {
private static final Logger logger = LoggerFactory.getLogger(SecureAuthenticationService.class);
private static final int MAX_LOGIN_ATTEMPTS = 5;
private static final int LOCKOUT_TIME_MINUTES = 30;
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 安全登入驗證
*/
public LoginResponse authenticateUser(LoginRequest request, HttpServletRequest httpRequest) {
String clientIp = getClientIpAddress(httpRequest);
String email = request.getEmail();
// 1. 基本輸入驗證
validateLoginRequest(request);
// 2. 檢查帳戶鎖定狀態
if (isAccountLocked(email)) {
logger.warn("Login attempt for locked account: {} from IP: {}", email, clientIp);
throw new AccountLockedException("Account is temporarily locked due to multiple failed attempts");
}
// 3. 查詢用戶
User user = userRepository.findByEmail(email)
.orElseThrow(() -> {
recordFailedAttempt(email, clientIp);
return new BadCredentialsException("Invalid credentials");
});
// 4. 驗證密碼
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
recordFailedAttempt(email, clientIp);
logger.warn("Invalid password attempt for user: {} from IP: {}", email, clientIp);
throw new BadCredentialsException("Invalid credentials");
}
// 5. 檢查帳戶狀態
if (!user.isEnabled()) {
logger.warn("Login attempt for disabled account: {} from IP: {}", email, clientIp);
throw new DisabledException("Account is disabled");
}
// 6. 清除失敗嘗試記錄
clearFailedAttempts(email);
// 7. 生成JWT令牌
String accessToken = jwtTokenUtil.generateAccessToken(user);
String refreshToken = jwtTokenUtil.generateRefreshToken(user);
// 8. 記錄成功登入
recordSuccessfulLogin(user, clientIp);
return LoginResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.tokenType("Bearer")
.expiresIn(jwtTokenUtil.getAccessTokenExpiration())
.build();
}
/**
* 記錄失敗登入嘗試
*/
private void recordFailedAttempt(String email, String clientIp) {
String key = "failed_attempts:" + email;
Integer attempts = (Integer) redisTemplate.opsForValue().get(key);
attempts = (attempts == null) ? 1 : attempts + 1;
redisTemplate.opsForValue().set(key, attempts, Duration.ofMinutes(LOCKOUT_TIME_MINUTES));
if (attempts >= MAX_LOGIN_ATTEMPTS) {
String lockKey = "account_locked:" + email;
redisTemplate.opsForValue().set(lockKey, true, Duration.ofMinutes(LOCKOUT_TIME_MINUTES));
logger.warn("Account locked due to {} failed attempts: {} from IP: {}", attempts, email, clientIp);
}
// 記錄審計日誌
auditService.logFailedLogin(email, clientIp, attempts);
}
/**
* 檢查帳戶是否被鎖定
*/
private boolean isAccountLocked(String email) {
String lockKey = "account_locked:" + email;
return Boolean.TRUE.equals(redisTemplate.opsForValue().get(lockKey));
}
/**
* 獲取客戶端IP地址
*/
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (StringUtils.hasText(xForwardedFor)) {
return xForwardedFor.split(",")[0].trim();
}
String xRealIp = request.getHeader("X-Real-IP");
if (StringUtils.hasText(xRealIp)) {
return xRealIp;
}
return request.getRemoteAddr();
}
}8.2 輸入驗證與防護
輸入驗證最佳實踐
/**
* 安全的輸入驗證工具類
*/
@Component
public class InputValidator {
private static final Pattern EMAIL_PATTERN =
Pattern.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");
private static final Pattern PHONE_PATTERN =
Pattern.compile("^\\+?[1-9]\\d{1,14}$");
private static final Pattern SAFE_STRING_PATTERN =
Pattern.compile("^[a-zA-Z0-9\\s\\-_.@]+$");
/**
* 驗證並清理用戶輸入
*/
public String sanitizeInput(String input, int maxLength) {
if (input == null) {
return null;
}
// 移除前後空白
input = input.trim();
// 長度檢查
if (input.length() > maxLength) {
throw new ValidationException("Input exceeds maximum length: " + maxLength);
}
// HTML編碼防止XSS
input = HtmlUtils.htmlEscape(input);
// 移除潛在危險字符
input = input.replaceAll("[<>\"'&]", "");
return input;
}
/**
* 驗證電子郵件格式
*/
public boolean isValidEmail(String email) {
return email != null && EMAIL_PATTERN.matcher(email).matches();
}
/**
* 驗證密碼強度
*/
public PasswordStrength validatePassword(String password) {
if (password == null || password.length() < 8) {
return PasswordStrength.WEAK;
}
boolean hasLower = password.chars().anyMatch(Character::isLowerCase);
boolean hasUpper = password.chars().anyMatch(Character::isUpperCase);
boolean hasDigit = password.chars().anyMatch(Character::isDigit);
boolean hasSpecial = password.chars().anyMatch(ch -> "!@#$%^&*()_+-=[]{}|;:,.<>?".indexOf(ch) >= 0);
int score = 0;
if (hasLower) score++;
if (hasUpper) score++;
if (hasDigit) score++;
if (hasSpecial) score++;
if (password.length() >= 12) score++;
if (score >= 4) return PasswordStrength.STRONG;
if (score >= 3) return PasswordStrength.MEDIUM;
return PasswordStrength.WEAK;
}
/**
* SQL注入防護檢查
*/
public void validateForSQLInjection(String input) {
if (input == null) return;
String upperInput = input.toUpperCase();
String[] sqlKeywords = {
"SELECT", "INSERT", "UPDATE", "DELETE", "DROP", "UNION",
"EXEC", "EXECUTE", "SCRIPT", "JAVASCRIPT", "VBSCRIPT"
};
for (String keyword : sqlKeywords) {
if (upperInput.contains(keyword)) {
logger.warn("Potential SQL injection attempt detected: {}", input);
throw new SecurityException("Invalid input detected");
}
}
}
}
/**
* 安全的DTO驗證
*/
public class CreateUserRequest {
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
@Size(max = 100, message = "Email must not exceed 100 characters")
private String email;
@NotBlank(message = "Name is required")
@Pattern(regexp = "^[a-zA-Z\\s]+$", message = "Name can only contain letters and spaces")
@Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
private String name;
@NotBlank(message = "Password is required")
@Size(min = 8, max = 128, message = "Password must be between 8 and 128 characters")
@Pattern(
regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]+$",
message = "Password must contain at least one uppercase letter, one lowercase letter, one digit, and one special character"
)
private String password;
@Pattern(regexp = "^\\+?[1-9]\\d{1,14}$", message = "Invalid phone number format")
private String phoneNumber;
// Getters and setters...
}8.3 資料加密與保護
敏感資料加密
@Component
public class EncryptionService {
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int GCM_IV_LENGTH = 12;
private static final int GCM_TAG_LENGTH = 16;
@Value("${app.encryption.key}")
private String encryptionKey;
/**
* 加密敏感資料
*/
public String encrypt(String plainText) {
try {
SecretKeySpec keySpec = new SecretKeySpec(
Base64.getDecoder().decode(encryptionKey), "AES");
Cipher cipher = Cipher.getInstance(ALGORITHM);
// 生成隨機IV
byte[] iv = new byte[GCM_IV_LENGTH];
SecureRandom.getInstanceStrong().nextBytes(iv);
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, parameterSpec);
byte[] encryptedData = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
// 將IV和加密資料組合
byte[] result = new byte[GCM_IV_LENGTH + encryptedData.length];
System.arraycopy(iv, 0, result, 0, GCM_IV_LENGTH);
System.arraycopy(encryptedData, 0, result, GCM_IV_LENGTH, encryptedData.length);
return Base64.getEncoder().encodeToString(result);
} catch (Exception e) {
logger.error("Encryption failed", e);
throw new CryptographyException("Failed to encrypt data", e);
}
}
/**
* 解密敏感資料
*/
public String decrypt(String encryptedText) {
try {
byte[] encryptedData = Base64.getDecoder().decode(encryptedText);
// 提取IV
byte[] iv = new byte[GCM_IV_LENGTH];
System.arraycopy(encryptedData, 0, iv, 0, iv.length);
// 提取加密的資料
byte[] cipherText = new byte[encryptedData.length - GCM_IV_LENGTH];
System.arraycopy(encryptedData, GCM_IV_LENGTH, cipherText, 0, cipherText.length);
SecretKeySpec keySpec = new SecretKeySpec(
Base64.getDecoder().decode(encryptionKey), "AES");
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, parameterSpec);
byte[] plainText = cipher.doFinal(cipherText);
return new String(plainText, StandardCharsets.UTF_8);
} catch (Exception e) {
logger.error("Decryption failed", e);
throw new CryptographyException("Failed to decrypt data", e);
}
}
/**
* 雜湊敏感資料(不可逆)
*/
public String hashSensitiveData(String data, String salt) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
digest.update(salt.getBytes(StandardCharsets.UTF_8));
byte[] hashedBytes = digest.digest(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hashedBytes);
} catch (NoSuchAlgorithmException e) {
throw new CryptographyException("Hashing algorithm not available", e);
}
}
}
/**
* 資料庫敏感欄位加密
*/
@Entity
@Table(name = "user_profiles")
public class UserProfile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_id", nullable = false)
private Long userId;
// 使用JPA AttributeConverter自動加密/解密
@Convert(converter = EncryptedStringConverter.class)
@Column(name = "id_number")
private String idNumber; // 身分證字號 - 加密儲存
@Convert(converter = EncryptedStringConverter.class)
@Column(name = "phone_number")
private String phoneNumber; // 電話號碼 - 加密儲存
@Column(name = "address_hash")
private String addressHash; // 地址雜湊值 - 用於比對但不可逆
// Getters and setters...
}
@Converter
public class EncryptedStringConverter implements AttributeConverter<String, String> {
@Autowired
private EncryptionService encryptionService;
@Override
public String convertToDatabaseColumn(String attribute) {
if (attribute == null) return null;
return encryptionService.encrypt(attribute);
}
@Override
public String convertToEntityAttribute(String dbData) {
if (dbData == null) return null;
return encryptionService.decrypt(dbData);
}
}8.4 API 安全防護
Rate Limiting 和 API 保護
@Component
public class RateLimitingFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(RateLimitingFilter.class);
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 不同端點的限流配置
private final Map<String, RateLimit> rateLimits = Map.of(
"/api/auth/login", new RateLimit(5, Duration.ofMinutes(15)), // 登入:15分鐘5次
"/api/auth/register", new RateLimit(3, Duration.ofHours(1)), // 註冊:1小時3次
"/api/users", new RateLimit(100, Duration.ofMinutes(1)), // 一般API:1分鐘100次
"/api/admin", new RateLimit(50, Duration.ofMinutes(1)) // 管理API:1分鐘50次
);
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String clientIp = getClientIpAddress(httpRequest);
String requestPath = httpRequest.getRequestURI();
// 找到匹配的限流規則
RateLimit rateLimit = findMatchingRateLimit(requestPath);
if (rateLimit != null) {
if (!checkRateLimit(clientIp, requestPath, rateLimit)) {
httpResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
httpResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
httpResponse.getWriter().write(
"{\"error\":\"Rate limit exceeded\",\"message\":\"Too many requests\"}"
);
return;
}
}
chain.doFilter(request, response);
}
private boolean checkRateLimit(String clientIp, String path, RateLimit rateLimit) {
String key = "rate_limit:" + clientIp + ":" + path;
try {
String countStr = (String) redisTemplate.opsForValue().get(key);
int currentCount = countStr != null ? Integer.parseInt(countStr) : 0;
if (currentCount >= rateLimit.getMaxRequests()) {
logger.warn("Rate limit exceeded for IP: {} on path: {}", clientIp, path);
return false;
}
// 增加計數
if (currentCount == 0) {
redisTemplate.opsForValue().set(key, "1", rateLimit.getTimeWindow());
} else {
redisTemplate.opsForValue().increment(key);
}
return true;
} catch (Exception e) {
logger.error("Error checking rate limit", e);
return true; // 發生錯誤時允許通過,避免影響正常服務
}
}
private RateLimit findMatchingRateLimit(String path) {
return rateLimits.entrySet().stream()
.filter(entry -> path.startsWith(entry.getKey()))
.map(Map.Entry::getValue)
.findFirst()
.orElse(null);
}
@Data
@AllArgsConstructor
private static class RateLimit {
private int maxRequests;
private Duration timeWindow;
}
}8.5 前端安全防護
XSS 和 CSRF 防護
// XSS 防護工具
export class XSSProtection {
/**
* HTML 編碼防止 XSS 攻擊
*/
static escapeHtml(unsafe: string): string {
if (!unsafe) return unsafe;
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
/**
* 清理 URL 防止 JavaScript 協議
*/
static sanitizeUrl(url: string): string {
if (!url) return url;
const dangerous = /^(javascript|data|vbscript):/i;
if (dangerous.test(url)) {
return 'about:blank';
}
return url;
}
/**
* 安全地設置 innerHTML
*/
static safeSetInnerHTML(element: HTMLElement, html: string): void {
// 使用 DOMPurify 或類似庫清理 HTML
const cleanHTML = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'],
ALLOWED_ATTR: []
});
element.innerHTML = cleanHTML;
}
}
// CSRF 防護
export class CSRFProtection {
private static readonly CSRF_HEADER = 'X-CSRF-Token';
/**
* 獲取 CSRF Token
*/
static getCSRFToken(): string | null {
const metaTag = document.querySelector('meta[name="csrf-token"]');
return metaTag ? metaTag.getAttribute('content') : null;
}
/**
* 在 HTTP 請求中添加 CSRF Token
*/
static addCSRFToken(headers: Record<string, string>): Record<string, string> {
const token = this.getCSRFToken();
if (token) {
headers[this.CSRF_HEADER] = token;
}
return headers;
}
}
// Axios 攔截器設置安全標頭
axios.interceptors.request.use((config) => {
// 添加 CSRF Token
config.headers = CSRFProtection.addCSRFToken(config.headers || {});
// 設置 Content-Type
if (config.data && typeof config.data === 'object') {
config.headers['Content-Type'] = 'application/json';
}
return config;
});
// Vue 組件安全防護
export default defineComponent({
name: 'SecureUserProfile',
setup() {
const userInput = ref('');
const sanitizedInput = computed(() => {
return XSSProtection.escapeHtml(userInput.value);
});
const handleSubmit = async () => {
// 輸入驗證
if (!validateInput(userInput.value)) {
throw new Error('Invalid input');
}
try {
await userService.updateProfile({
content: sanitizedInput.value
});
} catch (error) {
console.error('Update failed:', error);
}
};
const validateInput = (input: string): boolean => {
// 基本驗證規則
if (!input || input.length > 1000) return false;
// 檢查危險內容
const dangerousPatterns = [
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
/javascript:/gi,
/on\w+\s*=/gi
];
return !dangerousPatterns.some(pattern => pattern.test(input));
};
return {
userInput,
sanitizedInput,
handleSubmit
};
}
});8.6 安全審計與監控
安全審計日誌
@Service
public class SecurityAuditService {
private static final Logger auditLogger = LoggerFactory.getLogger("SECURITY_AUDIT");
@Autowired
private AuditLogRepository auditLogRepository;
/**
* 記錄安全事件
*/
public void logSecurityEvent(SecurityEventType eventType, String userId,
String details, HttpServletRequest request) {
SecurityAuditLog auditLog = SecurityAuditLog.builder()
.eventType(eventType)
.userId(userId)
.ipAddress(getClientIpAddress(request))
.userAgent(request.getHeader("User-Agent"))
.details(details)
.timestamp(LocalDateTime.now())
.build();
// 異步保存到資料庫
CompletableFuture.runAsync(() -> {
try {
auditLogRepository.save(auditLog);
} catch (Exception e) {
auditLogger.error("Failed to save audit log", e);
}
});
// 同步記錄到日誌檔案
auditLogger.info("Security Event: {} | User: {} | IP: {} | Details: {}",
eventType, userId, auditLog.getIpAddress(), details);
// 高風險事件立即通知
if (isHighRiskEvent(eventType)) {
notifySecurityTeam(auditLog);
}
}
/**
* 檢測可疑活動
*/
@Scheduled(fixedRate = 300000) // 每5分鐘執行
public void detectSuspiciousActivity() {
LocalDateTime since = LocalDateTime.now().minusMinutes(15);
// 檢測多次失敗登入
List<Object[]> failedLogins = auditLogRepository
.findFailedLoginsByIpSince(since, 10);
failedLogins.forEach(result -> {
String ipAddress = (String) result[0];
Long count = (Long) result[1];
if (count >= 10) {
blockSuspiciousIP(ipAddress, Duration.ofHours(1));
logSecurityEvent(SecurityEventType.SUSPICIOUS_ACTIVITY,
null, "Multiple failed logins from IP: " + ipAddress, null);
}
});
// 檢測異常API調用
detectAnomalousAPIUsage(since);
}
private boolean isHighRiskEvent(SecurityEventType eventType) {
return Arrays.asList(
SecurityEventType.ACCOUNT_LOCKED,
SecurityEventType.PRIVILEGE_ESCALATION,
SecurityEventType.DATA_BREACH_ATTEMPT,
SecurityEventType.SUSPICIOUS_ACTIVITY
).contains(eventType);
}
private void notifySecurityTeam(SecurityAuditLog auditLog) {
// 發送安全警報
SecurityAlert alert = SecurityAlert.builder()
.severity(AlertSeverity.HIGH)
.eventType(auditLog.getEventType())
.description(auditLog.getDetails())
.ipAddress(auditLog.getIpAddress())
.timestamp(auditLog.getTimestamp())
.build();
securityAlertService.sendAlert(alert);
}
}
@Entity
@Table(name = "security_audit_logs")
public class SecurityAuditLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(EnumType.STRING)
@Column(name = "event_type", nullable = false)
private SecurityEventType eventType;
@Column(name = "user_id")
private String userId;
@Column(name = "ip_address", nullable = false)
private String ipAddress;
@Column(name = "user_agent")
private String userAgent;
@Column(name = "details", columnDefinition = "TEXT")
private String details;
@Column(name = "timestamp", nullable = false)
private LocalDateTime timestamp;
// Getters and setters...
}
public enum SecurityEventType {
LOGIN_SUCCESS,
LOGIN_FAILED,
LOGOUT,
PASSWORD_CHANGED,
ACCOUNT_LOCKED,
PRIVILEGE_ESCALATION,
DATA_BREACH_ATTEMPT,
SUSPICIOUS_ACTIVITY,
API_RATE_LIMIT_EXCEEDED,
UNAUTHORIZED_ACCESS_ATTEMPT
}8.7 安全配置檢查清單
認證與授權:
密碼安全
- 強密碼策略實施
- BCrypt 密碼加密
- 密碼過期政策
- 帳戶鎖定機制
會話管理
- JWT Token 安全配置
- Token 過期時間設置
- Refresh Token 輪換
- 安全的登出機制
輸入驗證:
資料驗證
- 所有輸入都經過驗證
- SQL 注入防護
- XSS 攻擊防護
- 檔案上傳安全檢查
API 安全
- Rate Limiting 實施
- CORS 配置正確
- API 版本控制
- 錯誤訊息不洩露敏感資訊
資料保護:
加密
- 傳輸中資料加密 (HTTPS/TLS)
- 靜態資料加密
- 敏感欄位加密儲存
- 安全的金鑰管理
存取控制
- 最小權限原則
- 角色基礎存取控制
- 資源級權限檢查
- 定期權限審查
監控與審計:
安全監控
- 全面的審計日誌
- 即時安全警報
- 異常行為檢測
- 入侵檢測系統
合規性
- GDPR 合規
- 資料保留政策
- 隱私政策實施
- 定期安全評估
監控與審計:
安全監控
- 全面的審計日誌
- 即時安全警報
- 異常行為檢測
- 入侵檢測系統
合規性
- GDPR 合規
- 資料保留政策
- 隱私政策實施
- 定期安全評估
9. 容器化與DevOps
9.1 Docker 容器化實踐
Dockerfile 最佳實踐
# 多階段構建
FROM openjdk:17-jdk-slim AS builder
# 設置工作目錄
WORKDIR /workspace/app
# 複製 Maven 配置檔案
COPY pom.xml .
COPY .mvn .mvn
COPY mvnw .
# 下載依賴(利用 Docker 層快取)
RUN ./mvnw dependency:go-offline
# 複製源碼
COPY src src
# 構建應用程式
RUN ./mvnw clean package -DskipTests
# 生產環境映像
FROM openjdk:17-jre-slim
# 創建非 root 用戶
RUN groupadd -r appuser && useradd -r -g appuser appuser
# 設置工作目錄
WORKDIR /app
# 複製構建產物
COPY --from=builder /workspace/app/target/*.jar app.jar
# 設置所有者
RUN chown appuser:appuser app.jar
# 切換到非 root 用戶
USER appuser
# 健康檢查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
# 暴露端口
EXPOSE 8080
# 啟動命令
ENTRYPOINT ["java", "-jar", "app.jar"]Docker Compose 配置
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=docker
- DATABASE_URL=jdbc:postgresql://db:5432/myapp
- DATABASE_USERNAME=myapp
- DATABASE_PASSWORD_FILE=/run/secrets/db_password
- REDIS_URL=redis://redis:6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
secrets:
- db_password
networks:
- app-network
volumes:
- app-logs:/app/logs
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
reservations:
memory: 256M
cpus: '0.25'
restart: unless-stopped
db:
image: postgres:15-alpine
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=myapp
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
volumes:
- postgres-data:/var/lib/postgresql/data
- ./init-scripts:/docker-entrypoint-initdb.d
ports:
- "5432:5432"
secrets:
- db_password
networks:
- app-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myapp"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
networks:
- app-network
command: redis-server --appendonly yes
volumes:
postgres-data:
redis-data:
app-logs:
networks:
app-network:
driver: bridge
secrets:
db_password:
file: ./secrets/db_password.txt9.2 CI/CD 流水線設計
GitHub Actions 工作流程
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Run tests
run: ./mvnw test
env:
DATABASE_URL: jdbc:postgresql://localhost:5432/test_db
DATABASE_USERNAME: postgres
DATABASE_PASSWORD: postgres
- name: Generate test report
uses: dorny/test-reporter@v1
if: success() || failure()
with:
name: Maven Tests
path: target/surefire-reports/*.xml
reporter: java-junit
- name: Code coverage
run: ./mvnw jacoco:report
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./target/site/jacoco/jacoco.xml
security-scan:
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run Snyk to check for vulnerabilities
uses: snyk/actions/maven@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
build-and-push:
runs-on: ubuntu-latest
needs: [test, security-scan]
if: github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Log in to Container Registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
deploy-staging:
runs-on: ubuntu-latest
needs: build-and-push
if: github.ref == 'refs/heads/develop'
environment: staging
steps:
- name: Deploy to staging
run: |
echo "Deploying to staging environment"
# 部署腳本
deploy-production:
runs-on: ubuntu-latest
needs: build-and-push
if: github.ref == 'refs/heads/main'
environment: production
steps:
- name: Deploy to production
run: |
echo "Deploying to production environment"
# 部署腳本9.3 Kubernetes 部署策略
Kubernetes 部署配置
# k8s/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: myapp
---
# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: myapp-config
namespace: myapp
data:
application.yml: |
spring:
profiles:
active: k8s
datasource:
url: jdbc:postgresql://postgres-service:5432/myapp
username: myapp
redis:
host: redis-service
port: 6379
---
# k8s/secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: myapp-secrets
namespace: myapp
type: Opaque
data:
database-password: <base64-encoded-password>
jwt-secret: <base64-encoded-jwt-secret>
---
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-deployment
namespace: myapp
labels:
app: myapp
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
annotations:
prometheus.io/scrape: "true"
prometheus.io/path: /actuator/prometheus
prometheus.io/port: "8080"
spec:
containers:
- name: myapp
image: ghcr.io/username/myapp:latest
ports:
- containerPort: 8080
env:
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: myapp-secrets
key: database-password
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: myapp-secrets
key: jwt-secret
volumeMounts:
- name: config-volume
mountPath: /app/config
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 20
periodSeconds: 5
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
volumes:
- name: config-volume
configMap:
name: myapp-config
---
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: myapp-service
namespace: myapp
spec:
selector:
app: myapp
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: ClusterIP
---
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-ingress
namespace: myapp
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/rate-limit: "100"
spec:
tls:
- hosts:
- api.myapp.com
secretName: myapp-tls
rules:
- host: api.myapp.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp-service
port:
number: 80Helm Chart 配置
# charts/myapp/Chart.yaml
apiVersion: v2
name: myapp
description: A Helm chart for MyApp
type: application
version: 1.0.0
appVersion: "1.0.0"
# charts/myapp/values.yaml
replicaCount: 3
image:
repository: ghcr.io/username/myapp
tag: "latest"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
targetPort: 8080
ingress:
enabled: true
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: api.myapp.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: myapp-tls
hosts:
- api.myapp.com
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 80
targetMemoryUtilizationPercentage: 80
config:
database:
host: postgres-service
port: 5432
name: myapp
redis:
host: redis-service
port: 63799.4 基礎設施即代碼
Terraform 配置
# terraform/main.tf
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.23"
}
}
}
provider "aws" {
region = var.aws_region
}
# VPC 配置
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
name = "${var.project_name}-vpc"
cidr = var.vpc_cidr
azs = var.availability_zones
private_subnets = var.private_subnet_cidrs
public_subnets = var.public_subnet_cidrs
enable_nat_gateway = true
enable_vpn_gateway = false
tags = var.common_tags
}
# EKS 叢集
module "eks" {
source = "terraform-aws-modules/eks/aws"
cluster_name = "${var.project_name}-eks"
cluster_version = var.kubernetes_version
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets
eks_managed_node_groups = {
main = {
desired_size = var.node_group_desired_size
max_size = var.node_group_max_size
min_size = var.node_group_min_size
instance_types = var.node_instance_types
capacity_type = "ON_DEMAND"
k8s_labels = {
Environment = var.environment
Application = var.project_name
}
}
}
tags = var.common_tags
}
# RDS 資料庫
resource "aws_db_instance" "main" {
identifier = "${var.project_name}-db"
engine = "postgres"
engine_version = var.postgres_version
instance_class = var.db_instance_class
allocated_storage = var.db_allocated_storage
max_allocated_storage = var.db_max_allocated_storage
storage_encrypted = true
db_name = var.db_name
username = var.db_username
password = var.db_password
vpc_security_group_ids = [aws_security_group.rds.id]
db_subnet_group_name = aws_db_subnet_group.main.name
backup_retention_period = var.db_backup_retention_period
backup_window = "03:00-04:00"
maintenance_window = "Sun:04:00-Sun:05:00"
skip_final_snapshot = false
final_snapshot_identifier = "${var.project_name}-db-final-snapshot"
tags = var.common_tags
}9.5 監控與日誌聚合
Prometheus 配置
# monitoring/prometheus-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: prometheus-config
namespace: monitoring
data:
prometheus.yml: |
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "/etc/prometheus/rules/*.yml"
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'kubernetes-apiservers'
kubernetes_sd_configs:
- role: endpoints
scheme: https
tls_config:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
relabel_configs:
- source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name]
action: keep
regex: default;kubernetes;https
- job_name: 'kubernetes-pods'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
action: replace
regex: ([^:]+)(?::\d+)?;(\d+)
replacement: $1:$2
target_label: __address__
alerts.yml: |
groups:
- name: application-alerts
rules:
- alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.1
for: 5m
labels:
severity: critical
annotations:
summary: "High error rate detected"
description: "Error rate is {{ $value }} for instance {{ $labels.instance }}"
- alert: HighMemoryUsage
expr: (container_memory_usage_bytes / container_spec_memory_limit_bytes) > 0.9
for: 2m
labels:
severity: warning
annotations:
summary: "High memory usage"
description: "Memory usage is above 90% for {{ $labels.pod }}"ELK Stack 配置
# logging/elasticsearch.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: elasticsearch
namespace: logging
spec:
serviceName: elasticsearch
replicas: 3
selector:
matchLabels:
app: elasticsearch
template:
metadata:
labels:
app: elasticsearch
spec:
containers:
- name: elasticsearch
image: elasticsearch:8.9.0
env:
- name: discovery.type
value: zen
- name: ES_JAVA_OPTS
value: "-Xms512m -Xmx512m"
ports:
- containerPort: 9200
name: rest
- containerPort: 9300
name: inter-node
volumeMounts:
- name: data
mountPath: /usr/share/elasticsearch/data
resources:
limits:
memory: 1Gi
cpu: 500m
requests:
memory: 512Mi
cpu: 250m
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 10Gi
---
# logging/logstash-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: logstash-config
namespace: logging
data:
logstash.conf: |
input {
beats {
port => 5044
}
}
filter {
if [fields][log_type] == "application" {
grok {
match => {
"message" => "%{TIMESTAMP_ISO8601:timestamp} \[%{DATA:thread}\] %{LOGLEVEL:level} %{DATA:logger} \[%{DATA:trace_id}\] \[%{DATA:user_id}\] - %{GREEDYDATA:log_message}"
}
}
date {
match => [ "timestamp", "yyyy-MM-dd HH:mm:ss.SSS" ]
}
if [log_message] =~ /^USER_ACTION:/ {
json {
source => "log_message"
target => "user_action"
}
}
}
}
output {
elasticsearch {
hosts => ["elasticsearch:9200"]
index => "logs-%{+YYYY.MM.dd}"
}
}10. 微服務架構
10.1 微服務設計原則
服務邊界劃分
// 用戶服務
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
@GetMapping("/{id}")
public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
return userService.findById(id)
.map(user -> ResponseEntity.ok(UserDto.from(user)))
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<UserDto> createUser(@Valid @RequestBody CreateUserRequest request) {
User user = userService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED)
.body(UserDto.from(user));
}
}
// 訂單服務
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
private final UserServiceClient userServiceClient;
@PostMapping
public ResponseEntity<OrderDto> createOrder(@Valid @RequestBody CreateOrderRequest request) {
// 驗證用戶存在
UserDto user = userServiceClient.getUser(request.getUserId())
.orElseThrow(() -> new UserNotFoundException("User not found"));
Order order = orderService.createOrder(request);
return ResponseEntity.status(HttpStatus.CREATED)
.body(OrderDto.from(order));
}
}資料一致性策略
// 事件驅動架構
@Component
public class OrderEventHandler {
private final PaymentServiceClient paymentClient;
private final InventoryServiceClient inventoryClient;
private final OrderRepository orderRepository;
@EventHandler
@Transactional
public void handle(OrderCreatedEvent event) {
try {
// 1. 檢查庫存
InventoryCheckResult inventoryResult = inventoryClient.checkInventory(
event.getOrderItems());
if (!inventoryResult.isAvailable()) {
publishEvent(new OrderInventoryUnavailableEvent(event.getOrderId()));
return;
}
// 2. 預留庫存
inventoryClient.reserveInventory(event.getOrderItems());
// 3. 處理付款
PaymentResult paymentResult = paymentClient.processPayment(
event.getPaymentInfo());
if (paymentResult.isSuccessful()) {
// 確認庫存
inventoryClient.confirmReservation(event.getOrderItems());
publishEvent(new OrderPaymentCompletedEvent(event.getOrderId()));
} else {
// 釋放庫存
inventoryClient.releaseReservation(event.getOrderItems());
publishEvent(new OrderPaymentFailedEvent(event.getOrderId()));
}
} catch (Exception e) {
logger.error("Failed to process order: {}", event.getOrderId(), e);
publishEvent(new OrderProcessingFailedEvent(event.getOrderId(), e.getMessage()));
}
}
}10.2 服務間通信
REST API 客戶端
@Component
public class UserServiceClient {
private final WebClient webClient;
private final CircuitBreaker circuitBreaker;
public UserServiceClient(WebClient.Builder webClientBuilder,
CircuitBreakerRegistry circuitBreakerRegistry) {
this.webClient = webClientBuilder
.baseUrl("http://user-service")
.build();
this.circuitBreaker = circuitBreakerRegistry.circuitBreaker("user-service");
}
public Optional<UserDto> getUser(Long userId) {
return circuitBreaker.executeSupplier(() ->
webClient.get()
.uri("/api/users/{id}", userId)
.retrieve()
.onStatus(HttpStatus.NOT_FOUND::equals,
response -> Mono.empty())
.bodyToMono(UserDto.class)
.timeout(Duration.ofSeconds(5))
.blockOptional()
);
}
@Retryable(value = {ResourceAccessException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000))
public UserDto createUser(CreateUserRequest request) {
return webClient.post()
.uri("/api/users")
.bodyValue(request)
.retrieve()
.bodyToMono(UserDto.class)
.timeout(Duration.ofSeconds(10))
.block();
}
}訊息佇列通信
@Configuration
@EnableRabbitMQ
public class RabbitMQConfig {
@Bean
public TopicExchange orderExchange() {
return new TopicExchange("order.exchange");
}
@Bean
public Queue orderCreatedQueue() {
return QueueBuilder.durable("order.created.queue").build();
}
@Bean
public Binding orderCreatedBinding() {
return BindingBuilder
.bind(orderCreatedQueue())
.to(orderExchange())
.with("order.created");
}
}
@Component
public class OrderEventPublisher {
private final RabbitTemplate rabbitTemplate;
public void publishOrderCreated(OrderCreatedEvent event) {
rabbitTemplate.convertAndSend(
"order.exchange",
"order.created",
event);
}
}
@RabbitListener(queues = "order.created.queue")
@Component
public class PaymentEventListener {
private final PaymentService paymentService;
@RabbitHandler
public void handleOrderCreated(OrderCreatedEvent event) {
try {
paymentService.processOrderPayment(event);
} catch (Exception e) {
logger.error("Failed to process payment for order: {}",
event.getOrderId(), e);
throw new AmqpRejectAndDontRequeueException("Payment processing failed", e);
}
}
}10.3 分散式事務處理
Saga 模式實現
@Component
public class OrderSagaOrchestrator {
private final PaymentServiceClient paymentClient;
private final InventoryServiceClient inventoryClient;
private final ShippingServiceClient shippingClient;
@SagaOrchestrationStart
public void processOrder(OrderCreatedEvent event) {
SagaTransaction saga = SagaTransaction.builder()
.sagaId(UUID.randomUUID().toString())
.orderId(event.getOrderId())
.build();
// 步驟 1: 檢查並預留庫存
saga.addStep(
() -> inventoryClient.reserveInventory(event.getOrderItems()),
() -> inventoryClient.releaseReservation(event.getOrderItems())
);
// 步驟 2: 處理付款
saga.addStep(
() -> paymentClient.processPayment(event.getPaymentInfo()),
() -> paymentClient.refundPayment(event.getPaymentInfo())
);
// 步驟 3: 安排配送
saga.addStep(
() -> shippingClient.scheduleShipping(event.getShippingInfo()),
() -> shippingClient.cancelShipping(event.getShippingInfo())
);
sagaManager.execute(saga);
}
}
@Component
public class SagaManager {
private final SagaRepository sagaRepository;
public void execute(SagaTransaction saga) {
sagaRepository.save(saga);
try {
for (SagaStep step : saga.getSteps()) {
step.execute();
saga.markStepCompleted(step);
sagaRepository.save(saga);
}
saga.markCompleted();
sagaRepository.save(saga);
} catch (Exception e) {
logger.error("Saga execution failed, starting compensation", e);
compensate(saga);
}
}
private void compensate(SagaTransaction saga) {
List<SagaStep> completedSteps = saga.getCompletedSteps();
Collections.reverse(completedSteps);
for (SagaStep step : completedSteps) {
try {
step.compensate();
saga.markStepCompensated(step);
sagaRepository.save(saga);
} catch (Exception e) {
logger.error("Compensation failed for step: {}", step.getName(), e);
}
}
saga.markFailed();
sagaRepository.save(saga);
}
}10.4 服務發現與負載均衡
Eureka 服務註冊
@SpringBootApplication
@EnableEurekaClient
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
// application.yml
eureka:
client:
service-url:
defaultZone: http://eureka-server:8761/eureka/
fetch-registry: true
register-with-eureka: true
instance:
instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${server.port}
prefer-ip-address: true
lease-renewal-interval-in-seconds: 10
lease-expiration-duration-in-seconds: 30Load Balancer 配置
@Configuration
public class LoadBalancerConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
@LoadBalanced
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
}
@Component
public class UserServiceClient {
private final RestTemplate restTemplate;
public UserDto getUser(Long userId) {
// 使用服務名稱而非 IP 地址
return restTemplate.getForObject(
"http://user-service/api/users/{id}",
UserDto.class,
userId);
}
}10.5 API Gateway 設計
Spring Cloud Gateway 配置
# application.yml
spring:
cloud:
gateway:
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
globalcors:
corsConfigurations:
'[/**]':
allowedOrigins: "*"
allowedMethods: "*"
allowedHeaders: "*"
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
- StripPrefix=2
- RateLimiter=user-service-limiter
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=2
- RateLimiter=order-service-limiter
- id: payment-service
uri: lb://payment-service
predicates:
- Path=/api/payments/**
filters:
- StripPrefix=2
- RateLimiter=payment-service-limiter自定義過濾器
@Component
public class AuthenticationGatewayFilterFactory
extends AbstractGatewayFilterFactory<AuthenticationGatewayFilterFactory.Config> {
private final JwtTokenProvider jwtTokenProvider;
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
if (isSecuredPath(request.getPath().value())) {
String token = extractToken(request);
if (token == null || !jwtTokenProvider.validateToken(token)) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// 添加用戶資訊到請求標頭
String userId = jwtTokenProvider.getUserId(token);
ServerHttpRequest mutatedRequest = request.mutate()
.header("X-User-Id", userId)
.build();
return chain.filter(exchange.mutate().request(mutatedRequest).build());
}
return chain.filter(exchange);
};
}
private boolean isSecuredPath(String path) {
return !path.startsWith("/api/auth/") &&
!path.equals("/api/health") &&
!path.equals("/api/docs");
}
private String extractToken(ServerHttpRequest request) {
String authorization = request.getHeaders().getFirst("Authorization");
if (authorization != null && authorization.startsWith("Bearer ")) {
return authorization.substring(7);
}
return null;
}
@Data
public static class Config {
// 配置參數
}
}11. 版本控制
11.1 Git 工作流程規範
Git Flow 分支管理策略
# 主要分支
master/main # 生產環境分支,只包含穩定版本
develop # 開發主分支,整合所有功能
# 支援分支
feature/* # 功能開發分支
release/* # 版本發布分支
hotfix/* # 緊急修復分支
# 分支命名規範
feature/user-authentication
feature/payment-integration
release/v1.2.0
hotfix/security-fix-v1.1.1功能開發工作流程
# 1. 從 develop 建立功能分支
git checkout develop
git pull origin develop
git checkout -b feature/user-profile-management
# 2. 開發功能並提交
git add .
git commit -m "feat: add user profile CRUD operations
- Implement user profile creation
- Add profile update functionality
- Include profile deletion with soft delete
- Add comprehensive unit tests
Closes #123"
# 3. 定期同步 develop 分支
git fetch origin
git rebase origin/develop
# 4. 推送功能分支
git push origin feature/user-profile-management
# 5. 建立 Pull Request
# 通過代碼審查後合併到 develop版本發布流程
# 1. 從 develop 建立 release 分支
git checkout develop
git pull origin develop
git checkout -b release/v1.2.0
# 2. 更新版本號和文檔
# - 更新 pom.xml 版本號
# - 更新 CHANGELOG.md
# - 更新 README.md
# 3. 進行最終測試和修復
git add .
git commit -m "chore: prepare release v1.2.0"
# 4. 合併到 master 並標記版本
git checkout master
git merge --no-ff release/v1.2.0
git tag -a v1.2.0 -m "Release version 1.2.0
Features:
- User profile management
- Enhanced authentication
- Performance improvements
Bug fixes:
- Fix memory leak in cache
- Resolve timezone issues"
# 5. 合併回 develop
git checkout develop
git merge --no-ff release/v1.2.0
# 6. 清理 release 分支
git branch -d release/v1.2.0
git push origin --delete release/v1.2.011.2 提交訊息規範
Conventional Commits 格式
# 基本格式
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
# 類型說明
feat: 新功能
fix: Bug 修復
docs: 文檔更新
style: 格式修改(不影響代碼運行)
refactor: 重構(既不修復bug也不新增功能)
perf: 效能優化
test: 添加或修改測試
chore: 建置過程或輔助工具變更
ci: CI/CD 相關變更
build: 建置系統或外部依賴變更提交訊息範例
# 功能新增
git commit -m "feat(auth): implement JWT token refresh mechanism
- Add automatic token refresh before expiration
- Include refresh token rotation for security
- Handle refresh failure gracefully with re-login
- Add comprehensive error handling
Closes #456"
# Bug 修復
git commit -m "fix(payment): resolve duplicate payment processing
The payment service was processing the same request multiple times
due to a race condition in the idempotency check.
- Add database-level unique constraint
- Implement proper transaction isolation
- Add request deduplication logic
Fixes #789"
# 破壞性變更
git commit -m "feat(api)!: restructure user API endpoints
BREAKING CHANGE: The user API endpoints have been restructured for better RESTful design.
Migration guide:
- GET /api/user/{id} → GET /api/users/{id}
- POST /api/user/create → POST /api/users
- PUT /api/user/{id}/update → PUT /api/users/{id}
Refs #234"
# 文檔更新
git commit -m "docs: update API documentation for v2.0
- Add new authentication endpoints
- Update request/response examples
- Include error code reference
- Add migration guide from v1.x"
# 重構
git commit -m "refactor(service): extract common validation logic
- Create ValidationUtils class for reusable validators
- Remove duplicated validation code from services
- Improve code maintainability and testability
- No functional changes"11.3 代碼審查流程
Pull Request 範本
## 變更描述
簡要描述此 PR 的變更內容和目的。
## 變更類型
- [ ] Bug 修復
- [ ] 新功能
- [ ] 重構
- [ ] 文檔更新
- [ ] 效能優化
- [ ] 其他(請說明)
## 測試
- [ ] 單元測試已通過
- [ ] 整合測試已通過
- [ ] 手動測試已完成
- [ ] 新增測試案例
## 檢查清單
- [ ] 代碼遵循專案的程式碼風格指引
- [ ] 自我審查已完成
- [ ] 添加了必要的註解
- [ ] 文檔已更新(如適用)
- [ ] 無破壞性變更,或已在描述中說明
## 相關議題
關閉 #(議題號碼)
## 截圖(如適用)
添加截圖或 GIF 來展示變更效果。
## 部署注意事項
- [ ] 需要資料庫遷移
- [ ] 需要配置文件更新
- [ ] 需要環境變數設置
- [ ] 其他部署注意事項代碼審查檢查清單
## 代碼品質審查
### 功能性
- [ ] 代碼實現了預期功能
- [ ] 邊界條件處理正確
- [ ] 錯誤處理適當
- [ ] 業務邏輯正確
### 代碼風格
- [ ] 命名規範一致
- [ ] 代碼格式正確
- [ ] 註解充分且準確
- [ ] 遵循專案的程式碼風格
### 安全性
- [ ] 輸入驗證充分
- [ ] 無安全漏洞
- [ ] 敏感資料處理正確
- [ ] 權限檢查適當
### 效能
- [ ] 無明顯效能問題
- [ ] 資料庫查詢優化
- [ ] 記憶體使用合理
- [ ] 演算法效率良好
### 可維護性
- [ ] 代碼結構清晰
- [ ] 函數和類別大小適中
- [ ] 依賴關係合理
- [ ] 易於測試和除錯
### 測試
- [ ] 測試覆蓋率足夠
- [ ] 測試案例完整
- [ ] 測試命名清楚
- [ ] 模擬和存根使用正確11.4 分支保護和自動化
GitHub 分支保護規則
# .github/branch_protection.yml
branches:
master:
protection:
required_status_checks:
strict: true
contexts:
- "continuous-integration"
- "code-quality-check"
- "security-scan"
enforce_admins: true
required_pull_request_reviews:
required_approving_review_count: 2
dismiss_stale_reviews: true
require_code_owner_reviews: true
restrictions:
users: []
teams: ["senior-developers"]
develop:
protection:
required_status_checks:
strict: true
contexts:
- "continuous-integration"
- "unit-tests"
required_pull_request_reviews:
required_approving_review_count: 1
dismiss_stale_reviews: trueGit Hooks 自動化
#!/bin/bash
# .git/hooks/pre-commit
# 提交前檢查
echo "執行提交前檢查..."
# 1. 檢查代碼格式
echo "檢查代碼格式..."
mvn spotless:check
if [ $? -ne 0 ]; then
echo "❌ 代碼格式檢查失敗,請執行 'mvn spotless:apply' 修復格式"
exit 1
fi
# 2. 執行單元測試
echo "執行單元測試..."
mvn test
if [ $? -ne 0 ]; then
echo "❌ 單元測試失敗"
exit 1
fi
# 3. 檢查提交訊息格式
echo "檢查提交訊息格式..."
commit_regex='^(feat|fix|docs|style|refactor|perf|test|chore|ci|build)(\(.+\))?: .{1,50}'
commit_msg=$(cat .git/COMMIT_EDITMSG)
if ! [[ $commit_msg =~ $commit_regex ]]; then
echo "❌ 提交訊息格式不正確"
echo "正確格式: type(scope): description"
echo "例如: feat(auth): add JWT authentication"
exit 1
fi
# 4. 檢查敏感資訊
echo "檢查敏感資訊..."
if git diff --cached --name-only | xargs grep -l "password\|secret\|key" > /dev/null; then
echo "⚠️ 檢測到可能的敏感資訊,請確認"
git diff --cached --name-only | xargs grep -n "password\|secret\|key"
read -p "確認要提交嗎?(y/N): " confirm
if [[ $confirm != [yY] ]]; then
exit 1
fi
fi
echo "✅ 所有檢查通過"#!/bin/bash
# .git/hooks/commit-msg
# 提交訊息檢查
commit_msg_file=$1
commit_msg=$(cat $commit_msg_file)
# 檢查提交訊息長度
if [ ${#commit_msg} -gt 72 ]; then
echo "❌ 提交訊息標題過長(超過72字符)"
exit 1
fi
# 檢查提交訊息格式
commit_pattern="^(feat|fix|docs|style|refactor|perf|test|chore|ci|build)(\(.+\))?: .+"
if ! [[ $commit_msg =~ $commit_pattern ]]; then
echo "❌ 提交訊息格式不符合 Conventional Commits 規範"
echo ""
echo "正確格式:"
echo " type(scope): description"
echo ""
echo "類型:"
echo " feat: 新功能"
echo " fix: 修復bug"
echo " docs: 文檔更新"
echo " style: 格式修改"
echo " refactor: 重構"
echo " perf: 效能優化"
echo " test: 測試相關"
echo " chore: 建置或輔助工具"
echo ""
echo "範例:"
echo " feat(auth): add JWT authentication"
echo " fix(payment): resolve duplicate transactions"
exit 1
fi
echo "✅ 提交訊息格式正確"11.5 版本標記和發布
語義化版本控制
# 版本號格式: MAJOR.MINOR.PATCH
# 例如: 1.2.3
# MAJOR: 不相容的 API 變更
# MINOR: 向後相容的功能新增
# PATCH: 向後相容的問題修正
# 預發布版本
1.2.3-alpha.1 # Alpha 版本
1.2.3-beta.2 # Beta 版本
1.2.3-rc.1 # Release Candidate
# 建置元資料
1.2.3+20230615 # 包含建置日期
1.2.3+git.abc123 # 包含 Git commit hash自動化版本標記
#!/bin/bash
# scripts/release.sh
# 自動化發布腳本
set -e
# 檢查當前分支
current_branch=$(git branch --show-current)
if [ "$current_branch" != "develop" ]; then
echo "❌ 請在 develop 分支執行發布"
exit 1
fi
# 檢查工作目錄是否乾淨
if [ -n "$(git status --porcelain)" ]; then
echo "❌ 工作目錄不乾淨,請提交所有變更"
exit 1
fi
# 獲取版本類型
echo "請選擇版本類型:"
echo "1. patch (bug修復)"
echo "2. minor (新功能)"
echo "3. major (破壞性變更)"
read -p "輸入選擇 (1/2/3): " version_type
case $version_type in
1) version_bump="patch" ;;
2) version_bump="minor" ;;
3) version_bump="major" ;;
*) echo "❌ 無效選擇"; exit 1 ;;
esac
# 更新版本號
current_version=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
current_version=${current_version#v}
IFS='.' read -ra VERSION_PARTS <<< "$current_version"
major=${VERSION_PARTS[0]}
minor=${VERSION_PARTS[1]:-0}
patch=${VERSION_PARTS[2]:-0}
case $version_bump in
"major") new_version="$((major + 1)).0.0" ;;
"minor") new_version="$major.$((minor + 1)).0" ;;
"patch") new_version="$major.$minor.$((patch + 1))" ;;
esac
echo "版本將從 v$current_version 更新到 v$new_version"
read -p "確認發布?(y/N): " confirm
if [[ $confirm != [yY] ]]; then
echo "發布已取消"
exit 0
fi
# 建立 release 分支
release_branch="release/v$new_version"
git checkout -b $release_branch
# 更新版本號文件
echo "更新版本號..."
sed -i "s/<version>.*<\/version>/<version>$new_version<\/version>/" pom.xml
git add pom.xml
# 生成 CHANGELOG
echo "生成 CHANGELOG..."
echo "## [$new_version] - $(date +%Y-%m-%d)" > temp_changelog
echo "" >> temp_changelog
git log --pretty=format:"- %s" v$current_version..HEAD >> temp_changelog
echo "" >> temp_changelog
cat CHANGELOG.md >> temp_changelog
mv temp_changelog CHANGELOG.md
git add CHANGELOG.md
# 提交版本更新
git commit -m "chore: prepare release v$new_version"
# 執行測試
echo "執行測試..."
mvn clean test
# 合併到 master
echo "合併到 master..."
git checkout master
git pull origin master
git merge --no-ff $release_branch
# 建立標籤
git tag -a "v$new_version" -m "Release v$new_version"
# 推送到遠端
git push origin master
git push origin "v$new_version"
# 合併回 develop
git checkout develop
git merge --no-ff $release_branch
git push origin develop
# 清理 release 分支
git branch -d $release_branch
echo "✅ 版本 v$new_version 發布成功!"11.6 協作最佳實踐
團隊協作規範
## Git 協作規範
### 分支命名規範
- `feature/description` - 功能開發
- `bugfix/description` - 錯誤修復
- `hotfix/description` - 緊急修復
- `refactor/description` - 代碼重構
- `docs/description` - 文檔更新
### 提交頻率
- 小而頻繁的提交
- 每個提交都應該是一個邏輯單元
- 避免大型的單一提交
- 定期推送到遠端分支
### 合併策略
- 功能分支使用 rebase 再合併
- 保持線性的提交歷史
- 使用 squash merge 整理提交
- 重要分支使用 merge commit
### 代碼審查要求
- 所有代碼都必須經過審查
- 至少需要一位資深開發者批准
- 自動化測試必須通過
- 代碼覆蓋率不能降低.gitignore 最佳實踐
# Java 編譯文件
*.class
*.jar
*.war
*.ear
*.zip
*.tar.gz
*.rar
# Maven
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
# IDE 文件
.idea/
*.iws
*.iml
*.ipr
.vscode/
.settings/
.metadata/
.classpath
.project
# 日誌文件
*.log
logs/
# 作業系統生成文件
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# 環境配置文件
.env
.env.local
.env.development
.env.test
.env.production
# 依賴目錄
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# 建置輸出
dist/
build/
out/
# 測試報告
coverage/
test-results/
*.lcov
# 臨時文件
*.tmp
*.temp
*.swp
*.swo
*~
# 敏感資料(絕對不要提交)
*.key
*.pem
*.p12
secrets.yml
credentials.json11.7 版本控制檢查清單
分支管理:
分支策略
- 使用 Git Flow 或類似的分支策略
- 分支命名規範一致
- 定期清理無用分支
- 保護重要分支
合併策略
- 使用適當的合併方式
- 保持提交歷史清晰
- 避免merge衝突
- 定期同步主分支
提交規範:
提交訊息
- 遵循 Conventional Commits
- 提交訊息清楚描述變更
- 包含相關的議題編號
- 避免無意義的提交訊息
提交內容
- 小而頻繁的提交
- 每個提交都是邏輯單元
- 不提交敏感資訊
- 適當的文件包含和排除
協作流程:
代碼審查
- 所有代碼都經過審查
- 使用Pull Request流程
- 審查檢查清單完整
- 及時回應審查意見
自動化
- CI/CD 流程設置完善
- 自動化測試集成
- 代碼品質檢查
- 安全掃描集成
12. 最佳實踐總結
12.1 開發生命週期最佳實踐
專案初始化檢查清單
## 新專案設置檢查清單
### 1. 專案結構
- [ ] 建立標準的 Maven/Gradle 專案結構
- [ ] 設置適當的 package 命名規範
- [ ] 建立測試目錄結構
- [ ] 配置資源文件目錄
### 2. 依賴管理
- [ ] 選擇穩定的依賴版本
- [ ] 設置依賴版本管理(BOM)
- [ ] 排除不必要的傳遞依賴
- [ ] 定期更新依賴版本
### 3. 配置管理
- [ ] 環境分離配置
- [ ] 使用外部化配置
- [ ] 敏感資料加密存儲
- [ ] 配置文件版本控制
### 4. 代碼品質
- [ ] 設置代碼格式化規則
- [ ] 配置靜態分析工具
- [ ] 建立測試覆蓋率要求
- [ ] 設置代碼審查流程開發環境標準化
# docker-compose.yml - 標準化開發環境
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=development
- DATABASE_URL=jdbc:postgresql://db:5432/myapp
depends_on:
- db
- redis
volumes:
- ./logs:/app/logs
db:
image: postgres:13
environment:
POSTGRES_DB: myapp
POSTGRES_USER: developer
POSTGRES_PASSWORD: devpass
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:6
ports:
- "6379:6379"
volumes:
- redis_data:/data
elasticsearch:
image: elasticsearch:7.14.0
environment:
- discovery.type=single-node
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ports:
- "9200:9200"
volumes:
- elasticsearch_data:/usr/share/elasticsearch/data
volumes:
postgres_data:
redis_data:
elasticsearch_data:12.2 程式碼品質標準
代碼審查檢查點
/**
* 代碼品質範例:高品質的 Service 類
*/
@Service
@Transactional(readOnly = true)
@Validated
public class UserServiceImpl implements UserService {
private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
private static final int MAX_BATCH_SIZE = 1000;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final EmailService emailService;
private final CacheManager cacheManager;
public UserServiceImpl(UserRepository userRepository,
PasswordEncoder passwordEncoder,
EmailService emailService,
CacheManager cacheManager) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.emailService = emailService;
this.cacheManager = cacheManager;
}
/**
* 創建用戶帳戶
*
* @param request 用戶創建請求,包含基本資料和驗證資訊
* @return 創建成功的用戶資料傳輸物件
* @throws DuplicateUserException 當用戶已存在時拋出
* @throws ValidationException 當輸入資料無效時拋出
*/
@Override
@Transactional
@PreAuthorize("hasRole('ADMIN') or hasRole('USER_MANAGER')")
public UserDto createUser(@Valid CreateUserRequest request) {
logger.info("Creating user with email: {}", request.getEmail());
// 1. 業務驗證
validateUserCreationRequest(request);
// 2. 檢查用戶是否已存在
if (userRepository.existsByEmail(request.getEmail())) {
throw new DuplicateUserException("User already exists with email: " + request.getEmail());
}
// 3. 建立用戶實體
User user = buildUserFromRequest(request);
try {
// 4. 保存用戶
User savedUser = userRepository.save(user);
// 5. 異步處理後續任務
processUserCreationTasks(savedUser);
// 6. 轉換並返回結果
UserDto result = convertToDto(savedUser);
logger.info("User created successfully with ID: {}", savedUser.getId());
return result;
} catch (DataIntegrityViolationException e) {
logger.error("Data integrity violation while creating user: {}", request.getEmail(), e);
throw new UserServiceException("Failed to create user due to data constraint violation", e);
} catch (Exception e) {
logger.error("Unexpected error while creating user: {}", request.getEmail(), e);
throw new UserServiceException("Failed to create user", e);
}
}
/**
* 批次獲取用戶資料(優化版本)
*/
@Override
@Cacheable(value = "userBatch", key = "#userIds.hashCode()")
public Map<Long, UserDto> getUsersBatch(@NotEmpty List<Long> userIds) {
if (userIds.size() > MAX_BATCH_SIZE) {
throw new IllegalArgumentException("Batch size exceeds maximum limit: " + MAX_BATCH_SIZE);
}
logger.debug("Fetching {} users in batch", userIds.size());
// 使用 IN 查詢避免 N+1 問題
List<User> users = userRepository.findByIdIn(userIds);
// 使用 Stream API 進行高效轉換
return users.stream()
.collect(Collectors.toMap(
User::getId,
this::convertToDto,
(existing, replacement) -> existing,
LinkedHashMap::new
));
}
/**
* 驗證用戶創建請求
*/
private void validateUserCreationRequest(CreateUserRequest request) {
// 密碼強度檢查
PasswordStrength strength = passwordValidator.validatePassword(request.getPassword());
if (strength == PasswordStrength.WEAK) {
throw new ValidationException("Password is too weak");
}
// 電子郵件域名檢查
if (!isAllowedEmailDomain(request.getEmail())) {
throw new ValidationException("Email domain is not allowed");
}
}
/**
* 從請求建立用戶實體
*/
private User buildUserFromRequest(CreateUserRequest request) {
return User.builder()
.email(request.getEmail().toLowerCase().trim())
.name(request.getName().trim())
.password(passwordEncoder.encode(request.getPassword()))
.status(UserStatus.PENDING)
.createdAt(LocalDateTime.now())
.build();
}
/**
* 異步處理用戶創建後續任務
*/
@Async("taskExecutor")
private void processUserCreationTasks(User user) {
try {
// 發送歡迎郵件
emailService.sendWelcomeEmail(user);
// 創建用戶配置檔
userProfileService.createDefaultProfile(user);
// 記錄審計日誌
auditService.logUserCreation(user);
} catch (Exception e) {
logger.error("Error processing user creation tasks for user: {}", user.getId(), e);
// 不拋出異常,避免影響主要流程
}
}
/**
* 實體轉換為 DTO
*/
private UserDto convertToDto(User user) {
return UserDto.builder()
.id(user.getId())
.email(user.getEmail())
.name(user.getName())
.status(user.getStatus())
.createdAt(user.getCreatedAt())
.lastLoginAt(user.getLastLoginAt())
.build();
}
}12.3 測試策略總覽
測試金字塔實施
/**
* 測試金字塔實施範例
*/
// 1. 單元測試(70%)- 快速、獨立、專注於單一功能
@ExtendWith(MockitoExtension.class)
class UserServiceUnitTest {
@Mock private UserRepository userRepository;
@Mock private EmailService emailService;
@InjectMocks private UserServiceImpl userService;
@Test
@DisplayName("應該成功創建用戶當提供有效資料時")
void shouldCreateUserSuccessfully_WhenValidDataProvided() {
// Given
CreateUserRequest request = createValidUserRequest();
when(userRepository.existsByEmail(request.getEmail())).thenReturn(false);
when(userRepository.save(any(User.class))).thenReturn(createTestUser());
// When
UserDto result = userService.createUser(request);
// Then
assertThat(result).isNotNull();
assertThat(result.getEmail()).isEqualTo(request.getEmail());
verify(emailService).sendWelcomeEmail(any(User.class));
}
}
// 2. 整合測試(20%)- 測試組件間協作
@SpringBootTest
@Testcontainers
@Transactional
class UserServiceIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13");
@Autowired private UserService userService;
@Autowired private TestEntityManager entityManager;
@Test
@DisplayName("完整用戶創建流程測試")
void shouldCompleteUserCreationFlow() {
// Given
CreateUserRequest request = createValidUserRequest();
// When
UserDto createdUser = userService.createUser(request);
entityManager.flush();
// Then
assertThat(createdUser.getId()).isNotNull();
// 驗證資料庫狀態
Optional<User> savedUser = userRepository.findById(createdUser.getId());
assertThat(savedUser).isPresent();
assertThat(savedUser.get().getEmail()).isEqualTo(request.getEmail());
}
}
// 3. 端到端測試(10%)- 測試完整用戶流程
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class UserEndToEndTest {
@Autowired private TestRestTemplate restTemplate;
@Test
@DisplayName("用戶註冊到登入完整流程")
void shouldCompleteUserRegistrationAndLoginFlow() {
// 1. 用戶註冊
CreateUserRequest registerRequest = createValidUserRequest();
ResponseEntity<UserDto> registerResponse = restTemplate.postForEntity(
"/api/users", registerRequest, UserDto.class);
assertThat(registerResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
// 2. 用戶登入
LoginRequest loginRequest = new LoginRequest(
registerRequest.getEmail(), registerRequest.getPassword());
ResponseEntity<LoginResponse> loginResponse = restTemplate.postForEntity(
"/api/auth/login", loginRequest, LoginResponse.class);
assertThat(loginResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(loginResponse.getBody().getAccessToken()).isNotNull();
// 3. 訪問受保護資源
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(loginResponse.getBody().getAccessToken());
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<UserDto> profileResponse = restTemplate.exchange(
"/api/users/profile", HttpMethod.GET, entity, UserDto.class);
assertThat(profileResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
}
}12.4 效能監控最佳實踐
應用程式監控配置
# application-production.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus,loggers
endpoint:
health:
show-details: always
show-components: always
metrics:
export:
prometheus:
enabled: true
distribution:
percentiles-histogram:
http.server.requests: true
spring.data.repository.invocations: true
percentiles:
http.server.requests: 0.5, 0.9, 0.95, 0.99
sla:
http.server.requests: 10ms,50ms,100ms,200ms,500ms,1s,2s
# 日誌配置
logging:
level:
com.tutorial: INFO
org.springframework.web: WARN
org.hibernate.SQL: WARN
pattern:
console: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file:
name: logs/application.log
max-size: 100MB
max-history: 30關鍵指標監控
@Component
public class ApplicationMetrics {
private final MeterRegistry meterRegistry;
private final Counter userCreationCounter;
private final Timer userCreationTimer;
private final Gauge activeUsersGauge;
public ApplicationMetrics(MeterRegistry meterRegistry, UserService userService) {
this.meterRegistry = meterRegistry;
// 業務指標
this.userCreationCounter = Counter.builder("users.created.total")
.description("Total number of users created")
.register(meterRegistry);
this.userCreationTimer = Timer.builder("users.creation.time")
.description("Time taken to create a user")
.register(meterRegistry);
// 系統指標
this.activeUsersGauge = Gauge.builder("users.active.count")
.description("Number of active users")
.register(meterRegistry, userService, UserService::getActiveUserCount);
}
public void recordUserCreation(Duration duration) {
userCreationCounter.increment();
userCreationTimer.record(duration);
}
@EventListener
public void handleUserCreated(UserCreatedEvent event) {
userCreationCounter.increment(Tags.of("source", event.getSource()));
}
@Scheduled(fixedRate = 60000) // 每分鐘
public void recordSystemMetrics() {
// 記錄記憶體使用情況
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
Gauge.builder("jvm.memory.heap.used.ratio")
.register(meterRegistry, heapUsage, usage ->
(double) usage.getUsed() / usage.getMax());
}
}12.5 部署和運維最佳實踐
Docker 化最佳實踐
# Dockerfile - 多階段建置優化
FROM openjdk:17-jdk-slim as builder
WORKDIR /app
COPY pom.xml .
COPY src src
# 下載依賴(可快取層)
RUN apt-get update && apt-get install -y maven
RUN mvn dependency:go-offline -B
# 建置應用程式
RUN mvn clean package -DskipTests
# 運行階段
FROM openjdk:17-jre-slim
# 創建非 root 用戶
RUN groupadd -r appuser && useradd -r -g appuser appuser
# 安裝必要工具
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# 複製應用程式
COPY --from=builder /app/target/*.jar app.jar
# 設置文件權限
RUN chown appuser:appuser app.jar
# 切換到非 root 用戶
USER appuser
# 健康檢查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
# 暴露端口
EXPOSE 8080
# JVM 優化參數
ENV JAVA_OPTS="-Xms512m -Xmx2g -XX:+UseG1GC -XX:+UseStringDeduplication"
# 啟動應用程式
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]生產環境 Kubernetes 部署
# k8s/deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
labels:
app: user-service
spec:
replicas: 3
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
spec:
containers:
- name: user-service
image: myregistry/user-service:v1.0.0
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "production"
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: database-secret
key: url
resources:
requests:
memory: "512Mi"
cpu: "200m"
limits:
memory: "2Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
volumeMounts:
- name: logs
mountPath: /app/logs
volumes:
- name: logs
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: user-service-svc
spec:
selector:
app: user-service
ports:
- port: 80
targetPort: 8080
type: ClusterIP12.6 持續改進流程
代碼品質檢測流程
# .github/workflows/quality-check.yml
name: Code Quality Check
on:
pull_request:
branches: [ develop, master ]
jobs:
quality-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Cache Maven dependencies
uses: actions/cache@v3
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2
- name: Run tests
run: mvn clean test
- name: Generate test report
uses: dorny/test-reporter@v1
if: success() || failure()
with:
name: Maven Tests
path: target/surefire-reports/*.xml
reporter: java-junit
- name: Run SonarQube analysis
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: |
mvn sonar:sonar \
-Dsonar.projectKey=java_tutorial \
-Dsonar.organization=myorg \
-Dsonar.host.url=https://sonarcloud.io
- name: Check code coverage
run: |
mvn jacoco:report
COVERAGE=$(mvn jacoco:report | grep -oP 'Total.*?(\d+)%' | grep -oP '\d+')
if [ "$COVERAGE" -lt 80 ]; then
echo "Code coverage is below 80%: $COVERAGE%"
exit 1
fi
- name: Security scan
uses: securecodewarrior/github-action-add-sarif@v1
with:
sarif-file: 'security-scan-results.sarif'12.7 團隊協作指南
開發團隊標準作業程序
## 開發流程標準作業程序(SOP)
### 日常開發流程
1. **開始新功能開發**
- 從 Jira/GitHub Issues 領取任務
- 從最新的 `develop` 分支建立功能分支
- 按照命名規範創建分支:`feature/ISSUE-123-user-authentication`
2. **開發過程中**
- 小而頻繁的提交
- 遵循代碼風格指引
- 撰寫單元測試
- 定期 rebase develop 分支
3. **功能完成後**
- 自我代碼審查
- 確保所有測試通過
- 更新相關文檔
- 建立 Pull Request
4. **代碼審查**
- 至少一位資深開發者審查
- 回應審查意見
- 修正後重新審查
- 通過後合併到 develop
### 緊急修復流程
1. **識別緊急問題**
- 評估問題影響範圍
- 確定修復優先級
- 建立 hotfix 分支
2. **快速修復**
- 最小化變更範圍
- 快速測試驗證
- 立即部署到測試環境
3. **正式發布**
- 代碼審查(可簡化)
- 部署到生產環境
- 監控系統狀態
- 建立事後檢討
### 發布流程
1. **準備發布**
- 功能凍結
- 集成測試
- 性能測試
- 安全掃描
2. **版本發布**
- 建立 release 分支
- 更新版本號
- 生成發布說明
- 標記版本
3. **部署上線**
- 藍綠部署
- 滾動發布
- 監控指標
- 回滾準備12.8 總結與展望
關鍵成功因素
## 高品質軟體開發的關鍵要素
### 1. 技術卓越
- **代碼品質**: 可讀、可維護、可測試
- **架構設計**: 模組化、可擴展、高內聚低耦合
- **效能優化**: 預防性優化、監控驅動改進
- **安全第一**: 設計階段考慮安全、多層防護
### 2. 流程規範
- **版本控制**: 清晰的分支策略、規範的提交
- **持續整合**: 自動化測試、快速反饋
- **代碼審查**: 知識分享、品質把關
- **文檔維護**: 及時更新、易於理解
### 3. 團隊協作
- **溝通透明**: 定期同步、問題及時反饋
- **知識分享**: 技術分享會、代碼審查學習
- **持續學習**: 跟上技術趨勢、提升技能
- **責任意識**: 對代碼品質負責、用戶體驗優先
### 4. 工具支援
- **開發環境**: 標準化、容器化
- **自動化工具**: 建置、測試、部署自動化
- **監控告警**: 實時監控、主動發現問題
- **文檔工具**: API 文檔、架構圖表持續改進計劃
## 年度技術改進計劃
### Q1: 基礎建設完善
- [ ] 完善 CI/CD 流程
- [ ] 建立代碼品質門檻
- [ ] 實施自動化測試
- [ ] 設置監控告警
### Q2: 安全性提升
- [ ] 實施安全編碼規範
- [ ] 建立安全審計流程
- [ ] 進行安全漏洞掃描
- [ ] 安全培訓課程
### Q3: 效能優化
- [ ] 應用程式效能調優
- [ ] 資料庫查詢優化
- [ ] 快取策略優化
- [ ] 前端效能提升
### Q4: 創新實踐
- [ ] 探索新技術應用
- [ ] 微服務架構演進
- [ ] 雲原生技術採用
- [ ] AI/ML 技術整合最終檢查清單
## 專案交付檢查清單
### 代碼品質
- [ ] 代碼審查完成,無重大問題
- [ ] 單元測試覆蓋率 ≥ 80%
- [ ] 整合測試覆蓋主要流程
- [ ] 靜態分析無嚴重問題
- [ ] 效能測試通過預設指標
### 安全合規
- [ ] 安全漏洞掃描通過
- [ ] 敏感資料加密存儲
- [ ] 存取控制正確實施
- [ ] 審計日誌完整記錄
- [ ] 隱私政策合規
### 運維就緒
- [ ] 監控指標完整設置
- [ ] 日誌記錄規範清晰
- [ ] 健康檢查端點可用
- [ ] 部署腳本測試通過
- [ ] 回滾方案準備完成
### 文檔完整
- [ ] API 文檔更新完整
- [ ] 部署指南清楚明確
- [ ] 用戶手冊編寫完成
- [ ] 故障排除指南準備
- [ ] 架構設計文檔完善
### 團隊準備
- [ ] 團隊成員熟悉系統
- [ ] 值班輪替計劃確定
- [ ] 緊急聯絡方式更新
- [ ] 知識轉移完成
- [ ] 培訓課程安排完成結語
這份程式寫作指引涵蓋了現代軟體開發的各個重要面向,從基礎的程式碼風格到複雜的系統架構,從個人開發習慣到團隊協作流程。
記住,優秀的程式碼不僅僅是能運行的程式碼,更是能讓團隊高效協作、易於維護、安全可靠的程式碼。持續學習、不斷改進,讓我們一起打造更好的軟體產品!
「程式碼是寫給人看的,碰巧電腦也能執行。」 - Harold Abelson