後端開發指引

目錄

  1. 開發原則
  2. 專案結構與命名規範
  3. API 設計規範
  4. 資料庫存取與 ORM 規範
  5. 安全性規範
  6. 效能與擴展性指引
  7. 測試與品質保證
  8. 部署與維運指引
  9. 日誌管理與監控
  10. 資料驗證與清理
  11. 國際化與本地化
  12. 文件生成與 API 規範
  13. 第三方整合規範
  14. 程式碼審查與品質控制
  15. 依賴與配置管理
  16. 備份與災難恢復

1. 開發原則

1.1 架構模式

Clean Architecture 實作原則

  • 依賴反轉原則:內層不依賴外層,外層依賴內層
  • 單一職責原則:每個類別/模組只負責一個職責
  • 開放封閉原則:對擴展開放,對修改封閉
  • 介面隔離原則:使用者不應依賴不需要的介面
架構分層結構:
┌─────────────────────────────────────┐
│           Presentation Layer        │  ← Controllers, DTOs
├─────────────────────────────────────┤
│           Application Layer         │  ← Use Cases, Services
├─────────────────────────────────────┤
│             Domain Layer            │  ← Entities, Repositories
├─────────────────────────────────────┤
│          Infrastructure Layer       │  ← Database, External APIs
└─────────────────────────────────────┘

分層設計規範

  • Presentation Layer(表現層)

    • 負責接收 HTTP 請求並返回回應
    • 包含 Controller、DTO、Validator
    • 不包含業務邏輯
  • Application Layer(應用層)

    • 協調領域物件執行業務流程
    • 包含 Use Case、Application Service
    • 處理事務邊界
  • Domain Layer(領域層)

    • 包含核心業務邏輯
    • 實體、值物件、領域服務、Repository 介面
    • 不依賴外部框架
  • Infrastructure Layer(基礎設施層)

    • 實作技術細節
    • Database、External API、Message Queue
    • Framework 相關實作

1.2 模組化與可維護性原則

模組化設計

  • 按功能模組劃分:每個業務功能獨立成模組
  • 共用模組分離:通用功能抽取成共用模組
  • 循環依賴檢查:使用 ArchUnit 檢查模組間依賴關係
// 模組結構範例
com.company.platform
├── common/              // 共用模組
│   ├── exception/       // 異常處理
│   ├── util/           // 工具類別
│   └── config/         // 共用配置
├── user/               // 使用者模組
│   ├── domain/
│   ├── application/
│   ├── infrastructure/
│   └── presentation/
└── order/              // 訂單模組
    ├── domain/
    ├── application/
    ├── infrastructure/
    └── presentation/

可維護性原則

  • 程式碼可讀性:使用有意義的命名、適當的註解
  • 低耦合高內聚:模組間耦合度低,模組內聚度高
  • 設計模式應用:適當使用設計模式提升程式碼品質
  • 重構策略:定期重構,保持程式碼整潔

1.3 版本控制與分支策略

Git Flow 分支策略

master/main     ──────●────────●────────●──────→ (生產版本)
                       ↑        ↑        ↑
release         ────●──┘      ●──┘      ●──┘    (發佈分支)
                    ↑         ↑         ↑
develop         ●──●─────●────●─────●───●────→    (開發主分支)
                   ↑     ↑           ↑
feature         ●──┘   ●──┘         ●──┘        (功能分支)

分支命名規範

  • feature/功能名稱feature/user-authentication
  • bugfix/問題描述bugfix/login-validation-error
  • hotfix/緊急修復hotfix/security-patch-20241201
  • release/版本號release/v1.2.0

Commit 訊息規範

<type>(<scope>): <subject>

<body>

<footer>

Type 類型:

  • feat: 新功能
  • fix: 錯誤修復
  • docs: 文件更新
  • style: 程式碼格式
  • refactor: 重構
  • test: 測試
  • chore: 構建過程或輔助工具的變動

範例:

feat(user): add user authentication service

- Implement JWT token generation
- Add password encryption
- Create user login endpoint

Closes #123

2. 專案結構與命名規範

2.1 Maven 專案目錄結構

project-root/
├── pom.xml                          # Maven 配置文件
├── README.md                        # 專案說明文件
├── docker-compose.yml               # 本地開發環境
├── Dockerfile                       # 容器化配置
├── .gitignore                       # Git 忽略文件
├── .github/                         # GitHub 相關配置
│   ├── workflows/                   # CI/CD 流程
│   └── 指引/                        # 開發指引文件
├── docs/                            # 專案文件
│   ├── api/                         # API 文件
│   ├── database/                    # 資料庫設計文件
│   └── architecture/                # 架構設計文件
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/company/platform/
│   │   │       ├── common/          # 共用模組
│   │   │       │   ├── config/      # 配置類別
│   │   │       │   ├── exception/   # 異常處理
│   │   │       │   ├── util/        # 工具類別
│   │   │       │   └── constant/    # 常數定義
│   │   │       ├── user/            # 使用者模組
│   │   │       │   ├── domain/      # 領域層
│   │   │       │   │   ├── entity/  # 實體
│   │   │       │   │   ├── repository/ # Repository 介面
│   │   │       │   │   └── service/ # 領域服務
│   │   │       │   ├── application/ # 應用層
│   │   │       │   │   ├── service/ # 應用服務
│   │   │       │   │   └── dto/     # 資料傳輸物件
│   │   │       │   ├── infrastructure/ # 基礎設施層
│   │   │       │   │   ├── repository/ # Repository 實作
│   │   │       │   │   ├── external/   # 外部服務整合
│   │   │       │   │   └── config/     # 模組配置
│   │   │       │   └── presentation/   # 表現層
│   │   │       │       ├── controller/ # 控制器
│   │   │       │       ├── dto/        # 請求/回應 DTO
│   │   │       │       └── validator/  # 驗證器
│   │   │       └── Application.java    # 應用程式入口
│   │   └── resources/
│   │       ├── application.yml          # 主配置文件
│   │       ├── application-dev.yml      # 開發環境配置
│   │       ├── application-test.yml     # 測試環境配置
│   │       ├── application-prod.yml     # 生產環境配置
│   │       ├── db/migration/            # 資料庫遷移腳本
│   │       ├── static/                  # 靜態資源
│   │       └── templates/               # 模板文件
│   └── test/
│       ├── java/
│       │   └── com/company/platform/
│       │       ├── unit/                # 單元測試
│       │       ├── integration/         # 整合測試
│       │       └── architecture/        # 架構測試
│       └── resources/
│           ├── application-test.yml     # 測試配置
│           └── testdata/                # 測試資料
├── target/                              # 編譯輸出目錄
└── logs/                                # 日誌文件目錄

2.2 Package 命名規則

基本命名原則

  • 全小寫字母:使用小寫字母,避免大寫
  • 點分隔命名:使用點號分隔不同層級
  • 域名反轉:以公司域名反轉開頭
  • 有意義命名:Package 名稱應該明確表達其功能

標準 Package 結構

com.company.platform                    // 根 package
├── common                              // 共用模組
│   ├── config                          // 配置相關
│   ├── exception                       // 異常處理
│   ├── util                           // 工具類別
│   ├── constant                       // 常數定義
│   └── annotation                     // 自定義註解
├── {module}                           // 業務模組 (如 user, order, payment)
│   ├── domain                         // 領域層
│   │   ├── entity                     // 實體
│   │   ├── repository                 // Repository 介面
│   │   ├── service                    // 領域服務
│   │   └── valueobject               // 值物件
│   ├── application                    // 應用層
│   │   ├── service                    // 應用服務
│   │   ├── usecase                    // 用例
│   │   └── dto                        // 應用層 DTO
│   ├── infrastructure                 // 基礎設施層
│   │   ├── repository                 // Repository 實作
│   │   ├── external                   // 外部服務
│   │   ├── config                     // 模組配置
│   │   └── persistence               // 持久化相關
│   └── presentation                   // 表現層
│       ├── controller                 // REST 控制器
│       ├── dto                        // 請求/回應 DTO
│       ├── validator                  // 驗證器
│       └── mapper                     // 物件映射器

2.3 檔案與類別命名規範

類別命名規範

實體類別 (Entity)

// 使用名詞,PascalCase
public class User { }
public class OrderItem { }
public class PaymentTransaction { }

服務類別 (Service)

// 業務名稱 + Service 後綴
public class UserService { }
public class OrderProcessingService { }
public class PaymentCalculationService { }

控制器類別 (Controller)

// 資源名稱 + Controller 後綴
@RestController
public class UserController { }

@RestController  
public class OrderController { }

Repository 類別

// 實體名稱 + Repository 後綴
public interface UserRepository extends JpaRepository<User, Long> { }
public class UserRepositoryImpl implements UserRepository { }

DTO 類別

// 用途 + 實體名稱 + Dto/Request/Response 後綴
public class CreateUserRequestDto { }
public class UserResponseDto { }
public class UpdateOrderRequestDto { }

異常類別

// 具體異常 + Exception 後綴
public class UserNotFoundException extends RuntimeException { }
public class InvalidPaymentAmountException extends BusinessException { }

方法命名規範

CRUD 操作

// 查詢方法
public User findById(Long id) { }
public List<User> findByEmail(String email) { }
public Page<User> findAll(Pageable pageable) { }

// 建立方法
public User create(CreateUserRequestDto request) { }
public User save(User user) { }

// 更新方法
public User update(Long id, UpdateUserRequestDto request) { }
public void updateStatus(Long id, UserStatus status) { }

// 刪除方法
public void deleteById(Long id) { }
public void softDelete(Long id) { }

// 業務方法
public void activateUser(Long userId) { }
public boolean validatePassword(String password) { }
public void processPayment(PaymentRequestDto request) { }

布林方法

// 使用 is, has, can, should 等前綴
public boolean isActive() { }
public boolean hasPermission(String permission) { }
public boolean canProcess() { }
public boolean shouldNotify() { }

變數命名規範

區域變數和參數

// 使用 camelCase,有意義的名稱
public void processOrder(Long orderId, BigDecimal totalAmount) {
    User currentUser = getCurrentUser();
    List<OrderItem> orderItems = orderItemRepository.findByOrderId(orderId);
    PaymentMethod preferredPaymentMethod = user.getPreferredPaymentMethod();
}

常數

// 使用 UPPER_SNAKE_CASE
public static final String DEFAULT_ENCODING = "UTF-8";
public static final int MAX_RETRY_ATTEMPTS = 3;
public static final BigDecimal TAX_RATE = new BigDecimal("0.05");

成員變數

public class UserService {
    // 使用 camelCase
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final EmailService emailService;
}

檔案命名規範

Java 類別檔案

  • 檔名必須與類別名稱完全一致
  • 使用 PascalCase
  • 一個檔案只包含一個 public 類別

配置檔案

application.yml              # 主配置檔案
application-{profile}.yml    # 環境特定配置
logback-spring.xml           # 日誌配置
database-migration.sql       # 資料庫遷移腳本

測試檔案

// 測試類別命名:被測試類別 + Test 後綴
UserServiceTest.java         // 單元測試
UserControllerIntegrationTest.java  // 整合測試
UserRepositoryTest.java      // Repository 測試

3. API 設計規範

3.1 RESTful API 命名與版本化策略

URL 命名規範

資源命名原則

  • 使用名詞而非動詞
  • 使用複數形式表示集合資源
  • 使用小寫字母和連字符分隔
  • URL 應該直觀且自解釋
# 正確的 URL 設計
GET    /api/v1/users                    # 獲取使用者列表
GET    /api/v1/users/{id}               # 獲取特定使用者
POST   /api/v1/users                    # 建立新使用者
PUT    /api/v1/users/{id}               # 更新使用者
DELETE /api/v1/users/{id}               # 刪除使用者

# 巢狀資源
GET    /api/v1/users/{userId}/orders    # 獲取使用者的訂單
POST   /api/v1/users/{userId}/orders    # 為使用者建立訂單

# 複雜查詢
GET    /api/v1/users?status=active&role=admin
GET    /api/v1/orders?from=2024-01-01&to=2024-12-31

HTTP 方法使用規範

HTTP 方法用途冪等性安全性範例
GET查詢資源GET /api/v1/users/{id}
POST建立資源POST /api/v1/users
PUT完整更新資源PUT /api/v1/users/{id}
PATCH部分更新資源PATCH /api/v1/users/{id}
DELETE刪除資源DELETE /api/v1/users/{id}

API 版本化策略

URL 版本化(推薦)

# 在 URL 路徑中包含版本號
GET /api/v1/users
GET /api/v2/users

# 版本號規則:主版本號.次版本號
v1.0, v1.1, v2.0

Header 版本化

GET /api/users
Accept: application/vnd.company.v1+json
API-Version: v1

版本管理原則

  • 主版本號:不向後相容的變更
  • 次版本號:向後相容的功能新增
  • 修訂號:向後相容的錯誤修復
  • 同時維護最多 3 個主版本
  • 提前 6 個月通知版本淘汰

3.2 請求與回應格式

標準請求格式

請求 Header

Content-Type: application/json;charset=UTF-8
Accept: application/json
Authorization: Bearer {jwt-token}
X-Request-ID: {uuid}
X-Client-Version: {version}

分頁請求參數

GET /api/v1/users?page=0&size=20&sort=createdAt,desc

查詢參數規範

# 篩選條件
GET /api/v1/users?status=active&role=admin&email=user@example.com

# 日期範圍
GET /api/v1/orders?startDate=2024-01-01&endDate=2024-12-31

# 搜尋
GET /api/v1/users?search=john&searchFields=name,email

標準回應格式

成功回應結構

{
  "success": true,
  "code": "200",
  "message": "操作成功",
  "data": {
    // 實際資料內容
  },
  "timestamp": "2024-12-01T10:30:00Z",
  "requestId": "550e8400-e29b-41d4-a716-446655440000"
}

分頁回應結構

{
  "success": true,
  "code": "200",
  "message": "查詢成功",
  "data": {
    "content": [
      // 分頁資料
    ],
    "page": {
      "number": 0,
      "size": 20,
      "totalElements": 100,
      "totalPages": 5,
      "first": true,
      "last": false
    }
  },
  "timestamp": "2024-12-01T10:30:00Z",
  "requestId": "550e8400-e29b-41d4-a716-446655440000"
}

錯誤回應結構

{
  "success": false,
  "code": "USER_NOT_FOUND",
  "message": "使用者不存在",
  "errors": [
    {
      "field": "userId",
      "code": "INVALID_VALUE",
      "message": "使用者 ID 無效"
    }
  ],
  "timestamp": "2024-12-01T10:30:00Z",
  "requestId": "550e8400-e29b-41d4-a716-446655440000"
}

JSON Schema 範例

使用者建立請求 Schema

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "title": "CreateUserRequest",
  "required": ["username", "email", "password"],
  "properties": {
    "username": {
      "type": "string",
      "minLength": 3,
      "maxLength": 50,
      "pattern": "^[a-zA-Z0-9_]+$"
    },
    "email": {
      "type": "string",
      "format": "email",
      "maxLength": 100
    },
    "password": {
      "type": "string",
      "minLength": 8,
      "maxLength": 100,
      "pattern": "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]"
    },
    "firstName": {
      "type": "string",
      "maxLength": 50
    },
    "lastName": {
      "type": "string",
      "maxLength": 50
    },
    "phoneNumber": {
      "type": "string",
      "pattern": "^\\+?[1-9]\\d{1,14}$"
    }
  },
  "additionalProperties": false
}

3.3 錯誤處理與錯誤碼設計

HTTP 狀態碼使用規範

狀態碼名稱使用場景範例
200OK成功處理請求查詢、更新成功
201Created成功建立資源建立使用者成功
204No Content成功處理但無回應內容刪除成功
400Bad Request請求參數錯誤驗證失敗
401Unauthorized未認證Token 無效
403Forbidden已認證但無權限存取被拒絕
404Not Found資源不存在使用者不存在
409Conflict資源衝突使用者名稱重複
422Unprocessable Entity語義錯誤業務邏輯錯誤
429Too Many Requests請求頻率過高超過 API 限制
500Internal Server Error伺服器內部錯誤系統異常

錯誤碼設計規範

錯誤碼結構

{MODULE}_{ERROR_TYPE}_{SPECIFIC_ERROR}

範例:
USER_VALIDATION_EMAIL_INVALID
ORDER_BUSINESS_INSUFFICIENT_STOCK
PAYMENT_EXTERNAL_GATEWAY_TIMEOUT

錯誤碼分類

public enum ErrorCode {
    // 系統級錯誤 (SYS)
    SYS_INTERNAL_ERROR("SYS_001", "系統內部錯誤"),
    SYS_DATABASE_ERROR("SYS_002", "資料庫錯誤"),
    SYS_EXTERNAL_SERVICE_ERROR("SYS_003", "外部服務錯誤"),
    
    // 驗證錯誤 (VAL)
    VAL_REQUIRED_FIELD_MISSING("VAL_001", "必填欄位缺失"),
    VAL_INVALID_FORMAT("VAL_002", "格式不正確"),
    VAL_OUT_OF_RANGE("VAL_003", "值超出範圍"),
    
    // 認證授權錯誤 (AUTH)
    AUTH_TOKEN_INVALID("AUTH_001", "Token 無效"),
    AUTH_TOKEN_EXPIRED("AUTH_002", "Token 已過期"),
    AUTH_INSUFFICIENT_PERMISSION("AUTH_003", "權限不足"),
    
    // 業務邏輯錯誤 (BIZ)
    BIZ_USER_NOT_FOUND("BIZ_001", "使用者不存在"),
    BIZ_DUPLICATE_EMAIL("BIZ_002", "電子郵件已存在"),
    BIZ_INSUFFICIENT_BALANCE("BIZ_003", "餘額不足");
    
    private final String code;
    private final String message;
}

異常處理實作

全域異常處理器

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(ValidationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResponse<Void> handleValidationException(ValidationException ex) {
        log.warn("Validation error: {}", ex.getMessage());
        return ApiResponse.error(ex.getErrorCode(), ex.getMessage());
    }

    @ExceptionHandler(BusinessException.class)
    @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
    public ApiResponse<Void> handleBusinessException(BusinessException ex) {
        log.warn("Business error: {}", ex.getMessage());
        return ApiResponse.error(ex.getErrorCode(), ex.getMessage());
    }

    @ExceptionHandler(ResourceNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ApiResponse<Void> handleResourceNotFoundException(ResourceNotFoundException ex) {
        log.warn("Resource not found: {}", ex.getMessage());
        return ApiResponse.error(ErrorCode.BIZ_RESOURCE_NOT_FOUND, ex.getMessage());
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ApiResponse<Void> handleGenericException(Exception ex) {
        log.error("Unexpected error occurred", ex);
        return ApiResponse.error(ErrorCode.SYS_INTERNAL_ERROR, "系統異常,請稍後再試");
    }
}

自定義異常類別

@Getter
public class BusinessException extends RuntimeException {
    private final ErrorCode errorCode;
    private final Object[] args;

    public BusinessException(ErrorCode errorCode, Object... args) {
        super(errorCode.getMessage());
        this.errorCode = errorCode;
        this.args = args;
    }
}

@Getter
public class ValidationException extends RuntimeException {
    private final ErrorCode errorCode;
    private final List<FieldError> fieldErrors;

    public ValidationException(ErrorCode errorCode, List<FieldError> fieldErrors) {
        super(errorCode.getMessage());
        this.errorCode = errorCode;
        this.fieldErrors = fieldErrors;
    }
}

API 回應封裝

@Data
@Builder
public class ApiResponse<T> {
    private boolean success;
    private String code;
    private String message;
    private T data;
    private List<FieldError> errors;
    private String timestamp;
    private String requestId;

    public static <T> ApiResponse<T> success(T data) {
        return ApiResponse.<T>builder()
                .success(true)
                .code("200")
                .message("操作成功")
                .data(data)
                .timestamp(Instant.now().toString())
                .requestId(MDC.get("requestId"))
                .build();
    }

    public static <T> ApiResponse<T> error(ErrorCode errorCode, String message) {
        return ApiResponse.<T>builder()
                .success(false)
                .code(errorCode.getCode())
                .message(message)
                .timestamp(Instant.now().toString())
                .requestId(MDC.get("requestId"))
                .build();
    }
}

---

## 4. 資料庫存取與 ORM 規範

### 4.1 JPA/QueryDSL 使用規範

#### JPA 實體設計規範

**基本實體設計**
```java
@Entity
@Table(name = "users", 
       indexes = {
           @Index(name = "idx_users_email", columnList = "email"),
           @Index(name = "idx_users_status", columnList = "status")
       })
@EntityListeners(AuditingEntityListener.class)
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;
    
    @Column(name = "username", length = 50, nullable = false, unique = true)
    @NotBlank(message = "使用者名稱不可為空")
    @Size(min = 3, max = 50, message = "使用者名稱長度須在 3-50 字元間")
    private String username;
    
    @Column(name = "email", length = 100, nullable = false, unique = true)
    @Email(message = "電子郵件格式不正確")
    @NotBlank(message = "電子郵件不可為空")
    private String email;
    
    @Column(name = "password", length = 255, nullable = false)
    @JsonIgnore
    private String password;
    
    @Enumerated(EnumType.STRING)
    @Column(name = "status", length = 20, nullable = false)
    private UserStatus status;
    
    @CreatedDate
    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;
    
    @LastModifiedDate
    @Column(name = "updated_at", nullable = false)
    private LocalDateTime updatedAt;
    
    @Version
    @Column(name = "version", nullable = false)
    private Long version;
}

關聯關係設計

// 一對多關係
@Entity
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;
    
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<OrderItem> orderItems = new ArrayList<>();
}

// 多對多關係
@Entity
public class User {
    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(
        name = "user_roles",
        joinColumns = @JoinColumn(name = "user_id"),
        inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private Set<Role> roles = new HashSet<>();
}

Repository 設計規範

基本 Repository

@Repository
public interface UserRepository extends JpaRepository<User, Long>, QuerydslPredicateExecutor<User> {
    
    // 基本查詢方法
    Optional<User> findByEmail(String email);
    Optional<User> findByUsername(String username);
    List<User> findByStatus(UserStatus status);
    
    // 複合查詢
    @Query("SELECT u FROM User u WHERE u.status = :status AND u.createdAt >= :fromDate")
    List<User> findActiveUsersAfterDate(@Param("status") UserStatus status, 
                                       @Param("fromDate") LocalDateTime fromDate);
    
    // 原生 SQL 查詢
    @Query(value = "SELECT COUNT(*) FROM users WHERE status = ?1", nativeQuery = true)
    Long countByStatusNative(String status);
    
    // 更新操作
    @Modifying
    @Query("UPDATE User u SET u.status = :status WHERE u.id = :id")
    int updateUserStatus(@Param("id") Long id, @Param("status") UserStatus status);
    
    // 分頁查詢
    Page<User> findByStatusAndEmailContaining(UserStatus status, String email, Pageable pageable);
}

QueryDSL 查詢實作

@Repository
@RequiredArgsConstructor
public class UserRepositoryCustomImpl implements UserRepositoryCustom {
    
    private final JPAQueryFactory queryFactory;
    
    @Override
    public List<User> findUsersWithComplexConditions(UserSearchCriteria criteria) {
        QUser user = QUser.user;
        
        BooleanBuilder builder = new BooleanBuilder();
        
        if (StringUtils.hasText(criteria.getUsername())) {
            builder.and(user.username.containsIgnoreCase(criteria.getUsername()));
        }
        
        if (StringUtils.hasText(criteria.getEmail())) {
            builder.and(user.email.containsIgnoreCase(criteria.getEmail()));
        }
        
        if (criteria.getStatus() != null) {
            builder.and(user.status.eq(criteria.getStatus()));
        }
        
        if (criteria.getFromDate() != null) {
            builder.and(user.createdAt.goe(criteria.getFromDate()));
        }
        
        if (criteria.getToDate() != null) {
            builder.and(user.createdAt.loe(criteria.getToDate()));
        }
        
        return queryFactory
                .selectFrom(user)
                .where(builder)
                .orderBy(user.createdAt.desc())
                .limit(criteria.getLimit())
                .fetch();
    }
    
    @Override
    public Page<UserDto> findUsersWithJoin(UserSearchCriteria criteria, Pageable pageable) {
        QUser user = QUser.user;
        QUserProfile profile = QUserProfile.userProfile;
        
        List<UserDto> content = queryFactory
                .select(Projections.constructor(UserDto.class,
                        user.id,
                        user.username,
                        user.email,
                        profile.firstName,
                        profile.lastName))
                .from(user)
                .leftJoin(user.profile, profile)
                .where(buildPredicate(criteria))
                .orderBy(user.createdAt.desc())
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();
        
        Long total = queryFactory
                .select(user.count())
                .from(user)
                .leftJoin(user.profile, profile)
                .where(buildPredicate(criteria))
                .fetchOne();
        
        return new PageImpl<>(content, pageable, total != null ? total : 0L);
    }
}

4.2 SQL 最佳化原則

查詢最佳化策略

索引使用規範

-- 單欄索引
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_status ON users(status);

-- 複合索引(順序很重要)
CREATE INDEX idx_users_status_created_at ON users(status, created_at);
CREATE INDEX idx_orders_user_status_date ON orders(user_id, status, order_date);

-- 唯一索引
CREATE UNIQUE INDEX uk_users_username ON users(username);

-- 部分索引(PostgreSQL)
CREATE INDEX idx_active_users ON users(email) WHERE status = 'ACTIVE';

-- 函式索引
CREATE INDEX idx_users_lower_email ON users(LOWER(email));

查詢最佳化規則

// 1. 避免 SELECT *
@Query("SELECT u.id, u.username, u.email FROM User u WHERE u.status = :status")
List<UserBasicDto> findUserBasicInfo(@Param("status") UserStatus status);

// 2. 使用適當的 JOIN
@Query("SELECT u FROM User u JOIN FETCH u.profile WHERE u.status = :status")
List<User> findUsersWithProfile(@Param("status") UserStatus status);

// 3. 分頁查詢
@Query(value = "SELECT u FROM User u WHERE u.status = :status",
       countQuery = "SELECT COUNT(u) FROM User u WHERE u.status = :status")
Page<User> findByStatusPaged(@Param("status") UserStatus status, Pageable pageable);

// 4. 批次查詢
@Query("SELECT u FROM User u WHERE u.id IN :ids")
List<User> findByIdIn(@Param("ids") List<Long> ids);

N+1 問題解決

// 問題:N+1 查詢
@Entity
public class Order {
    @ManyToOne(fetch = FetchType.LAZY) // 預設是 LAZY
    private User user;
}

// 解決方案 1:JOIN FETCH
@Query("SELECT o FROM Order o JOIN FETCH o.user WHERE o.status = :status")
List<Order> findOrdersWithUser(@Param("status") OrderStatus status);

// 解決方案 2:@EntityGraph
@EntityGraph(attributePaths = {"user", "orderItems"})
@Query("SELECT o FROM Order o WHERE o.status = :status")
List<Order> findOrdersWithUserAndItems(@Param("status") OrderStatus status);

// 解決方案 3:分批查詢
@BatchSize(size = 10)
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> orderItems;

資料庫連線最佳化

連線池配置

spring:
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      pool-name: MainPool
      minimum-idle: 5
      maximum-pool-size: 20
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
      leak-detection-threshold: 60000
      data-source-properties:
        cachePrepStmts: true
        prepStmtCacheSize: 250
        prepStmtCacheSqlLimit: 2048
        useServerPrepStmts: true
        useLocalSessionState: true
        rewriteBatchedStatements: true
        cacheResultSetMetadata: true
        cacheServerConfiguration: true
        elideSetAutoCommits: true
        maintainTimeStats: false

4.3 資料庫交易管理策略

交易註解使用

基本交易配置

@Service
@Transactional(readOnly = true) // 類別層級預設為唯讀
@RequiredArgsConstructor
public class UserService {
    
    private final UserRepository userRepository;
    private final EmailService emailService;
    
    // 查詢方法使用唯讀交易
    public Optional<User> findById(Long id) {
        return userRepository.findById(id);
    }
    
    // 寫入方法覆寫為可寫交易
    @Transactional(readOnly = false, rollbackFor = Exception.class)
    public User createUser(CreateUserRequestDto request) {
        User user = User.builder()
                .username(request.getUsername())
                .email(request.getEmail())
                .password(passwordEncoder.encode(request.getPassword()))
                .status(UserStatus.ACTIVE)
                .build();
        
        User savedUser = userRepository.save(user);
        
        // 發送歡迎郵件(如果失敗不應回滾交易)
        try {
            emailService.sendWelcomeEmail(savedUser.getEmail());
        } catch (Exception e) {
            log.warn("Failed to send welcome email for user: {}", savedUser.getId(), e);
        }
        
        return savedUser;
    }
    
    // 複雜交易處理
    @Transactional(
        rollbackFor = {BusinessException.class, DataAccessException.class},
        noRollbackFor = {EmailSendException.class},
        timeout = 30,
        isolation = Isolation.READ_COMMITTED
    )
    public void processOrderPayment(Long orderId, PaymentRequestDto paymentRequest) {
        Order order = orderRepository.findById(orderId)
                .orElseThrow(() -> new BusinessException(ErrorCode.ORDER_NOT_FOUND));
        
        // 檢查庫存
        validateInventory(order);
        
        // 處理付款
        PaymentResult result = paymentService.processPayment(paymentRequest);
        
        // 更新訂單狀態
        order.updateStatus(OrderStatus.PAID);
        order.setPaymentId(result.getPaymentId());
        orderRepository.save(order);
        
        // 更新庫存
        inventoryService.decreaseInventory(order.getOrderItems());
    }
}

程式化交易管理

TransactionTemplate 使用

@Service
@RequiredArgsConstructor
public class BatchProcessingService {
    
    private final TransactionTemplate transactionTemplate;
    private final UserRepository userRepository;
    
    public void processBatchUsers(List<CreateUserRequestDto> requests) {
        for (CreateUserRequestDto request : requests) {
            transactionTemplate.executeWithoutResult(status -> {
                try {
                    createUser(request);
                } catch (Exception e) {
                    log.error("Failed to create user: {}", request.getUsername(), e);
                    status.setRollbackOnly();
                }
            });
        }
    }
    
    public <T> T executeInNewTransaction(Supplier<T> operation) {
        TransactionTemplate newTransactionTemplate = new TransactionTemplate(transactionManager);
        newTransactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        return newTransactionTemplate.execute(status -> operation.get());
    }
}

分散式交易處理

多資料庫交易配置

@Configuration
@EnableTransactionManagement
public class DatabaseConfig {
    
    @Primary
    @Bean("primaryDataSource")
    @ConfigurationProperties("spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Bean("secondaryDataSource")
    @ConfigurationProperties("spring.datasource.secondary")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Primary
    @Bean("primaryTransactionManager")
    public PlatformTransactionManager primaryTransactionManager(
            @Qualifier("primaryDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
    
    @Bean("secondaryTransactionManager")
    public PlatformTransactionManager secondaryTransactionManager(
            @Qualifier("secondaryDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

// 使用特定交易管理器
@Service
public class MultiDatabaseService {
    
    @Transactional("primaryTransactionManager")
    public void operateOnPrimaryDatabase() {
        // 操作主資料庫
    }
    
    @Transactional("secondaryTransactionManager")
    public void operateOnSecondaryDatabase() {
        // 操作次資料庫
    }
}

5. 安全性規範(符合 SSDLC)

5.1 身分驗證與授權

JWT Token 實作

JWT 配置與產生

@Component
@ConfigurationProperties(prefix = "app.jwt")
@Data
public class JwtConfig {
    private String secret;
    private int expirationMs = 86400000; // 24 hours
    private int refreshExpirationMs = 604800000; // 7 days
    private String issuer = "company-platform";
}

@Service
@RequiredArgsConstructor
@Slf4j
public class JwtTokenService {
    
    private final JwtConfig jwtConfig;
    private Key signingKey;
    
    @PostConstruct
    public void init() {
        byte[] keyBytes = Decoders.BASE64.decode(jwtConfig.getSecret());
        this.signingKey = Keys.hmacShaKeyFor(keyBytes);
    }
    
    public String generateAccessToken(UserPrincipal userPrincipal) {
        Date expiryDate = new Date(System.currentTimeMillis() + jwtConfig.getExpirationMs());
        
        return Jwts.builder()
                .setSubject(userPrincipal.getId().toString())
                .setIssuer(jwtConfig.getIssuer())
                .setIssuedAt(new Date())
                .setExpiration(expiryDate)
                .claim("username", userPrincipal.getUsername())
                .claim("authorities", userPrincipal.getAuthorities().stream()
                        .map(GrantedAuthority::getAuthority)
                        .collect(Collectors.toList()))
                .signWith(signingKey, SignatureAlgorithm.HS512)
                .compact();
    }
    
    public String generateRefreshToken(UserPrincipal userPrincipal) {
        Date expiryDate = new Date(System.currentTimeMillis() + jwtConfig.getRefreshExpirationMs());
        
        return Jwts.builder()
                .setSubject(userPrincipal.getId().toString())
                .setIssuer(jwtConfig.getIssuer())
                .setIssuedAt(new Date())
                .setExpiration(expiryDate)
                .claim("type", "refresh")
                .signWith(signingKey, SignatureAlgorithm.HS512)
                .compact();
    }
    
    public Claims validateToken(String token) {
        try {
            return Jwts.parserBuilder()
                    .setSigningKey(signingKey)
                    .requireIssuer(jwtConfig.getIssuer())
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException e) {
            log.warn("JWT token is expired: {}", e.getMessage());
            throw new AuthenticationException("Token 已過期");
        } catch (UnsupportedJwtException e) {
            log.warn("JWT token is unsupported: {}", e.getMessage());
            throw new AuthenticationException("不支援的 Token 格式");
        } catch (MalformedJwtException e) {
            log.warn("JWT token is malformed: {}", e.getMessage());
            throw new AuthenticationException("Token 格式錯誤");
        } catch (SignatureException e) {
            log.warn("JWT signature is invalid: {}", e.getMessage());
            throw new AuthenticationException("Token 簽章無效");
        } catch (IllegalArgumentException e) {
            log.warn("JWT token compact is invalid: {}", e.getMessage());
            throw new AuthenticationException("Token 內容無效");
        }
    }
}

繼續添加其餘安全性內容、效能與擴展性指引、測試與品質保證、部署與維運指引等章節…


6. 效能與擴展性指引

6.1 Cache 機制(Redis)

Redis 配置與使用

Redis 連線配置

spring:
  redis:
    host: localhost
    port: 6379
    password: ${REDIS_PASSWORD:}
    timeout: 2000ms
    database: 0
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 5
        max-wait: 2000ms
      shutdown-timeout: 200ms

Redis 快取配置

@Configuration
@EnableCaching
@RequiredArgsConstructor
public class CacheConfig {
    
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30))
                .serializeKeysWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new GenericJackson2JsonRedisSerializer()));
        
        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(config)
                .transactionAware()
                .build();
    }
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
}

6.2 非同步與批次處理

非同步處理配置

線程池配置

@Configuration
@EnableAsync
public class AsyncConfig {
    
    @Bean("taskExecutor")
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(100);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("Async-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

7. 測試與品質保證

7.1 單元測試(JUnit 5)與整合測試策略

測試配置

測試基礎配置

@SpringBootTest
@ActiveProfiles("test")
@Transactional
@Rollback
class UserServiceTest {
    
    @Autowired
    private UserService userService;
    
    @MockBean
    private UserRepository userRepository;
    
    @Test
    @DisplayName("使用者建立 - 成功案例")
    void createUser_Success() {
        // Given
        CreateUserRequestDto request = CreateUserRequestDto.builder()
                .username("testuser")
                .email("test@example.com")
                .password("Test123456!")
                .build();
        
        User mockUser = User.builder()
                .id(1L)
                .username("testuser")
                .email("test@example.com")
                .status(UserStatus.ACTIVE)
                .build();
        
        when(userRepository.save(any(User.class))).thenReturn(mockUser);
        
        // When
        UserResponseDto result = userService.createUser(request);
        
        // Then
        assertThat(result).isNotNull();
        assertThat(result.getId()).isEqualTo(1L);
        assertThat(result.getUsername()).isEqualTo("testuser");
        
        verify(userRepository).save(any(User.class));
    }
}

8. 部署與維運指引

8.1 CI/CD 流程

GitHub Actions 配置

.github/workflows/ci.yml

name: CI/CD Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

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
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'corretto'
    
    - name: Cache Maven dependencies
      uses: actions/cache@v3
      with:
        path: ~/.m2
        key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
    
    - name: Run tests
      run: mvn clean test
    
    - name: Run SonarQube analysis
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
      run: mvn sonar:sonar
    
    - name: Build JAR
      run: mvn clean package -DskipTests
    
    - name: Build Docker image
      run: docker build -t app:${{ github.sha }} .

8.2 容器化規範

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 clean package -DskipTests

FROM openjdk:17-jre-slim

RUN addgroup --system --gid 1001 appgroup && \
    adduser --system --uid 1001 --gid 1001 appuser

WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
RUN chown appuser:appgroup app.jar

USER appuser

EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:8080/actuator/health || exit 1

ENTRYPOINT ["java", "-jar", "app.jar"]

8.3 監控與警報設定

Actuator 配置

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: when-authorized
  metrics:
    export:
      prometheus:
        enabled: true

9. 日誌管理與監控

9.1 日誌框架配置

Logback 配置

logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    
    <!-- 控制台輸出 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%logger{36}] - %msg%n</pattern>
        </encoder>
    </appender>
    
    <!-- 檔案輸出 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/application.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logs/application.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
            <maxFileSize>100MB</maxFileSize>
            <maxHistory>30</maxHistory>
            <totalSizeCap>3GB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%logger{36}] [%X{requestId}] - %msg%n</pattern>
        </encoder>
    </appender>
    
    <!-- 錯誤日誌單獨輸出 -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/error.log</file>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logs/error.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
            <maxHistory>90</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%logger{36}] [%X{requestId}] - %msg%n</pattern>
        </encoder>
    </appender>
    
    <!-- 結構化日誌輸出 (JSON) -->
    <appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/application.json</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logs/application.%d{yyyy-MM-dd}.json.gz</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <providers>
                <timestamp/>
                <logLevel/>
                <loggerName/>
                <message/>
                <mdc/>
                <stackTrace/>
            </providers>
        </encoder>
    </appender>
    
    <!-- 開發環境配置 -->
    <springProfile name="dev,local">
        <root level="DEBUG">
            <appender-ref ref="CONSOLE"/>
            <appender-ref ref="FILE"/>
        </root>
    </springProfile>
    
    <!-- 生產環境配置 -->
    <springProfile name="prod">
        <root level="INFO">
            <appender-ref ref="FILE"/>
            <appender-ref ref="ERROR_FILE"/>
            <appender-ref ref="JSON_FILE"/>
        </root>
    </springProfile>
    
    <!-- 特定 Logger 配置 -->
    <logger name="com.company.platform" level="DEBUG" additivity="false">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE"/>
    </logger>
    
    <logger name="org.springframework.security" level="DEBUG" additivity="false">
        <appender-ref ref="CONSOLE"/>
    </logger>
    
    <logger name="org.hibernate.SQL" level="DEBUG" additivity="false">
        <appender-ref ref="CONSOLE"/>
    </logger>
</configuration>

日誌使用規範

日誌級別使用指引

@Slf4j
@Service
public class UserService {
    
    public User createUser(CreateUserRequestDto request) {
        log.info("Creating user with username: {}", request.getUsername());
        
        try {
            // 記錄重要的業務操作
            log.debug("Validating user data: {}", request);
            
            User user = userRepository.save(buildUser(request));
            
            log.info("User created successfully with ID: {}", user.getId());
            
            // 記錄業務指標
            log.info("USER_CREATED username={} userId={} timestamp={}", 
                    user.getUsername(), user.getId(), Instant.now());
            
            return user;
            
        } catch (DataIntegrityViolationException e) {
            log.warn("Duplicate user creation attempt: {}", request.getUsername(), e);
            throw new BusinessException(ErrorCode.DUPLICATE_USERNAME);
        } catch (Exception e) {
            log.error("Failed to create user: {}", request.getUsername(), e);
            throw new SystemException(ErrorCode.USER_CREATION_FAILED, e);
        }
    }
}

結構化日誌記錄

@Component
@Slf4j
public class AuditLogger {
    
    public void logUserAction(String action, Long userId, String details) {
        MDC.put("action", action);
        MDC.put("userId", String.valueOf(userId));
        MDC.put("timestamp", Instant.now().toString());
        
        log.info("User action performed: {}", details);
        
        MDC.clear();
    }
    
    public void logApiCall(String method, String endpoint, String responseStatus, long duration) {
        MDC.put("httpMethod", method);
        MDC.put("endpoint", endpoint);
        MDC.put("responseStatus", responseStatus);
        MDC.put("duration", String.valueOf(duration));
        
        log.info("API call completed");
        
        MDC.clear();
    }
}

9.2 應用程式監控

Micrometer + Prometheus 配置

監控指標配置

@Configuration
public class MetricsConfig {
    
    @Bean
    public TimedAspect timedAspect(MeterRegistry registry) {
        return new TimedAspect(registry);
    }
    
    @Bean
    public CountedAspect countedAspect(MeterRegistry registry) {
        return new CountedAspect(registry);
    }
    
    @Bean
    public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
        return registry -> registry.config()
                .commonTags("application", "backend-service")
                .commonTags("environment", getEnvironment());
    }
}

自定義監控指標

@Service
@RequiredArgsConstructor
public class MetricsService {
    
    private final MeterRegistry meterRegistry;
    private final Counter userCreationCounter;
    private final Timer userCreationTimer;
    
    @PostConstruct
    public void init() {
        this.userCreationCounter = Counter.builder("user.creation.count")
                .description("Total user creation attempts")
                .tag("status", "success")
                .register(meterRegistry);
                
        this.userCreationTimer = Timer.builder("user.creation.duration")
                .description("User creation duration")
                .register(meterRegistry);
    }
    
    @Timed(value = "user.service.create", description = "Time taken to create user")
    @Counted(value = "user.service.create.attempts", description = "User creation attempts")
    public User createUser(CreateUserRequestDto request) {
        return Timer.Sample.start(meterRegistry)
                .stop(userCreationTimer.time(() -> {
                    User user = doCreateUser(request);
                    userCreationCounter.increment(Tags.of("status", "success"));
                    return user;
                }));
    }
}

9.3 分散式追蹤

Spring Cloud Sleuth 配置

追蹤配置

spring:
  sleuth:
    sampler:
      probability: 1.0  # 開發環境 100% 採樣,生產環境建議 0.1
    zipkin:
      base-url: http://zipkin:9411
    web:
      skip-pattern: "/actuator.*|/health.*"

自定義 Span

@Service
@RequiredArgsConstructor
public class PaymentService {
    
    private final Tracer tracer;
    
    public PaymentResult processPayment(PaymentRequest request) {
        Span span = tracer.nextSpan()
                .name("payment-processing")
                .tag("payment.amount", request.getAmount().toString())
                .tag("payment.method", request.getMethod())
                .start();
        
        try (Tracer.SpanInScope ws = tracer.withSpanInScope(span)) {
            return doProcessPayment(request);
        } finally {
            span.end();
        }
    }
}

10. 資料驗證與清理

10.1 輸入驗證規範

Bean Validation 使用

DTO 驗證註解

@Data
@Builder
public class CreateUserRequestDto {
    
    @NotBlank(message = "使用者名稱不可為空")
    @Size(min = 3, max = 50, message = "使用者名稱長度須在 3-50 字元間")
    @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "使用者名稱只能包含字母、數字和底線")
    private String username;
    
    @NotBlank(message = "電子郵件不可為空")
    @Email(message = "電子郵件格式不正確")
    @Size(max = 100, message = "電子郵件長度不可超過 100 字元")
    private String email;
    
    @NotBlank(message = "密碼不可為空")
    @Size(min = 8, max = 100, message = "密碼長度須在 8-100 字元間")
    @Pattern(
        regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]+$",
        message = "密碼必須包含大小寫字母、數字和特殊字符"
    )
    private String password;
    
    @NotBlank(message = "姓名不可為空")
    @Size(max = 50, message = "姓名長度不可超過 50 字元")
    private String firstName;
    
    @Size(max = 50, message = "姓氏長度不可超過 50 字元")
    private String lastName;
    
    @Pattern(regexp = "^\\+?[1-9]\\d{1,14}$", message = "手機號碼格式不正確")
    private String phoneNumber;
    
    @Valid
    @NotNull(message = "地址資訊不可為空")
    private AddressDto address;
}

自定義驗證器

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueEmailValidator.class)
public @interface UniqueEmail {
    String message() default "電子郵件已存在";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

@Component
@RequiredArgsConstructor
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
    
    private final UserRepository userRepository;
    
    @Override
    public boolean isValid(String email, ConstraintValidatorContext context) {
        if (email == null) {
            return true; // 由 @NotBlank 處理
        }
        return !userRepository.existsByEmail(email);
    }
}

群組驗證

驗證群組定義

public interface ValidationGroups {
    interface Create {}
    interface Update {}
    interface Delete {}
}

@Data
public class UserDto {
    
    @Null(groups = Create.class, message = "建立時 ID 必須為空")
    @NotNull(groups = Update.class, message = "更新時 ID 不可為空")
    private Long id;
    
    @NotBlank(groups = {Create.class, Update.class}, message = "使用者名稱不可為空")
    @UniqueUsername(groups = Create.class)
    private String username;
    
    @NotBlank(groups = Create.class, message = "密碼不可為空")
    @Null(groups = Update.class, message = "更新時不可修改密碼")
    private String password;
}

10.2 資料清理與消毒

XSS 防護

HTML 清理工具

@Component
public class HtmlSanitizer {
    
    private final PolicyFactory policy;
    
    public HtmlSanitizer() {
        this.policy = Sanitizers.FORMATTING
                .and(Sanitizers.LINKS)
                .and(Sanitizers.BLOCKS)
                .and(Sanitizers.TABLES);
    }
    
    public String sanitize(String html) {
        if (html == null) {
            return null;
        }
        return policy.sanitize(html);
    }
    
    public String stripHtml(String html) {
        if (html == null) {
            return null;
        }
        return Jsoup.clean(html, Whitelist.none());
    }
}

JSON 字符清理

@Component
public class JsonSanitizer {
    
    public String sanitizeJsonString(String input) {
        if (input == null) {
            return null;
        }
        
        return input
                .replace("\\", "\\\\")
                .replace("\"", "\\\"")
                .replace("\b", "\\b")
                .replace("\f", "\\f")
                .replace("\n", "\\n")
                .replace("\r", "\\r")
                .replace("\t", "\\t");
    }
}

SQL 注入防護

參數化查詢

@Repository
public class UserRepositoryImpl {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    // 正確:使用參數化查詢
    public List<User> findByEmailDomain(String domain) {
        String jpql = "SELECT u FROM User u WHERE u.email LIKE :pattern";
        return entityManager.createQuery(jpql, User.class)
                .setParameter("pattern", "%" + domain)
                .getResultList();
    }
    
    // 錯誤:字串拼接 (容易 SQL 注入)
    // public List<User> findByEmailDomainUnsafe(String domain) {
    //     String jpql = "SELECT u FROM User u WHERE u.email LIKE '%" + domain + "'";
    //     return entityManager.createQuery(jpql, User.class).getResultList();
    // }
}

11. 國際化與本地化

11.1 訊息國際化

國際化配置

MessageSource 配置

@Configuration
public class InternationalizationConfig {
    
    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = 
                new ReloadableResourceBundleMessageSource();
        messageSource.setBasename("classpath:messages/messages");
        messageSource.setDefaultEncoding("UTF-8");
        messageSource.setUseCodeAsDefaultMessage(true);
        messageSource.setCacheSeconds(3600);
        return messageSource;
    }
    
    @Bean
    public LocaleResolver localeResolver() {
        AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
        localeResolver.setDefaultLocale(Locale.TRADITIONAL_CHINESE);
        localeResolver.setSupportedLocales(Arrays.asList(
                Locale.TRADITIONAL_CHINESE,
                Locale.SIMPLIFIED_CHINESE,
                Locale.ENGLISH,
                Locale.JAPANESE
        ));
        return localeResolver;
    }
}

訊息資源檔案

messages_zh_TW.properties

# 驗證訊息
validation.user.username.required=使用者名稱為必填欄位
validation.user.email.invalid=電子郵件格式不正確
validation.user.password.weak=密碼強度不足

# 業務訊息
business.user.not.found=找不到指定的使用者
business.user.already.exists=使用者已存在
business.order.insufficient.stock=庫存不足

# 系統訊息
system.internal.error=系統內部錯誤,請稍後再試
system.database.connection.failed=資料庫連線失敗

messages_en_US.properties

# Validation messages
validation.user.username.required=Username is required
validation.user.email.invalid=Invalid email format
validation.user.password.weak=Password is too weak

# Business messages
business.user.not.found=User not found
business.user.already.exists=User already exists
business.order.insufficient.stock=Insufficient stock

# System messages
system.internal.error=Internal server error, please try again later
system.database.connection.failed=Database connection failed

國際化服務

MessageService

@Service
@RequiredArgsConstructor
public class MessageService {
    
    private final MessageSource messageSource;
    
    public String getMessage(String code) {
        return getMessage(code, null);
    }
    
    public String getMessage(String code, Object[] args) {
        Locale locale = LocaleContextHolder.getLocale();
        return messageSource.getMessage(code, args, code, locale);
    }
    
    public String getMessage(String code, Object[] args, Locale locale) {
        return messageSource.getMessage(code, args, code, locale);
    }
}

11.2 多時區處理

時區配置

時區轉換工具

@Component
public class TimeZoneUtil {
    
    private static final String DEFAULT_TIMEZONE = "Asia/Taipei";
    
    public ZonedDateTime convertToUserTimezone(LocalDateTime localDateTime, String userTimezone) {
        ZoneId defaultZone = ZoneId.of(DEFAULT_TIMEZONE);
        ZoneId userZone = ZoneId.of(userTimezone);
        
        return localDateTime.atZone(defaultZone).withZoneSameInstant(userZone);
    }
    
    public LocalDateTime convertToSystemTime(ZonedDateTime userDateTime) {
        ZoneId systemZone = ZoneId.of(DEFAULT_TIMEZONE);
        return userDateTime.withZoneSameInstant(systemZone).toLocalDateTime();
    }
    
    public String formatDateTime(LocalDateTime dateTime, String pattern, Locale locale) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern, locale);
        return dateTime.format(formatter);
    }
}

12. 文件生成與 API 規範

12.1 OpenAPI/Swagger 配置

Swagger 配置

OpenAPI 配置

@Configuration
@OpenAPIDefinition(
    info = @Info(
        title = "後端服務 API",
        version = "v1.0",
        description = "後端服務的 RESTful API 文件",
        contact = @Contact(
            name = "開發團隊",
            email = "dev-team@company.com",
            url = "https://company.com/dev-team"
        ),
        license = @License(
            name = "MIT License",
            url = "https://opensource.org/licenses/MIT"
        )
    ),
    servers = {
        @Server(url = "http://localhost:8080", description = "開發環境"),
        @Server(url = "https://api-staging.company.com", description = "測試環境"),
        @Server(url = "https://api.company.com", description = "生產環境")
    }
)
@SecuritySchemes({
    @SecurityScheme(
        name = "bearerAuth",
        type = SecuritySchemeType.HTTP,
        scheme = "bearer",
        bearerFormat = "JWT"
    )
})
public class OpenApiConfig {
    
    @Bean
    public GroupedOpenApi publicApi() {
        return GroupedOpenApi.builder()
                .group("public")
                .pathsToMatch("/api/v1/public/**")
                .build();
    }
    
    @Bean
    public GroupedOpenApi privateApi() {
        return GroupedOpenApi.builder()
                .group("private")
                .pathsToMatch("/api/v1/users/**", "/api/v1/orders/**")
                .build();
    }
}

API 文件註解

Controller 文件化

@RestController
@RequestMapping("/api/v1/users")
@Tag(name = "使用者管理", description = "使用者相關操作的 API")
@SecurityRequirement(name = "bearerAuth")
@RequiredArgsConstructor
public class UserController {
    
    private final UserService userService;
    
    @Operation(
        summary = "建立新使用者",
        description = "建立一個新的使用者帳號",
        responses = {
            @ApiResponse(
                responseCode = "201",
                description = "使用者建立成功",
                content = @Content(
                    mediaType = "application/json",
                    schema = @Schema(implementation = ApiResponse.class),
                    examples = @ExampleObject(
                        name = "成功回應",
                        value = """
                        {
                          "success": true,
                          "code": "201",
                          "message": "使用者建立成功",
                          "data": {
                            "id": 1,
                            "username": "john_doe",
                            "email": "john@example.com",
                            "status": "ACTIVE"
                          }
                        }
                        """
                    )
                )
            ),
            @ApiResponse(
                responseCode = "400",
                description = "請求參數錯誤",
                content = @Content(
                    mediaType = "application/json",
                    schema = @Schema(implementation = ApiResponse.class)
                )
            ),
            @ApiResponse(
                responseCode = "409",
                description = "使用者已存在",
                content = @Content(
                    mediaType = "application/json",
                    schema = @Schema(implementation = ApiResponse.class)
                )
            )
        }
    )
    @PostMapping
    public ResponseEntity<ApiResponse<UserResponseDto>> createUser(
            @Valid @RequestBody @io.swagger.v3.oas.annotations.parameters.RequestBody(
                description = "使用者建立請求",
                required = true,
                content = @Content(
                    schema = @Schema(implementation = CreateUserRequestDto.class),
                    examples = @ExampleObject(
                        name = "建立使用者範例",
                        value = """
                        {
                          "username": "john_doe",
                          "email": "john@example.com",
                          "password": "SecurePass123!",
                          "firstName": "John",
                          "lastName": "Doe"
                        }
                        """
                    )
                )
            ) CreateUserRequestDto request) {
        
        UserResponseDto user = userService.createUser(request);
        return ResponseEntity.status(HttpStatus.CREATED)
                .body(ApiResponse.success(user));
    }
    
    @Operation(
        summary = "查詢使用者列表",
        description = "分頁查詢使用者列表,支援多種篩選條件"
    )
    @GetMapping
    public ResponseEntity<ApiResponse<Page<UserResponseDto>>> getUsers(
            @Parameter(description = "使用者狀態", schema = @Schema(implementation = UserStatus.class))
            @RequestParam(required = false) UserStatus status,
            
            @Parameter(description = "電子郵件關鍵字")
            @RequestParam(required = false) String email,
            
            @Parameter(description = "頁碼", example = "0")
            @RequestParam(defaultValue = "0") int page,
            
            @Parameter(description = "每頁大小", example = "20")
            @RequestParam(defaultValue = "20") int size,
            
            @Parameter(description = "排序欄位", example = "createdAt,desc")
            @RequestParam(defaultValue = "createdAt,desc") String sort) {
        
        Pageable pageable = PageRequest.of(page, size, Sort.by(sort.split(",")));
        Page<UserResponseDto> users = userService.findUsers(status, email, pageable);
        return ResponseEntity.ok(ApiResponse.success(users));
    }
}

12.2 API 文件最佳實踐

文件內容規範

DTO Schema 文件化

@Schema(description = "使用者回應資料")
@Data
@Builder
public class UserResponseDto {
    
    @Schema(description = "使用者 ID", example = "1")
    private Long id;
    
    @Schema(description = "使用者名稱", example = "john_doe")
    private String username;
    
    @Schema(description = "電子郵件", example = "john@example.com")
    private String email;
    
    @Schema(description = "使用者狀態", implementation = UserStatus.class)
    private UserStatus status;
    
    @Schema(description = "建立時間", example = "2024-01-01T10:00:00Z")
    private LocalDateTime createdAt;
    
    @Schema(description = "更新時間", example = "2024-01-01T10:00:00Z")
    private LocalDateTime updatedAt;
}

13. 第三方整合規範

13.1 外部 API 呼叫

RestTemplate/WebClient 配置

HTTP 客戶端配置

@Configuration
public class HttpClientConfig {
    
    @Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        
        // 設置連線超時
        HttpComponentsClientHttpRequestFactory factory = 
                new HttpComponentsClientHttpRequestFactory();
        factory.setConnectTimeout(5000);
        factory.setReadTimeout(10000);
        
        restTemplate.setRequestFactory(factory);
        
        // 添加攔截器
        restTemplate.getInterceptors().add(new LoggingClientHttpRequestInterceptor());
        
        return restTemplate;
    }
    
    @Bean
    public WebClient webClient() {
        return WebClient.builder()
                .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(1024 * 1024))
                .filter(ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
                    log.info("Request: {} {}", clientRequest.method(), clientRequest.url());
                    return Mono.just(clientRequest);
                }))
                .filter(ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
                    log.info("Response status: {}", clientResponse.statusCode());
                    return Mono.just(clientResponse);
                }))
                .build();
    }
}

外部服務客戶端

@Service
@RequiredArgsConstructor
@Slf4j
public class ExternalPaymentService {
    
    private final RestTemplate restTemplate;
    private final PaymentConfig paymentConfig;
    
    @Retryable(
        value = {ResourceAccessException.class, HttpServerErrorException.class},
        maxAttempts = 3,
        backoff = @Backoff(delay = 1000, multiplier = 2)
    )
    @CircuitBreaker(name = "payment-service", fallbackMethod = "fallbackPayment")
    public PaymentResponse processPayment(PaymentRequest request) {
        try {
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            headers.setBearerAuth(paymentConfig.getApiKey());
            
            HttpEntity<PaymentRequest> entity = new HttpEntity<>(request, headers);
            
            ResponseEntity<PaymentResponse> response = restTemplate.postForEntity(
                    paymentConfig.getBaseUrl() + "/payments",
                    entity,
                    PaymentResponse.class
            );
            
            log.info("Payment processed successfully: {}", response.getBody().getTransactionId());
            return response.getBody();
            
        } catch (HttpClientErrorException e) {
            log.error("Payment service client error: {}", e.getResponseBodyAsString(), e);
            throw new PaymentException("付款服務回應錯誤", e);
        } catch (HttpServerErrorException e) {
            log.error("Payment service server error: {}", e.getResponseBodyAsString(), e);
            throw new PaymentException("付款服務暫時無法使用", e);
        } catch (ResourceAccessException e) {
            log.error("Payment service connection error", e);
            throw new PaymentException("無法連接付款服務", e);
        }
    }
    
    public PaymentResponse fallbackPayment(PaymentRequest request, Exception ex) {
        log.warn("Payment service fallback triggered for request: {}", request.getOrderId(), ex);
        
        // 回傳預設回應或使用備用服務
        return PaymentResponse.builder()
                .status(PaymentStatus.PENDING)
                .message("付款處理中,請稍後查詢結果")
                .build();
    }
}

13.2 訊息佇列整合

RabbitMQ 配置

RabbitMQ 配置

@Configuration
@EnableRabbit
public class RabbitConfig {
    
    public static final String USER_EXCHANGE = "user.exchange";
    public static final String USER_CREATED_QUEUE = "user.created.queue";
    public static final String USER_CREATED_ROUTING_KEY = "user.created";
    
    @Bean
    public TopicExchange userExchange() {
        return new TopicExchange(USER_EXCHANGE);
    }
    
    @Bean
    public Queue userCreatedQueue() {
        return QueueBuilder.durable(USER_CREATED_QUEUE).build();
    }
    
    @Bean
    public Binding userCreatedBinding() {
        return BindingBuilder
                .bind(userCreatedQueue())
                .to(userExchange())
                .with(USER_CREATED_ROUTING_KEY);
    }
    
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate template = new RabbitTemplate(connectionFactory);
        template.setMessageConverter(new Jackson2JsonMessageConverter());
        return template;
    }
}

訊息發布者

@Service
@RequiredArgsConstructor
@Slf4j
public class UserEventPublisher {
    
    private final RabbitTemplate rabbitTemplate;
    
    public void publishUserCreated(UserCreatedEvent event) {
        try {
            rabbitTemplate.convertAndSend(
                    RabbitConfig.USER_EXCHANGE,
                    RabbitConfig.USER_CREATED_ROUTING_KEY,
                    event
            );
            log.info("Published user created event: {}", event.getUserId());
        } catch (Exception e) {
            log.error("Failed to publish user created event: {}", event.getUserId(), e);
            throw new MessagePublishException("無法發布使用者建立事件", e);
        }
    }
}

訊息消費者

@Service
@RabbitListener(queues = RabbitConfig.USER_CREATED_QUEUE)
@Slf4j
public class UserEventConsumer {
    
    @RabbitHandler
    public void handleUserCreated(UserCreatedEvent event) {
        log.info("Received user created event: {}", event.getUserId());
        
        try {
            // 處理使用者建立事件
            processUserCreated(event);
            log.info("User created event processed successfully: {}", event.getUserId());
        } catch (Exception e) {
            log.error("Failed to process user created event: {}", event.getUserId(), e);
            throw new MessageProcessingException("處理使用者建立事件失敗", e);
        }
    }
    
    private void processUserCreated(UserCreatedEvent event) {
        // 發送歡迎郵件
        // 初始化使用者設定
        // 記錄審計日誌
    }
}

14. 程式碼審查與品質控制

14.1 程式碼審查流程

Pull Request 規範

PR 模板

## 變更描述
簡要描述這次變更的內容和目的

## 變更類型
- [ ] Bug 修復
- [ ] 新功能
- [ ] 重構
- [ ] 文件更新
- [ ] 效能改善
- [ ] 測試新增

## 影響範圍
- [ ] 前端
- [ ] 後端
- [ ] 資料庫
- [ ] 第三方服務

## 測試清單
- [ ] 單元測試通過
- [ ] 整合測試通過
- [ ] 手動測試完成
- [ ] 效能測試通過(如適用)

## 檢查清單
- [ ] 程式碼符合編碼規範
- [ ] 已添加必要的測試
- [ ] 已更新相關文件
- [ ] 已考慮向後相容性
- [ ] 已檢查安全性問題

## 相關 Issue
關聯的 Issue 編號:#

## 螢幕截圖(如適用)
貼上相關的螢幕截圖

## 其他說明
其他需要說明的內容

程式碼審查檢查點

審查檢查清單

## 架構與設計
- [ ] 是否遵循 Clean Architecture 原則
- [ ] 模組間依賴關係是否合理
- [ ] 是否適當使用設計模式
- [ ] 介面設計是否清晰

## 程式碼品質
- [ ] 命名是否有意義且一致
- [ ] 函數和類別大小是否適中
- [ ] 是否有重複程式碼
- [ ] 異常處理是否適當

## 效能考量
- [ ] 是否存在效能瓶頸
- [ ] 資料庫查詢是否最佳化
- [ ] 快取使用是否合理
- [ ] 記憶體使用是否高效

## 安全性
- [ ] 輸入驗證是否充分
- [ ] 是否存在 SQL 注入風險
- [ ] 敏感資料是否正確處理
- [ ] 權限檢查是否完整

## 測試覆蓋
- [ ] 測試覆蓋率是否足夠
- [ ] 邊界情況是否被測試
- [ ] 異常情況是否被測試
- [ ] 整合測試是否完整

14.2 靜態程式碼分析

SonarQube 配置

SonarQube 規則配置

# sonar-project.properties
sonar.projectKey=backend-service
sonar.projectName=Backend Service
sonar.projectVersion=1.0
sonar.sources=src/main/java
sonar.tests=src/test/java
sonar.java.binaries=target/classes
sonar.java.test.binaries=target/test-classes
sonar.junit.reportPaths=target/surefire-reports
sonar.jacoco.reportPaths=target/jacoco.exec
sonar.coverage.exclusions=**/*Config.java,**/*Application.java,**/*Dto.java

CheckStyle 配置

checkstyle.xml

<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
    "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
    "https://checkstyle.org/dtds/configuration_1_3.dtd">

<module name="Checker">
    <property name="charset" value="UTF-8"/>
    <property name="severity" value="error"/>
    <property name="fileExtensions" value="java"/>
    
    <!-- 檔案大小檢查 -->
    <module name="FileLength">
        <property name="max" value="500"/>
    </module>
    
    <!-- 行長度檢查 -->
    <module name="LineLength">
        <property name="max" value="120"/>
        <property name="ignorePattern" value="^package.*|^import.*|a href|href|http://|https://|ftp://"/>
    </module>
    
    <module name="TreeWalker">
        <!-- 命名規範 -->
        <module name="ConstantName"/>
        <module name="LocalFinalVariableName"/>
        <module name="LocalVariableName"/>
        <module name="MemberName"/>
        <module name="MethodName"/>
        <module name="PackageName"/>
        <module name="ParameterName"/>
        <module name="StaticVariableName"/>
        <module name="TypeName"/>
        
        <!-- 程式碼結構 -->
        <module name="MethodLength">
            <property name="max" value="50"/>
        </module>
        <module name="ParameterNumber">
            <property name="max" value="7"/>
        </module>
        
        <!-- 空白檢查 -->
        <module name="GenericWhitespace"/>
        <module name="MethodParamPad"/>
        <module name="NoWhitespaceAfter"/>
        <module name="NoWhitespaceBefore"/>
        <module name="ParenPad"/>
        <module name="TypecastParenPad"/>
        <module name="WhitespaceAfter"/>
        <module name="WhitespaceAround"/>
    </module>
</module>

15. 依賴與配置管理

15.1 Maven 依賴管理

依賴版本管理

pom.xml 最佳實踐

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>
    
    <groupId>com.company.platform</groupId>
    <artifactId>backend-service</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>
    
    <properties>
        <java.version>17</java.version>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        
        <!-- 依賴版本統一管理 -->
        <querydsl.version>5.0.0</querydsl.version>
        <mapstruct.version>1.5.3.Final</mapstruct.version>
        <testcontainers.version>1.19.3</testcontainers.version>
        <springdoc.version>2.2.0</springdoc.version>
    </properties>
    
    <dependencies>
        <!-- Spring Boot Starters -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        
        <!-- Database -->
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!-- QueryDSL -->
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-jpa</artifactId>
            <version>${querydsl.version}</version>
            <classifier>jakarta</classifier>
        </dependency>
        
        <!-- API Documentation -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>${springdoc.version}</version>
        </dependency>
        
        <!-- Testing -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>${testcontainers.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            
            <!-- 程式碼品質檢查 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-checkstyle-plugin</artifactId>
                <version>3.3.0</version>
                <configuration>
                    <configLocation>checkstyle.xml</configLocation>
                    <encoding>UTF-8</encoding>
                    <consoleOutput>true</consoleOutput>
                    <failsOnError>true</failsOnError>
                </configuration>
                <executions>
                    <execution>
                        <id>validate</id>
                        <phase>validate</phase>
                        <goals>
                            <goal>check</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            
            <!-- 測試覆蓋率 -->
            <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>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

15.2 配置檔案管理

環境配置分離

application.yml

spring:
  application:
    name: backend-service
  profiles:
    active: ${SPRING_PROFILES_ACTIVE:dev}
  
  # 共用配置
  datasource:
    driver-class-name: org.postgresql.Driver
    hikari:
      pool-name: MainPool
      minimum-idle: 5
      maximum-pool-size: 20
      connection-timeout: 30000
  
  jpa:
    hibernate:
      ddl-auto: validate
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect
        format_sql: true
    show-sql: false

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics
  endpoint:
    health:
      show-details: when-authorized

logging:
  level:
    com.company.platform: INFO
    org.springframework.security: WARN
    org.hibernate.SQL: WARN

---
# 開發環境配置
spring:
  config:
    activate:
      on-profile: dev
  
  datasource:
    url: jdbc:postgresql://localhost:5432/backend_dev
    username: ${DB_USERNAME:dev_user}
    password: ${DB_PASSWORD:dev_pass}
  
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: update
  
  cache:
    type: simple

logging:
  level:
    com.company.platform: DEBUG
    org.hibernate.SQL: DEBUG

---
# 測試環境配置
spring:
  config:
    activate:
      on-profile: test
  
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: sa
    password: 
  
  jpa:
    hibernate:
      ddl-auto: create-drop
    database-platform: org.hibernate.dialect.H2Dialect

---
# 生產環境配置
spring:
  config:
    activate:
      on-profile: prod
  
  datasource:
    url: ${DATABASE_URL}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
    hikari:
      maximum-pool-size: 50
  
  cache:
    type: redis
    redis:
      host: ${REDIS_HOST}
      port: ${REDIS_PORT:6379}
      password: ${REDIS_PASSWORD}

logging:
  level:
    com.company.platform: INFO
    org.springframework.web: WARN

16. 備份與災難恢復

16.1 資料備份策略

資料庫備份

自動備份腳本

#!/bin/bash

# 資料庫備份腳本
DB_HOST=${DB_HOST:-localhost}
DB_PORT=${DB_PORT:-5432}
DB_NAME=${DB_NAME:-backend_db}
DB_USER=${DB_USER:-postgres}
BACKUP_DIR="/backup/database"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/${DB_NAME}_$DATE.sql.gz"

# 建立備份目錄
mkdir -p $BACKUP_DIR

# 執行備份
pg_dump -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME | gzip > $BACKUP_FILE

# 檢查備份是否成功
if [ $? -eq 0 ]; then
    echo "Backup successful: $BACKUP_FILE"
    
    # 清理 7 天前的備份
    find $BACKUP_DIR -name "*.sql.gz" -mtime +7 -delete
    
    # 上傳到雲端儲存 (AWS S3)
    aws s3 cp $BACKUP_FILE s3://company-backups/database/
    
else
    echo "Backup failed"
    exit 1
fi

應用程式資料備份

檔案備份配置

# Docker Compose 備份配置
version: '3.8'
services:
  app:
    image: backend-service:latest
    volumes:
      - app-logs:/app/logs
      - app-data:/app/data
  
  backup:
    image: alpine:latest
    volumes:
      - app-logs:/backup/logs:ro
      - app-data:/backup/data:ro
    command: |
      sh -c "
        apk add --no-cache aws-cli
        while true; do
          tar -czf /tmp/app-backup-$(date +%Y%m%d_%H%M%S).tar.gz /backup
          aws s3 cp /tmp/app-backup-*.tar.gz s3://company-backups/application/
          rm /tmp/app-backup-*.tar.gz
          sleep 86400  # 24 hours
        done
      "

volumes:
  app-logs:
  app-data:

16.2 災難恢復計畫

恢復程序

資料庫恢復腳本

#!/bin/bash

# 資料庫恢復腳本
BACKUP_FILE=$1
DB_HOST=${DB_HOST:-localhost}
DB_PORT=${DB_PORT:-5432}
DB_NAME=${DB_NAME:-backend_db}
DB_USER=${DB_USER:-postgres}

if [ -z "$BACKUP_FILE" ]; then
    echo "Usage: $0 <backup_file>"
    exit 1
fi

# 停止應用程式
docker-compose stop app

# 備份當前資料庫
pg_dump -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME > /tmp/pre_restore_backup.sql

# 刪除並重建資料庫
dropdb -h $DB_HOST -p $DB_PORT -U $DB_USER $DB_NAME
createdb -h $DB_HOST -p $DB_PORT -U $DB_USER $DB_NAME

# 恢復資料
if [[ $BACKUP_FILE == *.gz ]]; then
    gunzip -c $BACKUP_FILE | psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME
else
    psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME < $BACKUP_FILE
fi

# 檢查恢復是否成功
if [ $? -eq 0 ]; then
    echo "Database restored successfully"
    # 重啟應用程式
    docker-compose start app
else
    echo "Database restore failed"
    # 恢復原始資料庫
    psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME < /tmp/pre_restore_backup.sql
    exit 1
fi

災難恢復測試

恢復測試檢查清單

## 災難恢復測試清單

### 每月測試項目
- [ ] 資料庫備份完整性驗證
- [ ] 備份檔案可讀性測試
- [ ] 部分資料恢復測試
- [ ] 應用程式配置備份驗證

### 每季測試項目
- [ ] 完整災難恢復演練
- [ ] 跨區域備份恢復測試
- [ ] 恢復時間目標 (RTO) 驗證
- [ ] 恢復點目標 (RPO) 驗證

### 測試報告範本

測試日期: 測試類型: 測試環境: 測試結果: 恢復時間: 發現問題: 改善建議:


---

## 總結

這份後端開發指引現在已經涵蓋了完整的軟體開發生命週期,包含:

### 新增的重要章節:

1. **日誌管理與監控** - Logback 配置、結構化日誌、監控指標
2. **資料驗證與清理** - Bean Validation、XSS 防護、SQL 注入防護
3. **國際化與本地化** - 多語言支援、時區處理
4. **文件生成與 API 規範** - OpenAPI/Swagger 配置和最佳實踐
5. **第三方整合規範** - HTTP 客戶端、訊息佇列整合
6. **程式碼審查與品質控制** - PR 流程、靜態程式碼分析
7. **依賴與配置管理** - Maven 最佳實踐、環境配置管理
8. **備份與災難恢復** - 資料備份策略、災難恢復計畫

### 指引的完整性:

本指引現在提供了:
- ✅ 完整的架構設計指引
- ✅ 詳細的編碼規範
- ✅ 安全性最佳實踐
- ✅ 效能最佳化策略
- ✅ 測試策略和品質保證
- ✅ 部署和維運指引
- ✅ 監控和日誌管理
- ✅ 災難恢復計畫

所有規範都符合現代軟體開發最佳實踐,特別是 **SSDLC(安全軟體開發生命週期)** 的要求,確保系統的安全性、可靠性、可維護性和可擴展性。

開發團隊可以將此指引作為後端開發的標準參考,並根據具體專案需求進行適當的調整和擴充。