Java 編碼標準 是做什麼的?
適用於 Spring Boot 和 Quarkus 服務中可讀、可維護的 Java (17+) 程式碼標準。
使用時機
- 在 Spring Boot 或 Quarkus 專案中撰寫或審查 Java 程式碼
- 強制執行命名、不可變性或例外處理慣例
- 使用 record、sealed class 或模式匹配 (Java 17+)
- 審查 Optional、串流或泛型的使用
- 建構套件和專案佈局
- [QUARKUS]:處理 CDI 範圍、Panache 實體或反應式管道
運作方式
框架偵測
在套用標準之前,從建置檔判定框架:
- 建置檔包含
quarkus→ 套用 [QUARKUS] 慣例 - 建置檔包含
spring-boot→ 套用 [SPRING] 慣例 - 兩者皆未偵測到 → 僅套用共享慣例
核心原則
- 清晰勝於花俏
- 預設不可變;最小化共享可變狀態
- 快速失敗,並提供有意義的例外
- 一致的命名和套件結構
- [QUARKUS]:傾向建置時期處理而非執行時期處理;盡可能避免執行時期反射
範例
以下小節展示具體的 Spring Boot、Quarkus 以及共享的 Java 範例, 包含命名、不可變性、依賴注入、反應式程式碼、例外處理、 專案佈局、日誌記錄、組態和測試。
命名
// PASS: 類別/Record: 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 資源命名為 *Resource,而非 *Controller
public class MarketResource {}
// PASS: [SPRING] REST 控制器命名為 *Controller
public class MarketController {}
不可變性
// PASS: 偏好使用 record 和 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 實體使用公共欄位 (Quarkus 慣例)
@Entity
public class Market extends PanacheEntity {
public String name;
public MarketStatus status;
// Panache 在建置時期產生存取子;此處的公共欄位是習慣用法
}
// PASS: [QUARKUS] Panache MongoDB 實體
@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 bean、Producer
resource/ # JAX-RS 資源 (而非 "controller")
service/
repository/ # PanacheRepository 實作 (如果不使用 active record)
domain/ # JPA/Panache 實體、MongoDB 實體
dto/
util/
mapper/ # MapStruct mapper (如果使用)
src/main/resources/
application.properties # Quarkus 慣例 (透過 quarkus-config-yaml 支援 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 注入,範圍限定於宣告類別
Null 處理
- 僅在無法避免時接受
@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,儲存庫切片使用@DataJpaTest @SpringBootTest保留給完整的整合測試@MockBean用於在 Spring 上下文中取代 bean
[QUARKUS]
- 單元測試使用純 JUnit 5 + Mockito (無
@QuarkusTest) @QuarkusTest保留給 CDI 整合測試@InjectMock用於在整合測試中取代 CDI bean- 使用 Dev Services 來處理資料庫/Kafka/Redis — 當 Dev Services 足夠時,避免手動設定 Testcontainers
@QuarkusTestResource用於自訂外部服務生命週期
// [SPRING] 控制器測試
@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;
}
記住:保持程式碼有意圖、有型別且可觀察。為了可維護性而最佳化,除非經證明有必要,否則不要為了微觀最佳化而最佳化。