mcpField
J
java-coding-standards

java-coding-standards

github.com/midudev開発7.0k56 回インストール1 週間前に更新
リポジトリ

java-coding-standards について

Spring Boot や Quarkus のサービスを開発・レビューするチーム向けの Java 17 以降のコーディング規約スキルです。ビルドファイルからフレームワークを判定し、共通規約と個別の指針を適用します。

主な機能

  1. 01Spring Boot と Quarkus の規約
  2. 02ビルドファイルによるフレームワーク判定
  3. 03命名とパッケージ構成の指針
  4. 04不変性と型安全性の実践
  5. 05テストと例外処理の指針

活用シーン

  1. 01Java サービスコードのレビュー
  2. 02Spring Boot プロジェクトの構成
  3. 03Quarkus 固有規約の適用

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/midudev/autoskills --skill java-coding-standards

インストール後、Claude Code で/java-coding-standardsを入力して読み込まれたか確認します。

詳細

Java コーディング規約

Spring Boot および Quarkus のサービスで読みやすく、保守しやすい Java(17 以降)コードを書くための規約です。

使用する場面

  • Spring Boot または Quarkus プロジェクトで Java コードを作成またはレビューするとき
  • 命名規則、不変性、例外処理の規約を徹底するとき
  • レコード、シールドクラス、パターンマッチング(Java 17 以降)を使用するとき
  • Optional、ストリーム、ジェネリクスの使い方をレビューするとき
  • パッケージとプロジェクトのレイアウトを構成するとき
  • [QUARKUS]:CDI スコープ、Panache エンティティ、リアクティブパイプラインを扱うとき

動作の仕組み

フレームワークの検出

規約を適用する前に、ビルドファイルからフレームワークを特定します。

  • ビルドファイルに quarkus が含まれる → [QUARKUS] の規約を適用します
  • ビルドファイルに spring-boot が含まれる → [SPRING] の規約を適用します
  • どちらも検出されない → 共通規約のみを適用します

基本原則

  • 奇抜さより明瞭さを優先します
  • デフォルトで不変にし、共有される可変状態を最小限に抑えます
  • 意味のある例外を使って、早期に失敗させます
  • 命名とパッケージ構成に一貫性を持たせます
  • [QUARKUS]:ランタイム処理よりビルド時処理を優先し、可能な限りランタイムのリフレクションを避けます

例

以下のセクションでは、命名、不変性、依存性注入、リアクティブコード、例外、プロジェクトのレイアウト、ロギング、構成、テストに関する、Spring Boot、Quarkus、および共通の Java の具体例を示します。

命名

// PASS: Classes/Records: PascalCase
public class MarketService {}
public record Money(BigDecimal amount, Currency currency) {}

// PASS: Methods/fields: camelCase
private final MarketRepository marketRepository;
public Market findBySlug(String slug) {}

// PASS: Constants: UPPER_SNAKE_CASE
private static final int MAX_PAGE_SIZE = 100;

// PASS: [QUARKUS] JAX-RS resources named as *Resource, not *Controller
public class MarketResource {}

// PASS: [SPRING] REST controllers named as *Controller
public class MarketController {}

不変性

// PASS: Favor records and final fields
public record MarketDto(Long id, String name, MarketStatus status) {}

public class Market {
  private final Long id;
  private final String name;
  // getters only, no setters
}

// PASS: [QUARKUS] Panache active-record entities use public fields (Quarkus convention)
@Entity
public class Market extends PanacheEntity {
  public String name;
  public MarketStatus status;
  // Panache generates accessors at build time; public fields are idiomatic here
}

// PASS: [QUARKUS] Panache MongoDB entities
@MongoEntity(collection = "markets")
public class Market extends PanacheMongoEntity {
  public String name;
  public MarketStatus status;
}

Optional の使用

// PASS: Return Optional from find* methods
// [SPRING]
Optional<Market> market = marketRepository.findBySlug(slug);

// [QUARKUS] Panache
Optional<Market> market = Market.find("slug", slug).firstResultOptional();

// PASS: Map/flatMap instead of get()
return market
    .map(MarketResponse::from)
    .orElseThrow(() -> new EntityNotFoundException("Market not found"));

ストリームのベストプラクティス

// PASS: Use streams for transformations, keep pipelines short
List<String> names = markets.stream()
    .map(Market::name)
    .filter(Objects::nonNull)
    .toList();

// FAIL: Avoid complex nested streams; prefer loops for clarity

依存性注入

// PASS: [SPRING] Constructor injection (preferred over @Autowired on fields)
@Service
public class MarketService {
  private final MarketRepository marketRepository;

  public MarketService(MarketRepository marketRepository) {
    this.marketRepository = marketRepository;
  }
}

// PASS: [QUARKUS] Constructor injection
@ApplicationScoped
public class MarketService {
  private final MarketRepository marketRepository;

  @Inject
  public MarketService(MarketRepository marketRepository) {
    this.marketRepository = marketRepository;
  }
}

// PASS: [QUARKUS] Package-private field injection (acceptable in Quarkus — avoids proxy issues)
@ApplicationScoped
public class MarketService {
  @Inject
  MarketRepository marketRepository;
}

// FAIL: [SPRING] Field injection with @Autowired
@Autowired
private MarketRepository marketRepository; // use constructor injection

// FAIL: [QUARKUS] @Singleton when interception or lazy init is needed
@Singleton // non-proxyable — use @ApplicationScoped instead
public class MarketService {}

リアクティブパターン [QUARKUS]

// PASS: Return Uni/Multi from reactive endpoints
@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: Non-blocking pipeline composition
public Uni<OrderConfirmation> placeOrder(OrderRequest req) {
  return validateOrder(req)
      .chain(valid -> persistOrder(valid))
      .chain(order -> notifyFulfillment(order));
}

// FAIL: Blocking call inside a Uni/Multi pipeline
public Uni<Market> find(String slug) {
  Market m = Market.find("slug", slug).firstResult(); // BLOCKING — breaks event loop
  return Uni.createFrom().item(m);
}

// FAIL: Subscribing more than once to a shared Uni
Uni<Market> shared = fetchMarket(slug);
shared.subscribe().with(m -> log(m));
shared.subscribe().with(m -> cache(m)); // double subscribe — use 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] Option 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] Option 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/... (mirrors main)
[QUARKUS] Maven/Gradle
src/main/java/com/example/app/
  config/              # @ConfigMapping, @ConfigProperty beans, Producers
  resource/            # JAX-RS resources (not "controller")
  service/
  repository/          # PanacheRepository implementations (if not using active record)
  domain/              # JPA/Panache entities, MongoDB entities
  dto/
  util/
  mapper/              # MapStruct mappers (if used)
src/main/resources/
  application.properties   # Quarkus convention (YAML supported with quarkus-config-yaml)
  import.sql               # Hibernate auto-import for dev/test
src/test/java/... (mirrors main)

書式とスタイル

  • 2 個または 4 個のスペースを一貫して使用します(プロジェクトの標準に従います)
  • 1 ファイルにつき、トップレベルの public 型を 1 つだけ定義します
  • メソッドは短く、目的を絞り、ヘルパーを切り出します
  • メンバーは定数、フィールド、コンストラクター、public メソッド、protected メソッド、private メソッドの順に並べます

避けるべきコードの臭い

  • 長いパラメーターリスト → DTO またはビルダーを使用します
  • 深いネスト → 早期リターンを使用します
  • マジックナンバー → 名前付き定数を使用します
  • static な可変状態 → 依存性注入を優先します
  • 何もしない 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 (default, zero-cost at build time)
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] Alternative: simplified logging with @Inject
@Inject
Logger log; // CDI-injected, scoped to declaring class

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 (type-safe, build-time validated)
@ConfigMapping(prefix = "market")
public interface MarketConfig {
  int maxPageSize();
  Duration cacheTtl();
}

// [QUARKUS] Simple values with @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] Controller test
@WebMvcTest(MarketController.class)
class MarketControllerTest {
  @Autowired MockMvc mockMvc;
  @MockBean MarketService marketService;
}

// [QUARKUS] Integration test
@QuarkusTest
class MarketResourceTest {
  @InjectMock
  MarketService marketService;

  @Test
  void should_return_404_when_market_not_found() {
    given().when().get("/markets/unknown").then().statusCode(404);
  }
}

// [QUARKUS] Unit test (no CDI, no @QuarkusTest)
@ExtendWith(MockitoExtension.class)
class MarketServiceTest {
  @Mock MarketRepository marketRepository;
  @InjectMocks MarketService marketService;
}

覚えておいてください:コードは意図が明確で、型付けされ、観測可能な状態に保ちます。必要性が実証されていない限り、マイクロ最適化より保守性を優先します。

コメント