Java 編碼標準
適用於 Spring Boot 與 Quarkus 服務中,可讀性高且易於維護的 Java (17+) 程式碼標準。
使用時機
- 在 Spring Boot 或 Quarkus 專案中撰寫或審查 Java 程式碼
- 強制執行命名、不變性或例外處理慣例
- 使用 records、sealed classes 或 pattern matching(Java 17+)
- 審查 Optional、串流或泛型的使用
- 組織套件與專案佈局
- [QUARKUS]:處理 CDI scope、Panache entities 或反應式管線
運作方式
框架偵測
在套用標準之前,從建構檔判斷所使用的框架:
- 建構檔包含
quarkus→ 套用 [QUARKUS] 慣例 - 建構檔包含
spring-boot→ 套用 [SPRING] 慣例 - 兩者皆未偵測到 → 僅套用共用慣例
核心原則
- 清晰重於巧妙
- 預設為不變性;盡量減少共享的可變狀態
- 快速失敗,並提供有意義的例外
- 一致的命名與套件結構
- [QUARKUS]:偏好建置時期處理而非執行時期處理;盡可能避免執行時期反射
範例
下列各節展示了 Spring Boot、Quarkus 及共用 Java 在命名、不變性、依賴注入、反應式程式碼、例外、專案佈局、日誌、配置與測試方面的具體範例。
命名
// PASS: 類別/Records: PascalCase
public class MarketService {}
public record Money(BigDecimal amount, Currency currency) {}
// PASS: 方法/欄位: camelCase
private final MarketRepository marketRepository;
public Market findBySlug(String slug) {}
// PASS: 常數: UPPER_SNAKE_CASE
private static final int MAX_PAGE_SIZE = 100;
// PASS: [QUARKUS] JAX-RS resources 命名為 *Resource,而非 *Controller
public class MarketResource {}
// PASS: [SPRING] REST controllers 命名為 *Controller
public class MarketController {}
不變性
// PASS: 偏好使用 records 和 final 欄位
public record MarketDto(Long id, String name, MarketStatus status) {}
public class Market {
private final Long id;
private final String name;
// 僅 getter,無 setter
}
// PASS: [QUARKUS] Panache active-record entities 使用 public 欄位(Quarkus 慣例)
@Entity
public class Market extends PanacheEntity {
public String name;
public MarketStatus status;
// Panache 在建置時期產生存取器;在此使用 public 欄位是慣用寫法
}
// PASS: [QUARKUS] Panache MongoDB entities
@MongoEntity(collection = "markets")
public class Market extends PanacheMongoEntity {
public String name;
public MarketStatus status;
}
Optional 的使用
// PASS: 從 find* 方法回傳 Optional
// [SPRING]
Optional<Market> market = marketRepository.findBySlug(slug);
// [QUARKUS] Panache
Optional<Market> market = Market.find("slug", slug).firstResultOptional();
// PASS: 使用 map/flatMap 而非 get()
return market
.map(MarketResponse::from)
.orElseThrow(() -> new EntityNotFoundException("Market not found"));
串流最佳實踐
// PASS: 使用串流進行轉換,保持管線簡短
List<String> names = markets.stream()
.map(Market::name)
.filter(Objects::nonNull)
.toList();
// FAIL: 避免複雜的巢狀串流;為求清晰應優先使用迴圈
依賴注入
// PASS: [SPRING] 建構子注入(優先於欄位上的 @Autowired)
@Service
public class MarketService {
private final MarketRepository marketRepository;
public MarketService(MarketRepository marketRepository) {
this.marketRepository = marketRepository;
}
}
// PASS: [QUARKUS] 建構子注入
@ApplicationScoped
public class MarketService {
private final MarketRepository marketRepository;
@Inject
public MarketService(MarketRepository marketRepository) {
this.marketRepository = marketRepository;
}
}
// PASS: [QUARKUS] 套件層級欄位注入(在 Quarkus 中可接受 — 避免代理問題)
@ApplicationScoped
public class MarketService {
@Inject
MarketRepository marketRepository;
}
// FAIL: [SPRING] 使用 @Autowired 的欄位注入
@Autowired
private MarketRepository marketRepository; // 請使用建構子注入
// FAIL: [QUARKUS] 在需要攔截或懶載入時使用 @Singleton
@Singleton // 不可代理 — 應改用 @ApplicationScoped
public class MarketService {}
反應式模式 [QUARKUS]
// PASS: 從反應式端點回傳 Uni/Multi
@GET
@Path("/{slug}")
public Uni<Market> findBySlug(@PathParam("slug") String slug) {
return Market.find("slug", slug)
.<Market>firstResult()
.onItem().ifNull().failWith(() -> new MarketNotFoundException(slug));
}
// PASS: 非阻塞管線組合
public Uni<OrderConfirmation> placeOrder(OrderRequest req) {
return validateOrder(req)
.chain(valid -> persistOrder(valid))
.chain(order -> notifyFulfillment(order));
}
// FAIL: 在 Uni/Multi 管線中進行阻塞呼叫
public Uni<Market> find(String slug) {
Market m = Market.find("slug", slug).firstResult(); // 阻塞 — 中斷事件迴圈
return Uni.createFrom().item(m);
}
// FAIL: 對同一個共享的 Uni 進行多次訂閱
Uni<Market> shared = fetchMarket(slug);
shared.subscribe().with(m -> log(m));
shared.subscribe().with(m -> cache(m)); // 重複訂閱 — 請使用 Uni.memoize()
例外
- 對於領域錯誤,使用非受檢例外;將技術性例外包裝並附上脈絡
- 建立領域特定的例外(例如
MarketNotFoundException) - 避免寬泛的
catch (Exception ex),除非是在集中處重新拋出/記錄
throw new MarketNotFoundException(slug);
集中式例外處理
// [SPRING]
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MarketNotFoundException.class)
public ResponseEntity<ErrorResponse> handle(MarketNotFoundException ex) {
return ResponseEntity.status(404).body(ErrorResponse.from(ex));
}
}
// [QUARKUS] 選項 A:ExceptionMapper
@Provider
public class MarketNotFoundMapper implements ExceptionMapper<MarketNotFoundException> {
@Override
public Response toResponse(MarketNotFoundException ex) {
return Response.status(404).entity(ErrorResponse.from(ex)).build();
}
}
// [QUARKUS] 選項 B:@ServerExceptionMapper(RESTEasy Reactive)
@ServerExceptionMapper
public RestResponse<ErrorResponse> handle(MarketNotFoundException ex) {
return RestResponse.status(Status.NOT_FOUND, ErrorResponse.from(ex));
}
泛型與型別安全
- 避免原始型別;宣告泛型參數
- 對於可重複使用的工具方法,偏好有邊界的泛型
public <T extends Identifiable> Map<Long, T> indexById(Collection<T> items) { ... }
專案結構
[SPRING] Maven/Gradle
src/main/java/com/example/app/
config/
controller/
service/
repository/
domain/
dto/
util/
src/main/resources/
application.yml
src/test/java/... (與 main 目錄對應)
[QUARKUS] Maven/Gradle
src/main/java/com/example/app/
config/ # @ConfigMapping、@ConfigProperty beans、Producers
resource/ # JAX-RS resources(非 "controller")
service/
repository/ # PanacheRepository 實作(若未使用 active record 模式)
domain/ # JPA/Panache entities、MongoDB entities
dto/
util/
mapper/ # MapStruct mappers(若使用)
src/main/resources/
application.properties # Quarkus 慣例(YAML 支援需加上 quarkus-config-yaml)
import.sql # 開發/測試用的 Hibernate 自動匯入
src/test/java/... (與 main 目錄對應)
格式化與樣式
- 一致使用 2 或 4 個空格縮排(專案標準)
- 每個檔案一個公開頂層型別
- 保持方法簡短且專注;提取輔助方法
- 成員順序:常數、欄位、建構子、公開方法、protected、private
應避免的程式碼異味
- 長參數列表 → 使用 DTO/builder
- 深層巢狀 → 使用提前回傳
- 魔術數字 → 使用命名常數
- 靜態可變狀態 → 偏好依賴注入
- 空的 catch 區塊 → 記錄日誌並採取行動或重新拋出
- [QUARKUS]:在應使用
@ApplicationScoped時使用@Singleton— 會破壞代理與攔截機制 - [QUARKUS]:混用
quarkus-resteasy-reactive與quarkus-resteasy(經典)— 請選擇其中一個 - [QUARKUS]:在同一個限界上下文中同時使用 Panache active-record 與 repository 模式 — 請選擇其中一個
日誌記錄
// [SPRING] SLF4J
private static final Logger log = LoggerFactory.getLogger(MarketService.class);
log.info("fetch_market slug={}", slug);
log.error("failed_fetch_market slug={}", slug, ex);
// [QUARKUS] JBoss Logging(預設,在建置時期零成本)
private static final Logger log = Logger.getLogger(MarketService.class);
log.infof("fetch_market slug=%s", slug);
log.errorf(ex, "failed_fetch_market slug=%s", slug);
// [QUARKUS] 替代方案:使用 @Inject 簡化日誌記錄
@Inject
Logger log; // CDI 注入,作用域為宣告的類別
空值處理
- 僅在無法避免時接受
@Nullable;否則使用@NonNull - 在輸入上使用 Bean Validation(
@NotNull、@NotBlank) - [QUARKUS]:在
@BeanParam、@RestForm及請求主體參數上套用@Valid
配置
// [SPRING] @ConfigurationProperties
@ConfigurationProperties(prefix = "market")
public record MarketProperties(int maxPageSize, Duration cacheTtl) {}
// [QUARKUS] @ConfigMapping(型別安全,建置時期驗證)
@ConfigMapping(prefix = "market")
public interface MarketConfig {
int maxPageSize();
Duration cacheTtl();
}
// [QUARKUS] 使用 @ConfigProperty 取得簡單值
@ConfigProperty(name = "market.max-page-size", defaultValue = "100")
int maxPageSize;
測試期望
共用
- JUnit 5 + AssertJ 用於流暢的斷言
- Mockito 用於模擬;盡可能避免部分模擬
- 偏好確定性測試;無隱藏的 sleep
[SPRING]
@WebMvcTest用於 Controller 切片,@DataJpaTest用於 Repository 切片@SpringBootTest保留給完整的整合測試@MockBean用於取代 Spring 上下文中的 bean
[QUARKUS]
- 單元測試使用純 JUnit 5 + Mockito(無需
@QuarkusTest) @QuarkusTest保留給 CDI 整合測試@InjectMock用於取代整合測試中的 CDI bean- 使用 Dev Services 處理資料庫/Kafka/Redis — 當 Dev Services 足夠時,避免手動的 Testcontainers 設定
@QuarkusTestResource用於自訂外部服務的生命週期
// [SPRING] Controller 測試
@WebMvcTest(MarketController.class)
class MarketControllerTest {
@Autowired MockMvc mockMvc;
@MockBean MarketService marketService;
}
// [QUARKUS] 整合測試
@QuarkusTest
class MarketResourceTest {
@InjectMock
MarketService marketService;
@Test
void should_return_404_when_market_not_found() {
given().when().get("/markets/unknown").then().statusCode(404);
}
}
// [QUARKUS] 單元測試(無 CDI、無 @QuarkusTest)
@ExtendWith(MockitoExtension.class)
class MarketServiceTest {
@Mock MarketRepository marketRepository;
@InjectMocks MarketService marketService;
}
切記:讓程式碼具有意圖、型別明確且可觀察。優化可維護性勝於微觀最佳化,除非已證明有必要。