SKILL.md
readonlyread-only
name
java-coding-standards
description
Java 編碼規範,適用於 Spring Boot 與 Quarkus 服務:命名、不可變性、Optional 使用、串流、例外處理、泛型、CDI、反應式模式及專案結構。自動套用框架特定慣例。
Java 編碼規範
適用於 Spring Boot 與 Quarkus 服務中可讀、可維護的 Java (17+) 程式碼規範。
使用時機
- 在 Spring Boot 或 Quarkus 專案中撰寫或審查 Java 程式碼
- 強制執行命名、不可變性或例外處理慣例
- 使用 record、sealed class 或 pattern matching (Java 17+)
- 審查 Optional、串流或泛型的使用
- 組織套件與專案結構
- [QUARKUS]:使用 CDI scope、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 實體使用 public 欄位(Quarkus 慣例)
@Entity
public class Market extends PanacheEntity {
public String name;
public MarketStatus status;
// Panache 在建置時期產生存取器;此處使用 public 欄位是慣用寫法
}
// 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用於 Repository 切片@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;
}
切記:保持程式碼有目的性、型別明確且可觀察。以可維護性為優先,而非微觀最佳化,除非經證明確有必要。






