Java コーディング規約
Spring Boot および Quarkus サービスにおける、読みやすく保守しやすい Java 17+ コードのための規約。
使用タイミング
- Spring Boot または Quarkus プロジェクトで Java コードを作成またはレビューする場合
- 命名、不変性、例外処理に関する規約を適用する場合
- レコード、sealed クラス、パターンマッチング(Java 17+)を扱う場合
- Optional、ストリーム、ジェネリクスの使用をレビューする場合
- パッケージとプロジェクト構成を整理する場合
- [QUARKUS]:CDI スコープ、Panache エンティティ、リアクティブパイプラインを扱う場合
仕組み
フレームワーク検出
規約を適用する前に、ビルドファイルからフレームワークを特定します。
- ビルドファイルに
quarkusが含まれる → [QUARKUS] の規約を適用 - ビルドファイルに
spring-bootが含まれる → [SPRING] の規約を適用 - どちらも検出されない → 共通規約のみを適用
基本原則
- 賢さよりも明確さを優先
- デフォルトで不変にし、共有可変状態を最小化
- 意味のある例外でフェイルファスト
- 一貫した命名とパッケージ構成
- [QUARKUS]:ビルド時処理をランタイム処理より優先し、可能な限りランタイムリフレクションを回避
例
以下のセクションでは、Spring Boot、Quarkus、および共通 Java の具体的な例を、命名、不変性、依存性注入、リアクティブコード、例外、プロジェクト構成、ロギング、設定、テストについて示します。
命名
// PASS: クラス/レコード: パスカルケース
public class MarketService {}
public record Money(BigDecimal amount, Currency currency) {}
// PASS: メソッド/フィールド: キャメルケース
private final MarketRepository marketRepository;
public Market findBySlug(String slug) {}
// PASS: 定数: 大文字のスネークケース
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: レコードと 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 アクティブレコードエンティティは 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: get() の代わりに map/flatMap を使用
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(); // BLOCKING — イベントループを破壊
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 実装(アクティブレコードを使用しない場合)
domain/ # JPA/Panache エンティティ、MongoDB エンティティ
dto/
util/
mapper/ # MapStruct マッパー(使用する場合)
src/main/resources/
application.properties # Quarkus の慣例(quarkus-config-yaml で YAML サポート)
import.sql # 開発/テスト用の Hibernate 自動インポート
src/test/java/... (main をミラーリング)
書式とスタイル
- インデントはスペース2または4で統一(プロジェクト標準)
- ファイルごとに1つのパブリックトップレベル型
- メソッドは短く焦点を絞り、ヘルパーに抽出
- メンバーの順序:定数、フィールド、コンストラクタ、パブリックメソッド、プロテクテッド、プライベート
避けるべきコードの悪臭
- 長いパラメータリスト → DTO/ビルダーを使用
- 深いネスト → 早期 return
- マジックナンバー → 名前付き定数
- 静的可変状態 → 依存性注入を優先
- サイレントキャッチブロック → ログを記録して対処、または再スロー
- [QUARKUS]:
@ApplicationScopedを意図する箇所での@Singleton— プロキシとインターセプションを破壊 - [QUARKUS]:
quarkus-resteasy-reactiveとquarkus-resteasy(クラシック)の混在 — 1つのスタックを選択 - [QUARKUS]:同じ境界コンテキスト内での Panache アクティブレコードとリポジトリパターンの混在 — 1つを選択
ロギング
// [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、可能な限り部分モックは避ける
- 決定論的なテストを優先、隠れたスリープは避ける
[SPRING]
- コントローラスライスには
@WebMvcTest、リポジトリスライスには@DataJpaTest @SpringBootTestは完全な統合テストに限定- Spring コンテキストの Bean 置き換えには
@MockBean
[QUARKUS]
- 単体テストには通常の JUnit 5 + Mockito(
@QuarkusTestは使わない) @QuarkusTestは CDI 統合テストに限定- 統合テストでの CDI Bean 置き換えには
@InjectMock - データベース/Kafka/Redis には Dev Services — 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;
}
覚えておいてください:コードは意図的で、型付けされ、観測可能に保ちましょう。マイクロ最適化よりも保守性を優先し、それが証明された場合のみ最適化してください。