專案後端模式
本文檔說明花店管理系統的後端架構與設計模式。
目錄結構
app-server/src/main/java/io/leandev/app/
├── AppServer.java # 應用程式入口
├── config/ # 配置類別
│ ├── CacheConfig.java # 快取配置
│ └── TenantFilterAspect.java # 多租戶過濾切面
├── converter/ # JPA 轉換器
│ └── JsonConverters.java # JSON 欄位轉換
├── entity/ # Entity 層
│ ├── base/ # Entity 基類
│ │ ├── AuditableBase.java
│ │ └── AuditableTenantEntity.java
│ ├── tenant/ # 租戶相關
│ │ └── Tenant.java
│ ├── customer/ # 客戶模組
│ │ ├── Customer.java
│ │ └── CustomerStatus.java
│ └── sales/ # 銷售模組
│ ├── Product.java
│ └── ProductStatus.java
├── repository/ # Repository 層
│ ├── customer/
│ │ └── CustomerRepository.java
│ └── sales/
│ └── ProductRepository.java
├── service/ # Service 層
│ ├── cache/ # 快取服務
│ │ └── UserCacheService.java
│ ├── customer/
│ │ └── CustomerService.java
│ └── sales/
│ └── ProductService.java
├── controller/ # Controller 層
│ ├── customer/
│ │ └── CustomerController.java
│ └── sales/
│ └── ProductController.java
├── handler/ # 異常處理
│ └── GlobalRestExceptionHandler.java
└── security/ # 安全相關
└── Authority.java # 權限常數
分層架構
┌─────────────────────────────────────────────────────────────┐
│ Controller 層 │
│ - REST API 端點 │
│ - 請求/回應處理 │
│ - 權限控制 (@PreAuthorize) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Service 層 │
│ - 業務邏輯 │
│ - 交易管理 (@Transactional) │
│ - 跨 Repository 協調 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Repository 層 │
│ - 資料存取 │
│ - 使用 EntityManager + TupleQueryBuilder │
│ - 多租戶過濾(Hibernate Filter) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Entity 層 │
│ - 領域模型 │
│ - 業務方法 │
│ - 繼承 AuditableTenantEntity │
└─────────────────────────────────────────────────────────────┘
多租戶模式
TenantContext
所有請求都會設定租戶上下文:
// 在 Filter/Interceptor 中設定
TenantContext.setCurrentTenantId(tenantId);
// 在 Service 中取得
String tenantId = TenantContext.getCurrentTenantId();
租戶隔離
業務 Entity 繼承 AuditableTenantEntity,Hibernate Filter 自動過濾:
@Entity
@Getter
@Setter
public class Product extends AuditableTenantEntity implements Stateful<ProductStatus> {
// tenantId 由基類管理
// 查詢時由 TenantFilterAspect 自動啟用 Filter
}
TenantFilterAspect
@Aspect
@Component
public class TenantFilterAspect {
@PersistenceContext
private EntityManager entityManager;
@Before("@annotation(org.springframework.transaction.annotation.Transactional)")
public void enableTenantFilter() {
String tenantId = TenantContext.getCurrentTenantId();
if (tenantId != null) {
entityManager.unwrap(Session.class)
.enableFilter("tenantFilter")
.setParameter("tenantId", tenantId);
}
}
}
Entity 設計模式
Stateful 介面
有狀態的 Entity 實作 Stateful<T> 介面:
@Entity
@Getter
@Setter
public class Order extends AuditableTenantEntity implements Stateful<OrderStatus> {
@Enumerated(EnumType.STRING)
@Column(length = 20, nullable = false)
private OrderStatus status;
@Override
public OrderStatus getStatus() {
return status;
}
@Override
public void setStatus(OrderStatus status) {
this.status = status;
}
// Stateful 介面提供的輔助方法
public boolean isPending() {
return isInStatus(OrderStatus.PENDING);
}
}
UUID 主鍵
所有 Entity 使用 String UUID 作為主鍵:
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(length = 36)
private String id;
Repository 設計模式
EntityManager + TupleQueryBuilder
使用 EntityManager 直接實作,搭配框架的 TupleQueryBuilder:
@Repository
@RequiredArgsConstructor
public class ProductRepository {
private final EntityManager entityManager;
public Page<PropertyMap> findAll(Filter filter, Pageable pageable) {
TupleQueryBuilder<Product> builder = TupleQueryBuilder.<Product>of(entityManager)
.from(Product.class, "p")
.where(filter)
.select("id", "sku", "name", "categoryCode", "price", "status");
CriteriaQuery<Tuple> dataQuery = builder.build();
CriteriaQuery<Long> countQuery = builder.buildCountQuery();
QueryRunner<Tuple> queryRunner = new QueryRunner<>(entityManager);
Page<Tuple> tuples = queryRunner.findAll(dataQuery, countQuery, pageable);
TupleConstructor<PropertyMap> constructor = new TupleConstructor<>(PropertyMap.class);
return tuples.map(constructor::construct);
}
public Optional<Product> findById(String id) {
Product product = entityManager.find(Product.class, id);
// 手動驗證租戶(entityManager.find 不受 Filter 影響)
if (product != null && !product.getTenantId().equals(TenantContext.getCurrentTenantId())) {
return Optional.empty();
}
return Optional.ofNullable(product);
}
public Product save(Product product) {
if (product.getId() == null) {
entityManager.persist(product);
return product;
}
return entityManager.merge(product);
}
public void delete(Product product) {
entityManager.remove(entityManager.contains(product) ? product : entityManager.merge(product));
}
public long count(Filter filter) {
TupleQueryBuilder<Product> builder = TupleQueryBuilder.<Product>of(entityManager)
.from(Product.class, "p")
.where(filter);
return entityManager.createQuery(builder.buildCountQuery()).getSingleResult();
}
}
Service 設計模式
交易管理
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class ProductService {
private final ProductRepository productRepository;
private final ObjectMapper objectMapper;
// 讀取操作
@Transactional(readOnly = true)
public Optional<Product> findById(String id) {
return productRepository.findById(id);
}
@Transactional(readOnly = true)
public Page<PropertyMap> findAll(Filter filter, Pageable pageable) {
return productRepository.findAll(filter, pageable);
}
// 寫入操作(使用類別層級的 @Transactional)
public Product create(Product product) {
// 業務邏輯
return productRepository.save(product);
}
public Product update(String id, PropertyMap props) {
Product product = productRepository.findById(id)
.orElseThrow(() -> new NotFoundException("Product not found: ${0}", id));
// 使用 ObjectMapper 合併欄位
objectMapper.readerForUpdating(product)
.readValue(objectMapper.writeValueAsString(props));
return productRepository.save(product);
}
}
系統欄位保護
private static final List<String> SYSTEM_FIELDS = List.of("id", "tenantId", "sku");
public Product update(String id, PropertyMap props) {
// 移除系統欄位,防止用戶修改
SYSTEM_FIELDS.forEach(props::remove);
// ...
}
Controller 設計模式
REST 端點規範
@RestController
@RequestMapping("/api/v1/products")
@RequiredArgsConstructor
@Slf4j
public class ProductController {
private final ProductService productService;
private final ObjectMapper objectMapper;
private static final List<String> SYSTEM_FIELDS = List.of("id", "tenantId");
/// GET /api/v1/products - 列表查詢
@GetMapping
@PreAuthorize("hasAuthority('" + PRODUCT_R + "')")
public ResponseEntity<List<PropertyMap>> findAll(
@RequestParam(required = false) Filter filter,
@SortDefault(sort = "name", direction = Sort.Direction.DESC) Pageable pageable) {
Page<PropertyMap> data = productService.findAll(filter, pageable);
HttpHeaders headers = new HttpHeaders();
headers.set("X-Total-Count", String.valueOf(data.getTotalElements()));
return ResponseEntity.ok().headers(headers).body(data.getContent());
}
/// GET /api/v1/products/{id} - 單筆查詢
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('" + PRODUCT_R + "')")
public ResponseEntity<Product> getById(@PathVariable String id) {
Product product = productService.findById(id)
.orElseThrow(() -> new NotFoundException("Product not found: ${0}", id));
return ResponseEntity.ok(product);
}
/// POST /api/v1/products - 建立
@PostMapping
@PreAuthorize("hasAuthority('" + PRODUCT_W + "')")
public ResponseEntity<Product> create(@RequestBody PropertyMap props) {
SYSTEM_FIELDS.forEach(props::remove);
Product product = objectMapper.convertValue(props, Product.class);
Product created = productService.create(product);
log.info("[API] POST /products - Product {} created", created.getSku());
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
/// PATCH /api/v1/products/{id} - 部分更新
@PatchMapping("/{id}")
@PreAuthorize("hasAuthority('" + PRODUCT_W + "')")
public ResponseEntity<Product> update(
@PathVariable String id,
@RequestBody PropertyMap props) {
Product updated = productService.update(id, props);
log.info("[API] PATCH /products/{} - Updated", id);
return ResponseEntity.ok(updated);
}
/// DELETE /api/v1/products/{id} - 刪除
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('" + PRODUCT_D + "')")
public ResponseEntity<Void> delete(@PathVariable String id) {
productService.deleteWithCheck(id);
log.info("[API] DELETE /products/{}", id);
return ResponseEntity.noContent().build();
}
}
權限控制
使用 @PreAuthorize 進行權限控制,權限常數定義在 Authority 類別:
// security/Authority.java
public final class Authority {
public static final String PRODUCT_R = "PRODUCT_R"; // 讀取
public static final String PRODUCT_W = "PRODUCT_W"; // 寫入
public static final String PRODUCT_D = "PRODUCT_D"; // 刪除
public static final String PRODUCT_X = "PRODUCT_X"; // 執行(如狀態變更)
public static final String CUSTOMER_R = "CUSTOMER_R";
public static final String CUSTOMER_W = "CUSTOMER_W";
public static final String CUSTOMER_D = "CUSTOMER_D";
public static final String CUSTOMER_X = "CUSTOMER_X";
public static final String ORDER_R = "ORDER_R";
public static final String ORDER_W = "ORDER_W";
public static final String ORDER_D = "ORDER_D";
public static final String ORDER_X = "ORDER_X";
}
| 權限後綴 | 說明 | 用途 |
|---|---|---|
_R | Read | 讀取、列表查詢 |
_W | Write | 建立、更新 |
_D | Delete | 刪除 |
_X | Execute | 狀態變更、特殊操作 |
錯誤處理模式
框架異常
使用框架提供的異常類別:
// 找不到資源
throw new NotFoundException("Product not found: ${0}", id);
// 資料重複
throw new DuplicateException("Product name already exists: ${0}", name);
// 衝突(如刪除時有關聯資料)
throw new ConflictException("Cannot delete product with existing orders");
// 資料格式錯誤
throw new InvalidDataException("Invalid price value");
// 驗證錯誤
throw new ConstraintException(new Violation("status", "Status is required"));
全域異常處理
繼承 StandardRestExceptionHandler:
@ControllerAdvice
public class GlobalRestExceptionHandler extends StandardRestExceptionHandler {
static {
// 註冊自定義 Mapper(可選)
getRegistry().register(new CustomBusinessExceptionMapper());
}
}
快取模式
快取服務
使用框架的 Cache<K, V> API:
@Service
@RequiredArgsConstructor
public class UserCacheService {
private final Cache<Long, String> userCache;
// Cache-Aside Pattern
public Optional<String> getUser(Long userId, DatabaseLoader loader) {
String cached = userCache.get(userId);
if (cached != null) {
return Optional.of(cached);
}
Optional<String> fromDb = loader.load(userId);
fromDb.ifPresent(user -> userCache.put(userId, user));
return fromDb;
}
// Write-Through Pattern
public void updateUser(Long userId, String userData, DatabaseUpdater updater) {
updater.update(userId, userData);
userCache.put(userId, userData);
}
public void invalidate(Long userId) {
userCache.remove(userId);
}
public CacheStatistics getStatistics() {
return userCache.getStatistics();
}
}
快取配置
@Configuration
public class CacheConfig {
@Bean
public CacheManager cacheManager(@Value("${cache.path}") String cachePath) {
return CacheManagerBuilder.newCacheManager()
.withPersistence(Path.of(cachePath))
.build();
}
@Bean
public Cache<Long, String> userCache(CacheManager cacheManager) {
return CacheBuilder.<Long, String>newCache()
.withCacheManager(cacheManager)
.withName("userCache")
.withKeyType(Long.class)
.withValueType(String.class)
.withTtl(Duration.ofMinutes(30))
.withMaxSize(1000)
.build();
}
}
命名規範
| 類型 | 規範 | 範例 |
|---|---|---|
| Entity | PascalCase | Product、Customer |
| Repository | Entity + Repository | ProductRepository |
| Service | Entity + Service | ProductService |
| Controller | Entity + Controller | ProductController |
| 狀態枚舉 | Entity + Status | ProductStatus |
| 權限常數 | 模組_操作 | PRODUCT_R、PRODUCT_W |
| API 路徑 | /api/v1/{resources} | /api/v1/products |
下一步
- AppFuse 框架應用 - 框架核心功能詳解
- Entity 設計 - Entity 設計最佳實踐
- 資料庫遷移 - Flyway 遷移管理