後端開發指引
目錄
- 開發原則
- 專案結構與命名規範
- API 設計規範
- 資料庫存取與 ORM 規範
- 安全性規範
- 效能與擴展性指引
- 測試與品質保證
- 部署與維運指引
- 日誌管理與監控
- 資料驗證與清理
- 國際化與本地化
- 文件生成與 API 規範
- 第三方整合規範
- 程式碼審查與品質控制
- 依賴與配置管理
- 備份與災難恢復
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 #1232. 專案結構與命名規範
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-31HTTP 方法使用規範
| 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.0Header 版本化
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 狀態碼使用規範
| 狀態碼 | 名稱 | 使用場景 | 範例 |
|---|---|---|---|
| 200 | OK | 成功處理請求 | 查詢、更新成功 |
| 201 | Created | 成功建立資源 | 建立使用者成功 |
| 204 | No Content | 成功處理但無回應內容 | 刪除成功 |
| 400 | Bad Request | 請求參數錯誤 | 驗證失敗 |
| 401 | Unauthorized | 未認證 | Token 無效 |
| 403 | Forbidden | 已認證但無權限 | 存取被拒絕 |
| 404 | Not Found | 資源不存在 | 使用者不存在 |
| 409 | Conflict | 資源衝突 | 使用者名稱重複 |
| 422 | Unprocessable Entity | 語義錯誤 | 業務邏輯錯誤 |
| 429 | Too Many Requests | 請求頻率過高 | 超過 API 限制 |
| 500 | Internal 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: false4.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: 200msRedis 快取配置
@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: true9. 日誌管理與監控
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.javaCheckStyle 配置
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: WARN16. 備份與災難恢復
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(安全軟體開發生命週期)** 的要求,確保系統的安全性、可靠性、可維護性和可擴展性。
開發團隊可以將此指引作為後端開發的標準參考,並根據具體專案需求進行適當的調整和擴充。