Skip to main content

loom-java

Java language expertise for idiomatic, production-quality code.

소스 정보

저장소
cosmix/loom
최근 소스 활동
2026년 9월 24일 17:08
감지된 SKILL.md 언어
영어
스타
61
포크
4

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
loom-java
description
Java language expertise for idiomatic, production-quality code.
triggers
["java","jdk","jvm","javac","maven","mvn","pom.xml","surefire","gradle","gradlew","junit","jupiter","assertj","mockito","testcontainers","spring","spring boot","jpa","hibernate","jakarta","record","sealed","virtual threads","completablefuture","optional"]
# Java Language Expertise ## Overview Idiomatic, production-grade Java 21+ (25 is the current LTS): records, sealed hierarchies, pattern matching, streams, virtual threads, the Maven and Gradle builds, JUnit 5 and Spring Boot. Assumes the reader knows the language. The content is decision rules and the traps that survive review: `==` on boxed values, `Collectors.toMap` on a duplicate key, `@Transactional` on a self-call, a pinned virtual thread, a test task that runs zero JUnit 5 tests and reports success. ## Tooling Use the wrapper the repository ships (`./gradlew`, `./mvnw`); it pins the build tool version. Keep the existing build tool: a Maven-to-Gradle migration is a separate, requested change. | Task | Gradle | Maven | | --- | --- | --- | | Full gate | `./gradlew check` | `./mvnw -q verify` | | Compile main and tests | `./gradlew testClasses` | `./mvnw -q test-compile` | | Unit tests | `./gradlew test` | `./mvnw -q test` | | One test | `./gradlew test --tests 'com.acme.FooTest.bar'` | `./mvnw test -Dtest='FooTest#bar'` | | Dependency tree | `./gradlew dependencies --configuration runtimeClasspath` | `./mvnw dependency:tree` | | Why is X on the classpath | `./gradlew dependencyInsight --dependency jackson-databind` | `./mvnw dependency:tree -Dincludes=com.fasterxml.jackson.core` | | Format | `./gradlew spotlessApply` | `./mvnw spotless:apply` | - **Gradle:** `check` runs `test` plus every verification task a plugin attaches (Spotless's `spotlessCheck`, Checkstyle, SpotBugs); Error Prone runs inside `compileJava`. `build` is `check` plus `assemble`. A `test` task whose inputs have not changed is skipped as `UP-TO-DATE`; `--rerun` (Gradle 7.6+) forces it. - **Maven:** Surefire runs unit tests in the `test` phase (default includes `**/Test*.java`, `**/*Test.java`, `**/*Tests.java`, `**/*TestCase.java`); Failsafe runs `*IT.java` in `integration-test`/`verify`. Pin `maven-surefire-plugin` 3.x in `<pluginManagement>`: older Maven super-POMs bind Surefire 2.12.4, which runs no JUnit 5 tests and reports success. - **Versions:** Gradle keeps them in the version catalog (`gradle/libs.versions.toml`) and imports BOMs with `platform(...)`; Maven imports BOMs in `<dependencyManagement>` with `<scope>import</scope>`. Conflict resolution differs: Gradle picks the highest requested version, Maven picks the nearest declaration in the tree, which can silently downgrade a transitive dependency. `maven-enforcer-plugin`'s `dependencyConvergence` rule turns that into a build error. - **Static analysis:** `javac -Xlint:all -Werror`; Error Prone (javac plugin, compile-time bug patterns); NullAway (an Error Prone check) with JSpecify `@NullMarked`/`@Nullable`; SpotBugs on bytecode; Checkstyle or PMD for style rules. Format with Spotless running google-java-format or palantir-java-format. ```kotlin // build.gradle.kts for a Java project plugins { java alias(libs.plugins.spotless) alias(libs.plugins.errorprone) } java { toolchain { languageVersion = JavaLanguageVersion.of(21) } } dependencies { testImplementation(platform(libs.junit.bom)) testImplementation("org.junit.jupiter:junit-jupiter") testRuntimeOnly("org.junit.platform:junit-platform-launcher") testImplementation(libs.assertj.core) errorprone(libs.errorprone.core) } tasks.withType<JavaCompile>().configureEach { options.compilerArgs.addAll(listOf("-Xlint:all", "-Werror")) } tasks.test { useJUnitPlatform() // without it Gradle runs the JUnit 4 engine, which does not see Jupiter tests } ``` ## Records, Sealed Types and Pattern Matching Model data as records and closed sets of alternatives as sealed interfaces; `switch` then checks exhaustiveness (Java 21). ```java public sealed interface Command { record Upload(Path source, String target) implements Command { public Upload { // compact constructor: runs on every construction Objects.requireNonNull(source, "source"); if (target == null || target.isBlank()) throw new IllegalArgumentException("target is blank"); } } record Delete(String target) implements Command {} record Batch(List<Command> commands) implements Command { public Batch { commands = List.copyOf(commands); } // defensive copy; rejects null elements } } static String describe(Command command) { return switch (command) { // exhaustive over the sealed type: no default case Command.Upload(Path source, var target) -> "upload " + source + " -> " + target; case Command.Delete(String target) -> "delete " + target; case Command.Batch(var commands) when commands.isEmpty() -> "empty batch"; case Command.Batch(var commands) -> "batch of " + commands.size(); }; } ``` - Records nested in the sealed interface need no `permits` clause. Jackson (2.12+) deserializes records through the canonical constructor, so compact-constructor validation also guards parsed input. - Records are shallowly immutable. Copy mutable components (`List.copyOf`, `Map.copyOf`) in the compact constructor. An array component keeps reference equality: `record Blob(byte[] data)` has an `equals` that compares array identity; override `equals`/`hashCode` with `Arrays.equals`/`Arrays.hashCode` or hold an immutable type. - Leave `default` out of a `switch` over a sealed type. A new subtype then fails compilation at every switch that must handle it; a `default` branch absorbs it silently. - `instanceof` patterns bind and narrow in one step: `if (obj instanceof String s && !s.isBlank())`. The unnamed pattern `_` (Java 22) ignores record components you do not use. - Enums for fixed sets without per-instance data; `EnumMap`/`EnumSet` are array-backed and faster than hash collections keyed by enums. - `var` for locals whose type the right-hand side states (`var users = new ArrayList<User>()`); spell the type when the initializer is a call whose return type the reader cannot see. ## Errors and Null Handling ```java public Config load(Path path) throws ConfigException { try (var reader = Files.newBufferedReader(path)) { // closed on every exit path return parse(reader); } catch (NoSuchFileException e) { // specific before general throw new ConfigException("config not found: " + path, e); } catch (IOException e) { throw new ConfigException("reading config " + path, e); // always pass the cause } } ``` - Checked exceptions for conditions a caller can act on (missing file, refused connection); unchecked (`IllegalArgumentException`, `IllegalStateException`) for bugs and violated preconditions. Checked exceptions do not pass through lambdas: wrap in `UncheckedIOException` inside a stream and unwrap at the boundary. - Pass the cause to every wrapping exception. `throw new X(msg)` inside a `catch` discards the original stack trace. - try-with-resources closes resources in reverse order; an exception thrown by `close()` is attached to the primary one as suppressed (`getSuppressed()`). - `InterruptedException`: restore the flag with `Thread.currentThread().interrupt()` before returning or rethrowing unchecked. Swallowing it breaks cancellation for every caller up the stack. - `Optional` is a return type. Keep it out of fields, parameters and collections. `orElse(compute())` always evaluates `compute()`; `orElseGet(this::compute)` is lazy. Prefer `orElseThrow()` to `get()`. - Null: `Objects.requireNonNull(x, "x")` at public boundaries; JSpecify `@NullMarked` on the package plus `@Nullable` on the exceptions, checked at compile time by NullAway. Return empty collections instead of `null`. ## Streams and Collections ```java Map<String, Integer> totals = orders.stream() .collect(Collectors.toMap(Order::customer, Order::amount, Integer::sum)); // merge: duplicates add up List<String> names = users.stream() .filter(User::active) .map(User::name) .toList(); // unmodifiable (Java 16+) Map<Status, List<Order>> byStatus = orders.stream() .collect(Collectors.groupingBy(Order::status, () -> new EnumMap<>(Status.class), Collectors.toList())); ``` - `Collectors.toMap` without a merge function throws `IllegalStateException` on a duplicate key, and `NullPointerException` on a null value. - `Stream.toList()` returns an unmodifiable list that allows nulls; `Collectors.toList()` makes no promise about mutability; `Collectors.toUnmodifiableList()` rejects nulls. - `List.of`, `Set.of`, `Map.of` are immutable and reject nulls; `Set.of`/`Map.of` throw on duplicates at construction, and their iteration order is unspecified and changes between JVM runs. Sort, or use `LinkedHashMap`/`TreeMap`, wherever order is observable (JSON output, snapshots, log lines under test). - `Arrays.asList` is fixed-size and writes through to the backing array. - Sequenced collections (21): `getFirst()`, `getLast()`, `reversed()` on lists, deques, `LinkedHashSet` and `LinkedHashMap`. - Streams are lazy and single-use. Side effects in `peek` or `map` may never run: since Java 9, `count()` skips the pipeline when the size is known from the source. - `parallelStream()` runs on the JVM-wide common `ForkJoinPool`. Blocking IO inside it starves every other parallel stream and every `CompletableFuture.supplyAsync` without an executor. Use it only for CPU-bound work on large inputs, measured. - Stream gatherers (final in 24): `stream.gather(Gatherers.windowFixed(3))`, `windowSliding`, and `Gatherers.mapConcurrent(n, fn)` for bounded concurrent mapping on virtual threads. ## Concurrency ```java try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { List<Future<String>> pages = urls.stream() .map(url -> executor.submit(() -> fetch(url))) // one virtual thread per blocking call .toList(); for (Future<String> page : pages) { consume(page.get()); // ExecutionException wraps the task's failure } } // close() waits for every submitted task ``` - **Virtual threads (21)** are for blocking IO at high concurrency. Create one per task and never pool them. The executor has no size limit, so bound calls to a downstream service with a `Semaphore`. - **Pinning:** before JDK 24, blocking inside `synchronized` pinned the carrier thread; JEP 491 (JDK 24) removed that for `synchronized`. Native frames (JNI, some drivers) still pin. On 21, guard blocking calls with `ReentrantLock` and watch the `jdk.VirtualThreadPinned` JFR event. - `ThreadLocal` works on virtual threads, but each thread gets its own copy: a per-thread cache of heavy objects multiplies by the thread count. `ScopedValue` (final in 25) passes request context down a call tree immutably. - CPU-bound work belongs on a platform pool sized to the cores (`Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())`) or a `ForkJoinPool`. - `CompletableFuture.supplyAsync(fn)` without an executor runs on the common pool; pass one. `join()` throws unchecked `CompletionException`, `get()` throws checked `ExecutionException`. Attach `exceptionally`/`handle` at the end of a chain; a failed future nobody observes fails silently. - `StructuredTaskScope` is still a preview API in 25 and its shape changed between releases; keep it out of code that must build without `--enable-preview`. - Shared state: `ConcurrentHashMap.merge`/`compute` for atomic read-modify-write (a `get` followed by `put` races); `LongAdder` for contended counters, `AtomicLong` otherwise; `volatile` for a flag with one writer. Immutable records cross threads without locks. - Spring Boot 3.2+: `spring.threads.virtual.enabled=true` serves requests on virtual threads. ## Packages and Modules - Standard layout: `src/main/java`, `src/main/resources`, `src/test/java`, `src/test/resources`; the directory path matches the package. - Package-private (no modifier) is the default for implementation classes; mark `public` only the API. Tests in the same package reach package-private members without widening them. - JPMS (`module-info.java`): `exports` the API packages, `requires transitive` for types your API exposes, `opens` for packages that reflective frameworks (Jackson, Hibernate, Spring) must read. A missing `opens` surfaces at runtime as `InaccessibleObjectException`. Applications that publish no library usually stay on the classpath. ## Spring Boot Essentials ```java @ConfigurationProperties(prefix = "spool") @Validated public record SpoolProperties(@NotBlank String directory, @Positive int maxFiles) {} @Service class SpoolService { private final SpoolRepository repository; SpoolService(SpoolRepository repository) { // single constructor: injected without @Autowired this.repository = repository; } @Transactional public void archive(long id) { var entry = repository.findById(id).orElseThrow(() -> new EntryNotFoundException(id)); entry.markArchived(); // dirty checking writes the change at commit } } ``` - Register properties with `@ConfigurationPropertiesScan` or `@EnableConfigurationProperties(SpoolProperties.class)`. Records bind through the constructor; `@Validated` fails startup on invalid values. - Proxy-based annotations (`@Transactional`, `@Async`, `@Cacheable`) do nothing on a self-call: `this.archive(id)` from the same bean bypasses the proxy. Move the method to another bean. - Rollback happens by default only for unchecked exceptions and `Error`; a checked exception commits. Use `@Transactional(rollbackFor = Exception.class)` where checked exceptions signal failure. - Set `spring.jpa.open-in-view=false`. The default keeps the persistence context open through response rendering, hides lazy-loading N+1 queries and holds a connection for the whole request (Boot logs a warning about it). Load what a use case needs with `JOIN FETCH` or `@EntityGraph`. - JPA entities are classes with a no-arg constructor. Records cannot be entities. Base `equals`/`hashCode` on the id or a business key, and keep Lombok `@Data` off entities: its `toString`/`hashCode` walk lazy associations. - Actuator: expose `health` and `info`; keep `env`, `heapdump`, `loggers` and the rest behind authentication. - Boot 3+ uses the `jakarta.*` namespace (`jakarta.persistence`, `jakarta.validation`). `javax.*` imports mark Boot 2 era code. ## Testing ```java class SpoolWriterTest { @TempDir Path root; @Test void writesEntryUnderRoot() throws IOException { new SpoolWriter(root).write("a.json", "{}"); assertThat(root.resolve("a.json")).hasContent("{}"); } @ParameterizedTest @ValueSource(strings = {"../escape.json", "/etc/passwd", "a/../../b"}) void rejectsPathsOutsideRoot(String name) { var writer = new SpoolWriter(root); assertThatThrownBy(() -> writer.write(name, "{}")) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("outside spool root"); } } ``` - JUnit Jupiter (JUnit 5; JUnit 6 keeps the `org.junit.jupiter.api` annotations): `@Test`, `@ParameterizedTest` with `@ValueSource`/`@CsvSource`/`@MethodSource`, `@TempDir`, `@Nested`, `@BeforeEach`. Test classes and methods can be package-private. - AssertJ (`assertThat(...)`, `assertThatThrownBy`) produces better failure messages than `assertEquals`. JUnit's `assertThrows` returns the exception for further checks. - Mockito with `@ExtendWith(MockitoExtension.class)` uses strict stubs: an unused stub fails the test. Mock roles you own; build value objects for real. - Integration tests: Testcontainers with the production database engine. H2 in a compatibility mode differs in SQL dialect and locking. - Spring slices (`@WebMvcTest`, `@DataJpaTest`, `@JsonTest`) load a narrow context; `@SpringBootTest` loads all of it. Each distinct context configuration (another set of mocked beans, other properties) starts a new context, the usual cause of slow Spring suites. `@MockitoBean` (Spring Framework 6.2) replaces the deprecated `@MockBean`.
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기