跳至主要内容

專案後端模式

本文檔說明花店管理系統的後端架構與設計模式。

目錄結構

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";
}
權限後綴說明用途
_RRead讀取、列表查詢
_WWrite建立、更新
_DDelete刪除
_XExecute狀態變更、特殊操作

錯誤處理模式

框架異常

使用框架提供的異常類別:

// 找不到資源
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();
}
}

命名規範

類型規範範例
EntityPascalCaseProductCustomer
RepositoryEntity + RepositoryProductRepository
ServiceEntity + ServiceProductService
ControllerEntity + ControllerProductController
狀態枚舉Entity + StatusProductStatus
權限常數模組_操作PRODUCT_RPRODUCT_W
API 路徑/api/v1/{resources}/api/v1/products

下一步