程式寫作指引

目錄

  1. 前言
  2. 程式碼風格與命名規範
  3. 註解與文件撰寫
  4. 錯誤處理與日誌紀錄
  5. 單元測試與TDD
  6. 安全性考量
  7. Spring Boot 常用功能實踐
  8. 資料庫設計與操作
  9. 效能優化
  10. 容器化與DevOps
  11. 微服務架構
  12. 版本控制
  13. 最佳實踐總結

前言

本指引旨在幫助開發團隊撰寫高品質、可維護且安全的程式碼。無論您是剛入行的新進開發人員,還是經驗豐富的資深工程師,都可以透過這份指引提升程式設計技能,並確保專案的長期成功。

適用技術棧

  • 前端:Vue 3.x、Angular、Tailwind CSS、TypeScript
  • 後端:Spring Boot 3.x、Java 17+
  • 其他:Maven、Git、JUnit 5、SonarQube

核心原則

  1. 可讀性優先:程式碼是給人讀的,其次才是給機器執行的
  2. 安全第一:始終考慮安全性,遵循 OWASP 最佳實踐
  3. 效能意識:在不犧牲可讀性的前提下,追求最佳效能
  4. 可維護性:寫出易於修改和擴展的程式碼
  5. 測試友好:設計時就考慮可測試性

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) {
        // ...
    }
}

注意事項:

  1. 統一使用英文命名,避免中英混雜
  2. 避免使用具有誤導性的名稱
  3. 保持命名風格在整個專案中的一致性
  4. 使用領域相關的術語,提高程式碼的表達力
  5. 定期檢視和重構命名,確保其仍然準確描述功能

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;
    }
}

注意事項:

  1. 避免冗餘註解:不要為顯而易見的程式碼添加註解
  2. 保持註解更新:程式碼修改時,務必同步更新相關註解
  3. 使用標準格式:遵循 JavaDoc 或 JSDoc 標準格式
  4. 關注為什麼而非什麼:解釋程式碼的目的和原因,而非具體做法
  5. 使用範例:對於複雜的 API,提供使用範例
  6. 多語言考量:如果是國際化專案,考慮使用英文註解

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);
}

注意事項:

  1. 永不記錄敏感資訊:密碼、身分證字號、信用卡號等
  2. 使用結構化日誌:便於後續分析和監控
  3. 適當的日誌級別:DEBUG < INFO < WARN < ERROR
  4. 包含足夠上下文:使用 MDC 或結構化資料
  5. 定期清理日誌:避免磁碟空間不足
  6. 監控日誌檔案:設置日誌警報和監控

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);
}

注意事項:

  1. 遵循 AAA 模式:Arrange(準備)、Act(執行)、Assert(驗證)
  2. 一個測試一個概念:每個測試方法只驗證一個行為
  3. 使用描述性測試名稱:清楚表達測試意圖
  4. 獨立的測試:測試之間不應該有依賴關係
  5. 快速執行:單元測試應該能快速執行
  6. 保持測試簡單:測試程式碼應該比產品程式碼更簡單

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, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;")
            .replace(/"/g, "&quot;")
            .replace(/'/g, "&#039;");
    }
    
    /**
     * 清理 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);
    }
}

安全性注意事項:

  1. 永遠驗證輸入:不信任任何外部輸入
  2. 使用參數化查詢:防止 SQL 注入
  3. 適當的輸出編碼:防止 XSS 攻擊
  4. 實施認證和授權:確保適當的存取控制
  5. 記錄安全事件:監控可疑活動
  6. 定期安全審查:保持安全最佳實踐

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 開發注意事項:

  1. 使用 @Transactional 管理事務:確保資料一致性
  2. 適當的快取策略:避免過度快取或快取穿透
  3. 批次處理監控:記錄處理進度和錯誤
  4. JWT 安全性:使用強密鑰並適當設置過期時間
  5. 資料庫連線池優化:根據負載調整連線池大小
  6. 分頁查詢:避免一次載入大量資料

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 效能優化檢查清單

後端效能檢查:

  1. 資料庫優化

    • 適當的索引設計
    • 查詢語句優化
    • 連線池配置
    • 避免 N+1 查詢問題
  2. 快取策略

    • 多層快取架構
    • 快取預熱機制
    • 適當的失效策略
    • 快取穿透保護
  3. JVM 調優

    • 堆記憶體大小配置
    • 垃圾收集器選擇
    • GC 參數調優
    • 記憶體洩漏檢查

前端效能檢查:

  1. 程式碼優化

    • 程式碼分割和懶載入
    • 無用程式碼移除
    • 資源壓縮
    • 圖片優化
  2. 載入效能

    • 首屏載入時間 < 2秒
    • 資源預載入
    • CDN 使用
    • HTTP/2 支援
  3. 運行時效能

    • 虛擬滾動實現
    • 防抖和節流
    • 記憶化計算
    • 長列表優化

監控和報告:

  1. 關鍵指標監控

    • 回應時間 (P95 < 200ms)
    • 吞吐量監控
    • 錯誤率 < 0.1%
    • 資源使用率
  2. 持續優化

    • 定期效能測試
    • 效能基準建立
    • 瓶頸識別和解決
    • 效能回歸檢測

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, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;")
            .replace(/"/g, "&quot;")
            .replace(/'/g, "&#039;");
    }
    
    /**
     * 清理 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 安全配置檢查清單

認證與授權:

  1. 密碼安全

    • 強密碼策略實施
    • BCrypt 密碼加密
    • 密碼過期政策
    • 帳戶鎖定機制
  2. 會話管理

    • JWT Token 安全配置
    • Token 過期時間設置
    • Refresh Token 輪換
    • 安全的登出機制

輸入驗證:

  1. 資料驗證

    • 所有輸入都經過驗證
    • SQL 注入防護
    • XSS 攻擊防護
    • 檔案上傳安全檢查
  2. API 安全

    • Rate Limiting 實施
    • CORS 配置正確
    • API 版本控制
    • 錯誤訊息不洩露敏感資訊

資料保護:

  1. 加密

    • 傳輸中資料加密 (HTTPS/TLS)
    • 靜態資料加密
    • 敏感欄位加密儲存
    • 安全的金鑰管理
  2. 存取控制

    • 最小權限原則
    • 角色基礎存取控制
    • 資源級權限檢查
    • 定期權限審查

監控與審計:

  1. 安全監控

    • 全面的審計日誌
    • 即時安全警報
    • 異常行為檢測
    • 入侵檢測系統
  2. 合規性

    • GDPR 合規
    • 資料保留政策
    • 隱私政策實施
    • 定期安全評估

監控與審計:

  1. 安全監控

    • 全面的審計日誌
    • 即時安全警報
    • 異常行為檢測
    • 入侵檢測系統
  2. 合規性

    • 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.txt

9.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: 80

Helm 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: 6379

9.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: 30

Load 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.0

11.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: true

Git 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.json

11.7 版本控制檢查清單

分支管理:

  1. 分支策略

    • 使用 Git Flow 或類似的分支策略
    • 分支命名規範一致
    • 定期清理無用分支
    • 保護重要分支
  2. 合併策略

    • 使用適當的合併方式
    • 保持提交歷史清晰
    • 避免merge衝突
    • 定期同步主分支

提交規範:

  1. 提交訊息

    • 遵循 Conventional Commits
    • 提交訊息清楚描述變更
    • 包含相關的議題編號
    • 避免無意義的提交訊息
  2. 提交內容

    • 小而頻繁的提交
    • 每個提交都是邏輯單元
    • 不提交敏感資訊
    • 適當的文件包含和排除

協作流程:

  1. 代碼審查

    • 所有代碼都經過審查
    • 使用Pull Request流程
    • 審查檢查清單完整
    • 及時回應審查意見
  2. 自動化

    • 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: ClusterIP

12.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