Java コーディング規約 は何をしますか?
Spring Boot および Quarkus サービスにおける、読みやすく保守性の高い Java (17+) コードのための規約。
使用タイミング
- Spring Boot または Quarkus プロジェクトで Java コードを作成・レビューする場合
- 命名、不変性、例外処理の規約を適用する場合
- レコード、sealed クラス、パターンマッチング (Java 17+) を使用する場合
- Optional、ストリーム、ジェネリクスの使用方法をレビューする場合
- パッケージとプロジェクト構造を整理する場合
- [QUARKUS]: CDI スコープ、Panache エンティティ、リアクティブパイプラインを扱う場合
動作の仕組み
フレームワーク検出
規約を適用する前に、ビルドファイルからフレームワークを特定します。
- ビルドファイルに
quarkusが含まれる → [QUARKUS] の規約を適用 - ビルドファイルに
spring-bootが含まれる → [SPRING] の規約を適用 - いずれも検出されない → 共通の規約のみを適用
コア原則
- 技巧的であることよりも明確さを優先する
- デフォルトで不変(immutable)にし、共有可変状態を最小限にする
- 意味のある例外でフェイルファスト(fail fast)にする
- 一貫した命名とパッケージ構造
- [QUARKUS]: 実行時処理よりもビルド時処理を優先し、可能な限り実行時リフレクションを避ける
例
以下のセクションでは、Spring Boot、Quarkus、および共通の Java に関する具体的な例を示します。 命名、不変性、依存性注入、リアクティブコード、例外、プロジェクト構成、ロギング、設定、テストについて説明します。
命名
// PASS: クラス/レコード: 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: レコードと 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 への複数回の subscribe
Uni<Market> shared = fetchMarket(slug);
shared.subscribe().with(m -> log(m));
shared.subscribe().with(m -> cache(m)); // 二重 subscribe — 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));
}
ジェネリクスと型安全性
- ロータイプ(raw type)を避け、ジェネリックパラメータを宣言する
- 再利用可能なユーティリティには境界ワイルドカードを優先する
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 ファイルに 1 つの public トップレベル型
- メソッドは短く焦点を絞り、ヘルパーを抽出する
- メンバーの順序: 定数、フィールド、コンストラクタ、public メソッド、protected、private
回避すべきコードスメル
- 長いパラメータリスト → DTO/ビルダーを使用
- 深いネスト → 早期リターン
- マジックナンバー → 名前付き定数
- 静的で可変な状態 → 依存性注入を優先
- サイレントな catch ブロック → ログに記録して対処するか、再スロー
- [QUARKUS]:
@ApplicationScopedが意図されているところでの@Singleton— プロキシとインターセプションを壊す - [QUARKUS]:
quarkus-resteasy-reactiveとquarkus-resteasy(クラシック) の混在 — どちらか一方のスタックを選択 - [QUARKUS]: 同じ境界コンテキスト内での Panache アクティブレコードとリポジトリパターンの混在 — どちらか一方を選択
ロギング
// [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;
}
覚えておいてください: コードは意図的で、型付けされ、観測可能であることを心がけましょう。マイクロ最適化よりも保守性を優先し、それが証明された場合にのみ最適化を行ってください。