From 9ceb699e9a926255ddca8c0e1e34989a5e3c91f7 Mon Sep 17 00:00:00 2001 From: Chris Yau Date: Fri, 20 Mar 2026 11:49:21 +0800 Subject: [PATCH] feat(rules): add Java language rules (#645) Adds Java language rules (coding-style, hooks, patterns, security, testing) following the established language rule conventions. --- rules/java/coding-style.md | 114 +++++++++++++++++++++++++++++ rules/java/hooks.md | 18 +++++ rules/java/patterns.md | 146 +++++++++++++++++++++++++++++++++++++ rules/java/security.md | 100 +++++++++++++++++++++++++ rules/java/testing.md | 131 +++++++++++++++++++++++++++++++++ 5 files changed, 509 insertions(+) create mode 100644 rules/java/coding-style.md create mode 100644 rules/java/hooks.md create mode 100644 rules/java/patterns.md create mode 100644 rules/java/security.md create mode 100644 rules/java/testing.md diff --git a/rules/java/coding-style.md b/rules/java/coding-style.md new file mode 100644 index 00000000..d20d5ab6 --- /dev/null +++ b/rules/java/coding-style.md @@ -0,0 +1,114 @@ +--- +paths: + - "**/*.java" +--- +# Java Coding Style + +> This file extends [common/coding-style.md](../common/coding-style.md) with Java-specific content. + +## Formatting + +- **google-java-format** or **Checkstyle** (Google or Sun style) for enforcement +- One public top-level type per file +- Consistent indent: 2 or 4 spaces (match project standard) +- Member order: constants, fields, constructors, public methods, protected, private + +## Immutability + +- Prefer `record` for value types (Java 16+) +- Mark fields `final` by default — use mutable state only when required +- Return defensive copies from public APIs: `List.copyOf()`, `Map.copyOf()`, `Set.copyOf()` +- Copy-on-write: return new instances rather than mutating existing ones + +```java +// GOOD — immutable value type +public record OrderSummary(Long id, String customerName, BigDecimal total) {} + +// GOOD — final fields, no setters +public class Order { + private final Long id; + private final List items; + + public List getItems() { + return List.copyOf(items); + } +} +``` + +## Naming + +Follow standard Java conventions: +- `PascalCase` for classes, interfaces, records, enums +- `camelCase` for methods, fields, parameters, local variables +- `SCREAMING_SNAKE_CASE` for `static final` constants +- Packages: all lowercase, reverse domain (`com.example.app.service`) + +## Modern Java Features + +Use modern language features where they improve clarity: +- **Records** for DTOs and value types (Java 16+) +- **Sealed classes** for closed type hierarchies (Java 17+) +- **Pattern matching** with `instanceof` — no explicit cast (Java 16+) +- **Text blocks** for multi-line strings — SQL, JSON templates (Java 15+) +- **Switch expressions** with arrow syntax (Java 14+) +- **Pattern matching in switch** — exhaustive sealed type handling (Java 21+) + +```java +// Pattern matching instanceof +if (shape instanceof Circle c) { + return Math.PI * c.radius() * c.radius(); +} + +// Sealed type hierarchy +public sealed interface PaymentMethod permits CreditCard, BankTransfer, Wallet {} + +// Switch expression +String label = switch (status) { + case ACTIVE -> "Active"; + case SUSPENDED -> "Suspended"; + case CLOSED -> "Closed"; +}; +``` + +## Optional Usage + +- Return `Optional` from finder methods that may have no result +- Use `map()`, `flatMap()`, `orElseThrow()` — never call `get()` without `isPresent()` +- Never use `Optional` as a field type or method parameter + +```java +// GOOD +return repository.findById(id) + .map(ResponseDto::from) + .orElseThrow(() -> new OrderNotFoundException(id)); + +// BAD — Optional as parameter +public void process(Optional name) {} +``` + +## Error Handling + +- Prefer unchecked exceptions for domain errors +- Create domain-specific exceptions extending `RuntimeException` +- Avoid broad `catch (Exception e)` unless at top-level handlers +- Include context in exception messages + +```java +public class OrderNotFoundException extends RuntimeException { + public OrderNotFoundException(Long id) { + super("Order not found: id=" + id); + } +} +``` + +## Streams + +- Use streams for transformations; keep pipelines short (3-4 operations max) +- Prefer method references when readable: `.map(Order::getTotal)` +- Avoid side effects in stream operations +- For complex logic, prefer a loop over a convoluted stream pipeline + +## References + +See skill: `java-coding-standards` for full coding standards with examples. +See skill: `jpa-patterns` for JPA/Hibernate entity design patterns. diff --git a/rules/java/hooks.md b/rules/java/hooks.md new file mode 100644 index 00000000..9dd33b38 --- /dev/null +++ b/rules/java/hooks.md @@ -0,0 +1,18 @@ +--- +paths: + - "**/*.java" + - "**/pom.xml" + - "**/build.gradle" + - "**/build.gradle.kts" +--- +# Java Hooks + +> This file extends [common/hooks.md](../common/hooks.md) with Java-specific content. + +## PostToolUse Hooks + +Configure in `~/.claude/settings.json`: + +- **google-java-format**: Auto-format `.java` files after edit +- **checkstyle**: Run style checks after editing Java files +- **./mvnw compile** or **./gradlew compileJava**: Verify compilation after changes diff --git a/rules/java/patterns.md b/rules/java/patterns.md new file mode 100644 index 00000000..570282bd --- /dev/null +++ b/rules/java/patterns.md @@ -0,0 +1,146 @@ +--- +paths: + - "**/*.java" +--- +# Java Patterns + +> This file extends [common/patterns.md](../common/patterns.md) with Java-specific content. + +## Repository Pattern + +Encapsulate data access behind an interface: + +```java +public interface OrderRepository { + Optional findById(Long id); + List findAll(); + Order save(Order order); + void deleteById(Long id); +} +``` + +Concrete implementations handle storage details (JPA, JDBC, in-memory for tests). + +## Service Layer + +Business logic in service classes; keep controllers and repositories thin: + +```java +public class OrderService { + private final OrderRepository orderRepository; + private final PaymentGateway paymentGateway; + + public OrderService(OrderRepository orderRepository, PaymentGateway paymentGateway) { + this.orderRepository = orderRepository; + this.paymentGateway = paymentGateway; + } + + public OrderSummary placeOrder(CreateOrderRequest request) { + var order = Order.from(request); + paymentGateway.charge(order.total()); + var saved = orderRepository.save(order); + return OrderSummary.from(saved); + } +} +``` + +## Constructor Injection + +Always use constructor injection — never field injection: + +```java +// GOOD — constructor injection (testable, immutable) +public class NotificationService { + private final EmailSender emailSender; + + public NotificationService(EmailSender emailSender) { + this.emailSender = emailSender; + } +} + +// BAD — field injection (untestable without reflection, requires framework magic) +public class NotificationService { + @Inject // or @Autowired + private EmailSender emailSender; +} +``` + +## DTO Mapping + +Use records for DTOs. Map at service/controller boundaries: + +```java +public record OrderResponse(Long id, String customer, BigDecimal total) { + public static OrderResponse from(Order order) { + return new OrderResponse(order.getId(), order.getCustomerName(), order.getTotal()); + } +} +``` + +## Builder Pattern + +Use for objects with many optional parameters: + +```java +public class SearchCriteria { + private final String query; + private final int page; + private final int size; + private final String sortBy; + + private SearchCriteria(Builder builder) { + this.query = builder.query; + this.page = builder.page; + this.size = builder.size; + this.sortBy = builder.sortBy; + } + + public static class Builder { + private String query = ""; + private int page = 0; + private int size = 20; + private String sortBy = "id"; + + public Builder query(String query) { this.query = query; return this; } + public Builder page(int page) { this.page = page; return this; } + public Builder size(int size) { this.size = size; return this; } + public Builder sortBy(String sortBy) { this.sortBy = sortBy; return this; } + public SearchCriteria build() { return new SearchCriteria(this); } + } +} +``` + +## Sealed Types for Domain Models + +```java +public sealed interface PaymentResult permits PaymentSuccess, PaymentFailure { + record PaymentSuccess(String transactionId, BigDecimal amount) implements PaymentResult {} + record PaymentFailure(String errorCode, String message) implements PaymentResult {} +} + +// Exhaustive handling (Java 21+) +String message = switch (result) { + case PaymentSuccess s -> "Paid: " + s.transactionId(); + case PaymentFailure f -> "Failed: " + f.errorCode(); +}; +``` + +## API Response Envelope + +Consistent API responses: + +```java +public record ApiResponse(boolean success, T data, String error) { + public static ApiResponse ok(T data) { + return new ApiResponse<>(true, data, null); + } + public static ApiResponse error(String message) { + return new ApiResponse<>(false, null, message); + } +} +``` + +## References + +See skill: `springboot-patterns` for Spring Boot architecture patterns. +See skill: `jpa-patterns` for entity design and query optimization. diff --git a/rules/java/security.md b/rules/java/security.md new file mode 100644 index 00000000..31ca61b6 --- /dev/null +++ b/rules/java/security.md @@ -0,0 +1,100 @@ +--- +paths: + - "**/*.java" +--- +# Java Security + +> This file extends [common/security.md](../common/security.md) with Java-specific content. + +## Secrets Management + +- Never hardcode API keys, tokens, or credentials in source code +- Use environment variables: `System.getenv("API_KEY")` +- Use a secret manager (Vault, AWS Secrets Manager) for production secrets +- Keep local config files with secrets in `.gitignore` + +```java +// BAD +private static final String API_KEY = "sk-abc123..."; + +// GOOD — environment variable +String apiKey = System.getenv("PAYMENT_API_KEY"); +Objects.requireNonNull(apiKey, "PAYMENT_API_KEY must be set"); +``` + +## SQL Injection Prevention + +- Always use parameterized queries — never concatenate user input into SQL +- Use `PreparedStatement` or your framework's parameterized query API +- Validate and sanitize any input used in native queries + +```java +// BAD — SQL injection via string concatenation +Statement stmt = conn.createStatement(); +String sql = "SELECT * FROM orders WHERE name = '" + name + "'"; +stmt.executeQuery(sql); + +// GOOD — PreparedStatement with parameterized query +PreparedStatement ps = conn.prepareStatement("SELECT * FROM orders WHERE name = ?"); +ps.setString(1, name); + +// GOOD — JDBC template +jdbcTemplate.query("SELECT * FROM orders WHERE name = ?", mapper, name); +``` + +## Input Validation + +- Validate all user input at system boundaries before processing +- Use Bean Validation (`@NotNull`, `@NotBlank`, `@Size`) on DTOs when using a validation framework +- Sanitize file paths and user-provided strings before use +- Reject input that fails validation with clear error messages + +```java +// Validate manually in plain Java +public Order createOrder(String customerName, BigDecimal amount) { + if (customerName == null || customerName.isBlank()) { + throw new IllegalArgumentException("Customer name is required"); + } + if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException("Amount must be positive"); + } + return new Order(customerName, amount); +} +``` + +## Authentication and Authorization + +- Never implement custom auth crypto — use established libraries +- Store passwords with bcrypt or Argon2, never MD5/SHA1 +- Enforce authorization checks at service boundaries +- Clear sensitive data from logs — never log passwords, tokens, or PII + +## Dependency Security + +- Run `mvn dependency:tree` or `./gradlew dependencies` to audit transitive dependencies +- Use OWASP Dependency-Check or Snyk to scan for known CVEs +- Keep dependencies updated — set up Dependabot or Renovate + +## Error Messages + +- Never expose stack traces, internal paths, or SQL errors in API responses +- Map exceptions to safe, generic client messages at handler boundaries +- Log detailed errors server-side; return generic messages to clients + +```java +// Log the detail, return a generic message +try { + return orderService.findById(id); +} catch (OrderNotFoundException ex) { + log.warn("Order not found: id={}", id); + return ApiResponse.error("Resource not found"); // generic, no internals +} catch (Exception ex) { + log.error("Unexpected error processing order id={}", id, ex); + return ApiResponse.error("Internal server error"); // never expose ex.getMessage() +} +``` + +## References + +See skill: `springboot-security` for Spring Security authentication and authorization patterns. +See skill: `security-review` for general security checklists. diff --git a/rules/java/testing.md b/rules/java/testing.md new file mode 100644 index 00000000..aa2e91f3 --- /dev/null +++ b/rules/java/testing.md @@ -0,0 +1,131 @@ +--- +paths: + - "**/*.java" +--- +# Java Testing + +> This file extends [common/testing.md](../common/testing.md) with Java-specific content. + +## Test Framework + +- **JUnit 5** (`@Test`, `@ParameterizedTest`, `@Nested`, `@DisplayName`) +- **AssertJ** for fluent assertions (`assertThat(result).isEqualTo(expected)`) +- **Mockito** for mocking dependencies +- **Testcontainers** for integration tests requiring databases or services + +## Test Organization + +``` +src/test/java/com/example/app/ + service/ # Unit tests for service layer + controller/ # Web layer / API tests + repository/ # Data access tests + integration/ # Cross-layer integration tests +``` + +Mirror the `src/main/java` package structure in `src/test/java`. + +## Unit Test Pattern + +```java +@ExtendWith(MockitoExtension.class) +class OrderServiceTest { + + @Mock + private OrderRepository orderRepository; + + private OrderService orderService; + + @BeforeEach + void setUp() { + orderService = new OrderService(orderRepository); + } + + @Test + @DisplayName("findById returns order when exists") + void findById_existingOrder_returnsOrder() { + var order = new Order(1L, "Alice", BigDecimal.TEN); + when(orderRepository.findById(1L)).thenReturn(Optional.of(order)); + + var result = orderService.findById(1L); + + assertThat(result.customerName()).isEqualTo("Alice"); + verify(orderRepository).findById(1L); + } + + @Test + @DisplayName("findById throws when order not found") + void findById_missingOrder_throws() { + when(orderRepository.findById(99L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> orderService.findById(99L)) + .isInstanceOf(OrderNotFoundException.class) + .hasMessageContaining("99"); + } +} +``` + +## Parameterized Tests + +```java +@ParameterizedTest +@CsvSource({ + "100.00, 10, 90.00", + "50.00, 0, 50.00", + "200.00, 25, 150.00" +}) +@DisplayName("discount applied correctly") +void applyDiscount(BigDecimal price, int pct, BigDecimal expected) { + assertThat(PricingUtils.discount(price, pct)).isEqualByComparingTo(expected); +} +``` + +## Integration Tests + +Use Testcontainers for real database integration: + +```java +@Testcontainers +class OrderRepositoryIT { + + @Container + static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:16"); + + private OrderRepository repository; + + @BeforeEach + void setUp() { + var dataSource = new PGSimpleDataSource(); + dataSource.setUrl(postgres.getJdbcUrl()); + dataSource.setUser(postgres.getUsername()); + dataSource.setPassword(postgres.getPassword()); + repository = new JdbcOrderRepository(dataSource); + } + + @Test + void save_and_findById() { + var saved = repository.save(new Order(null, "Bob", BigDecimal.ONE)); + var found = repository.findById(saved.getId()); + assertThat(found).isPresent(); + } +} +``` + +For Spring Boot integration tests, see skill: `springboot-tdd`. + +## Test Naming + +Use descriptive names with `@DisplayName`: +- `methodName_scenario_expectedBehavior()` for method names +- `@DisplayName("human-readable description")` for reports + +## Coverage + +- Target 80%+ line coverage +- Use JaCoCo for coverage reporting +- Focus on service and domain logic — skip trivial getters/config classes + +## References + +See skill: `springboot-tdd` for Spring Boot TDD patterns with MockMvc and Testcontainers. +See skill: `java-coding-standards` for testing expectations.