java-coding-standards について
Spring Boot や Quarkus のサービスを開発・レビューするチーム向けの Java 17 以降のコーディング規約スキルです。ビルドファイルからフレームワークを判定し、共通規約と個別の指針を適用します。
主な機能
- 01Spring Boot と Quarkus の規約
- 02ビルドファイルによるフレームワーク判定
- 03命名とパッケージ構成の指針
- 04不変性と型安全性の実践
- 05テストと例外処理の指針
活用シーン
- 01Java サービスコードのレビュー
- 02Spring Boot プロジェクトの構成
- 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;
}
覚えておいてください:コードは意図が明確で、型付けされ、観測可能な状態に保ちます。必要性が実証されていない限り、マイクロ最適化より保守性を優先します。
コメント
Markdown 対応