mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-01-31 11:46:11 +08:00
feat(skills): add Java Spring Boot skills
Adds 6 new skills for Java Spring Boot development: - java-coding-standards: naming, immutability, Optional, streams - springboot-patterns: REST API, service layer, caching, async - springboot-tdd: JUnit 5, Mockito, MockMvc, Testcontainers - springboot-security: Spring Security, validation, CSRF - springboot-verification: Maven/Gradle build verification - jpa-patterns: entity design, relationships Thanks @examin!
This commit is contained in:
138
skills/java-coding-standards/SKILL.md
Normal file
138
skills/java-coding-standards/SKILL.md
Normal file
@@ -0,0 +1,138 @@
|
||||
---
|
||||
name: java-coding-standards
|
||||
description: Java coding standards for Spring Boot services: naming, immutability, Optional usage, streams, exceptions, generics, and project layout.
|
||||
---
|
||||
|
||||
# Java Coding Standards
|
||||
|
||||
Standards for readable, maintainable Java (17+) code in Spring Boot services.
|
||||
|
||||
## Core Principles
|
||||
|
||||
- Prefer clarity over cleverness
|
||||
- Immutable by default; minimize shared mutable state
|
||||
- Fail fast with meaningful exceptions
|
||||
- Consistent naming and package structure
|
||||
|
||||
## Naming
|
||||
|
||||
```java
|
||||
// ✅ Classes/Records: PascalCase
|
||||
public class MarketService {}
|
||||
public record Money(BigDecimal amount, Currency currency) {}
|
||||
|
||||
// ✅ Methods/fields: camelCase
|
||||
private final MarketRepository marketRepository;
|
||||
public Market findBySlug(String slug) {}
|
||||
|
||||
// ✅ Constants: UPPER_SNAKE_CASE
|
||||
private static final int MAX_PAGE_SIZE = 100;
|
||||
```
|
||||
|
||||
## Immutability
|
||||
|
||||
```java
|
||||
// ✅ 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
|
||||
}
|
||||
```
|
||||
|
||||
## Optional Usage
|
||||
|
||||
```java
|
||||
// ✅ Return Optional from find* methods
|
||||
Optional<Market> market = marketRepository.findBySlug(slug);
|
||||
|
||||
// ✅ Map/flatMap instead of get()
|
||||
return market
|
||||
.map(MarketResponse::from)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Market not found"));
|
||||
```
|
||||
|
||||
## Streams Best Practices
|
||||
|
||||
```java
|
||||
// ✅ Use streams for transformations, keep pipelines short
|
||||
List<String> names = markets.stream()
|
||||
.map(Market::name)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
|
||||
// ❌ Avoid complex nested streams; prefer loops for clarity
|
||||
```
|
||||
|
||||
## Exceptions
|
||||
|
||||
- Use unchecked exceptions for domain errors; wrap technical exceptions with context
|
||||
- Create domain-specific exceptions (e.g., `MarketNotFoundException`)
|
||||
- Avoid broad `catch (Exception ex)` unless rethrowing/logging centrally
|
||||
|
||||
```java
|
||||
throw new MarketNotFoundException(slug);
|
||||
```
|
||||
|
||||
## Generics and Type Safety
|
||||
|
||||
- Avoid raw types; declare generic parameters
|
||||
- Prefer bounded generics for reusable utilities
|
||||
|
||||
```java
|
||||
public <T extends Identifiable> Map<Long, T> indexById(Collection<T> items) { ... }
|
||||
```
|
||||
|
||||
## Project Structure (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)
|
||||
```
|
||||
|
||||
## Formatting and Style
|
||||
|
||||
- Use 2 or 4 spaces consistently (project standard)
|
||||
- One public top-level type per file
|
||||
- Keep methods short and focused; extract helpers
|
||||
- Order members: constants, fields, constructors, public methods, protected, private
|
||||
|
||||
## Code Smells to Avoid
|
||||
|
||||
- Long parameter lists → use DTO/builders
|
||||
- Deep nesting → early returns
|
||||
- Magic numbers → named constants
|
||||
- Static mutable state → prefer dependency injection
|
||||
- Silent catch blocks → log and act or rethrow
|
||||
|
||||
## Logging
|
||||
|
||||
```java
|
||||
private static final Logger log = LoggerFactory.getLogger(MarketService.class);
|
||||
log.info("fetch_market slug={}", slug);
|
||||
log.error("failed_fetch_market slug={}", slug, ex);
|
||||
```
|
||||
|
||||
## Null Handling
|
||||
|
||||
- Accept `@Nullable` only when unavoidable; otherwise use `@NonNull`
|
||||
- Use Bean Validation (`@NotNull`, `@NotBlank`) on inputs
|
||||
|
||||
## Testing Expectations
|
||||
|
||||
- JUnit 5 + AssertJ for fluent assertions
|
||||
- Mockito for mocking; avoid partial mocks where possible
|
||||
- Favor deterministic tests; no hidden sleeps
|
||||
|
||||
**Remember**: Keep code intentional, typed, and observable. Optimize for maintainability over micro-optimizations unless proven necessary.
|
||||
141
skills/jpa-patterns/SKILL.md
Normal file
141
skills/jpa-patterns/SKILL.md
Normal file
@@ -0,0 +1,141 @@
|
||||
---
|
||||
name: jpa-patterns
|
||||
description: JPA/Hibernate patterns for entity design, relationships, query optimization, transactions, auditing, indexing, pagination, and pooling in Spring Boot.
|
||||
---
|
||||
|
||||
# JPA/Hibernate Patterns
|
||||
|
||||
Use for data modeling, repositories, and performance tuning in Spring Boot.
|
||||
|
||||
## Entity Design
|
||||
|
||||
```java
|
||||
@Entity
|
||||
@Table(name = "markets", indexes = {
|
||||
@Index(name = "idx_markets_slug", columnList = "slug", unique = true)
|
||||
})
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class MarketEntity {
|
||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, length = 200)
|
||||
private String name;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 120)
|
||||
private String slug;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
private MarketStatus status = MarketStatus.ACTIVE;
|
||||
|
||||
@CreatedDate private Instant createdAt;
|
||||
@LastModifiedDate private Instant updatedAt;
|
||||
}
|
||||
```
|
||||
|
||||
Enable auditing:
|
||||
```java
|
||||
@Configuration
|
||||
@EnableJpaAuditing
|
||||
class JpaConfig {}
|
||||
```
|
||||
|
||||
## Relationships and N+1 Prevention
|
||||
|
||||
```java
|
||||
@OneToMany(mappedBy = "market", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private List<PositionEntity> positions = new ArrayList<>();
|
||||
```
|
||||
|
||||
- Default to lazy loading; use `JOIN FETCH` in queries when needed
|
||||
- Avoid `EAGER` on collections; use DTO projections for read paths
|
||||
|
||||
```java
|
||||
@Query("select m from MarketEntity m left join fetch m.positions where m.id = :id")
|
||||
Optional<MarketEntity> findWithPositions(@Param("id") Long id);
|
||||
```
|
||||
|
||||
## Repository Patterns
|
||||
|
||||
```java
|
||||
public interface MarketRepository extends JpaRepository<MarketEntity, Long> {
|
||||
Optional<MarketEntity> findBySlug(String slug);
|
||||
|
||||
@Query("select m from MarketEntity m where m.status = :status")
|
||||
Page<MarketEntity> findByStatus(@Param("status") MarketStatus status, Pageable pageable);
|
||||
}
|
||||
```
|
||||
|
||||
- Use projections for lightweight queries:
|
||||
```java
|
||||
public interface MarketSummary {
|
||||
Long getId();
|
||||
String getName();
|
||||
MarketStatus getStatus();
|
||||
}
|
||||
Page<MarketSummary> findAllBy(Pageable pageable);
|
||||
```
|
||||
|
||||
## Transactions
|
||||
|
||||
- Annotate service methods with `@Transactional`
|
||||
- Use `@Transactional(readOnly = true)` for read paths to optimize
|
||||
- Choose propagation carefully; avoid long-running transactions
|
||||
|
||||
```java
|
||||
@Transactional
|
||||
public Market updateStatus(Long id, MarketStatus status) {
|
||||
MarketEntity entity = repo.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Market"));
|
||||
entity.setStatus(status);
|
||||
return Market.from(entity);
|
||||
}
|
||||
```
|
||||
|
||||
## Pagination
|
||||
|
||||
```java
|
||||
PageRequest page = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending());
|
||||
Page<MarketEntity> markets = repo.findByStatus(MarketStatus.ACTIVE, page);
|
||||
```
|
||||
|
||||
For cursor-like pagination, include `id > :lastId` in JPQL with ordering.
|
||||
|
||||
## Indexing and Performance
|
||||
|
||||
- Add indexes for common filters (`status`, `slug`, foreign keys)
|
||||
- Use composite indexes matching query patterns (`status, created_at`)
|
||||
- Avoid `select *`; project only needed columns
|
||||
- Batch writes with `saveAll` and `hibernate.jdbc.batch_size`
|
||||
|
||||
## Connection Pooling (HikariCP)
|
||||
|
||||
Recommended properties:
|
||||
```
|
||||
spring.datasource.hikari.maximum-pool-size=20
|
||||
spring.datasource.hikari.minimum-idle=5
|
||||
spring.datasource.hikari.connection-timeout=30000
|
||||
spring.datasource.hikari.validation-timeout=5000
|
||||
```
|
||||
|
||||
For PostgreSQL LOB handling, add:
|
||||
```
|
||||
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
|
||||
```
|
||||
|
||||
## Caching
|
||||
|
||||
- 1st-level cache is per EntityManager; avoid keeping entities across transactions
|
||||
- For read-heavy entities, consider second-level cache cautiously; validate eviction strategy
|
||||
|
||||
## Migrations
|
||||
|
||||
- Use Flyway or Liquibase; never rely on Hibernate auto DDL in production
|
||||
- Keep migrations idempotent and additive; avoid dropping columns without plan
|
||||
|
||||
## Testing Data Access
|
||||
|
||||
- Prefer `@DataJpaTest` with Testcontainers to mirror production
|
||||
- Assert SQL efficiency using logs: set `logging.level.org.hibernate.SQL=DEBUG` and `logging.level.org.hibernate.orm.jdbc.bind=TRACE` for parameter values
|
||||
|
||||
**Remember**: Keep entities lean, queries intentional, and transactions short. Prevent N+1 with fetch strategies and projections, and index for your read/write paths.
|
||||
304
skills/springboot-patterns/SKILL.md
Normal file
304
skills/springboot-patterns/SKILL.md
Normal file
@@ -0,0 +1,304 @@
|
||||
---
|
||||
name: springboot-patterns
|
||||
description: Spring Boot architecture patterns, REST API design, layered services, data access, caching, async processing, and logging. Use for Java Spring Boot backend work.
|
||||
---
|
||||
|
||||
# Spring Boot Development Patterns
|
||||
|
||||
Spring Boot architecture and API patterns for scalable, production-grade services.
|
||||
|
||||
## REST API Structure
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/markets")
|
||||
@Validated
|
||||
class MarketController {
|
||||
private final MarketService marketService;
|
||||
|
||||
MarketController(MarketService marketService) {
|
||||
this.marketService = marketService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
ResponseEntity<Page<MarketResponse>> list(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size) {
|
||||
Page<Market> markets = marketService.list(PageRequest.of(page, size));
|
||||
return ResponseEntity.ok(markets.map(MarketResponse::from));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
ResponseEntity<MarketResponse> create(@Valid @RequestBody CreateMarketRequest request) {
|
||||
Market market = marketService.create(request);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(MarketResponse.from(market));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Repository Pattern (Spring Data JPA)
|
||||
|
||||
```java
|
||||
public interface MarketRepository extends JpaRepository<MarketEntity, Long> {
|
||||
@Query("select m from MarketEntity m where m.status = :status order by m.volume desc")
|
||||
List<MarketEntity> findActive(@Param("status") MarketStatus status, Pageable pageable);
|
||||
}
|
||||
```
|
||||
|
||||
## Service Layer with Transactions
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class MarketService {
|
||||
private final MarketRepository repo;
|
||||
|
||||
public MarketService(MarketRepository repo) {
|
||||
this.repo = repo;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Market create(CreateMarketRequest request) {
|
||||
MarketEntity entity = MarketEntity.from(request);
|
||||
MarketEntity saved = repo.save(entity);
|
||||
return Market.from(saved);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## DTOs and Validation
|
||||
|
||||
```java
|
||||
public record CreateMarketRequest(
|
||||
@NotBlank @Size(max = 200) String name,
|
||||
@NotBlank @Size(max = 2000) String description,
|
||||
@NotNull @FutureOrPresent Instant endDate,
|
||||
@NotEmpty List<@NotBlank String> categories) {}
|
||||
|
||||
public record MarketResponse(Long id, String name, MarketStatus status) {
|
||||
static MarketResponse from(Market market) {
|
||||
return new MarketResponse(market.id(), market.name(), market.status());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Exception Handling
|
||||
|
||||
```java
|
||||
@ControllerAdvice
|
||||
class GlobalExceptionHandler {
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
ResponseEntity<ApiError> handleValidation(MethodArgumentNotValidException ex) {
|
||||
String message = ex.getBindingResult().getFieldErrors().stream()
|
||||
.map(e -> e.getField() + ": " + e.getDefaultMessage())
|
||||
.collect(Collectors.joining(", "));
|
||||
return ResponseEntity.badRequest().body(ApiError.validation(message));
|
||||
}
|
||||
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
ResponseEntity<ApiError> handleAccessDenied() {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ApiError.of("Forbidden"));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
ResponseEntity<ApiError> handleGeneric(Exception ex) {
|
||||
// Log unexpected errors with stack traces
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(ApiError.of("Internal server error"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Caching
|
||||
|
||||
Requires `@EnableCaching` on a configuration class.
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class MarketCacheService {
|
||||
private final MarketRepository repo;
|
||||
|
||||
public MarketCacheService(MarketRepository repo) {
|
||||
this.repo = repo;
|
||||
}
|
||||
|
||||
@Cacheable(value = "market", key = "#id")
|
||||
public Market getById(Long id) {
|
||||
return repo.findById(id)
|
||||
.map(Market::from)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Market not found"));
|
||||
}
|
||||
|
||||
@CacheEvict(value = "market", key = "#id")
|
||||
public void evict(Long id) {}
|
||||
}
|
||||
```
|
||||
|
||||
## Async Processing
|
||||
|
||||
Requires `@EnableAsync` on a configuration class.
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class NotificationService {
|
||||
@Async
|
||||
public CompletableFuture<Void> sendAsync(Notification notification) {
|
||||
// send email/SMS
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Logging (SLF4J)
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class ReportService {
|
||||
private static final Logger log = LoggerFactory.getLogger(ReportService.class);
|
||||
|
||||
public Report generate(Long marketId) {
|
||||
log.info("generate_report marketId={}", marketId);
|
||||
try {
|
||||
// logic
|
||||
} catch (Exception ex) {
|
||||
log.error("generate_report_failed marketId={}", marketId, ex);
|
||||
throw ex;
|
||||
}
|
||||
return new Report();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Middleware / Filters
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class RequestLoggingFilter extends OncePerRequestFilter {
|
||||
private static final Logger log = LoggerFactory.getLogger(RequestLoggingFilter.class);
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
long start = System.currentTimeMillis();
|
||||
try {
|
||||
filterChain.doFilter(request, response);
|
||||
} finally {
|
||||
long duration = System.currentTimeMillis() - start;
|
||||
log.info("req method={} uri={} status={} durationMs={}",
|
||||
request.getMethod(), request.getRequestURI(), response.getStatus(), duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Pagination and Sorting
|
||||
|
||||
```java
|
||||
PageRequest page = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending());
|
||||
Page<Market> results = marketService.list(page);
|
||||
```
|
||||
|
||||
## Error-Resilient External Calls
|
||||
|
||||
```java
|
||||
public <T> T withRetry(Supplier<T> supplier, int maxRetries) {
|
||||
int attempts = 0;
|
||||
while (true) {
|
||||
try {
|
||||
return supplier.get();
|
||||
} catch (Exception ex) {
|
||||
attempts++;
|
||||
if (attempts >= maxRetries) {
|
||||
throw ex;
|
||||
}
|
||||
try {
|
||||
Thread.sleep((long) Math.pow(2, attempts) * 100L);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Rate Limiting (Filter + Bucket4j)
|
||||
|
||||
**Security Note**: The `X-Forwarded-For` header is untrusted by default because clients can spoof it.
|
||||
Only use forwarded headers when:
|
||||
1. Your app is behind a trusted reverse proxy (nginx, AWS ALB, etc.)
|
||||
2. You have registered `ForwardedHeaderFilter` as a bean
|
||||
3. You have configured `server.forward-headers-strategy=NATIVE` or `FRAMEWORK` in application properties
|
||||
4. Your proxy is configured to overwrite (not append to) the `X-Forwarded-For` header
|
||||
|
||||
When `ForwardedHeaderFilter` is properly configured, `request.getRemoteAddr()` will automatically
|
||||
return the correct client IP from the forwarded headers. Without this configuration, use
|
||||
`request.getRemoteAddr()` directly—it returns the immediate connection IP, which is the only
|
||||
trustworthy value.
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class RateLimitFilter extends OncePerRequestFilter {
|
||||
private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();
|
||||
|
||||
/*
|
||||
* SECURITY: This filter uses request.getRemoteAddr() to identify clients for rate limiting.
|
||||
*
|
||||
* If your application is behind a reverse proxy (nginx, AWS ALB, etc.), you MUST configure
|
||||
* Spring to handle forwarded headers properly for accurate client IP detection:
|
||||
*
|
||||
* 1. Set server.forward-headers-strategy=NATIVE (for cloud platforms) or FRAMEWORK in
|
||||
* application.properties/yaml
|
||||
* 2. If using FRAMEWORK strategy, register ForwardedHeaderFilter:
|
||||
*
|
||||
* @Bean
|
||||
* ForwardedHeaderFilter forwardedHeaderFilter() {
|
||||
* return new ForwardedHeaderFilter();
|
||||
* }
|
||||
*
|
||||
* 3. Ensure your proxy overwrites (not appends) the X-Forwarded-For header to prevent spoofing
|
||||
* 4. Configure server.tomcat.remoteip.trusted-proxies or equivalent for your container
|
||||
*
|
||||
* Without this configuration, request.getRemoteAddr() returns the proxy IP, not the client IP.
|
||||
* Do NOT read X-Forwarded-For directly—it is trivially spoofable without trusted proxy handling.
|
||||
*/
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
// Use getRemoteAddr() which returns the correct client IP when ForwardedHeaderFilter
|
||||
// is configured, or the direct connection IP otherwise. Never trust X-Forwarded-For
|
||||
// headers directly without proper proxy configuration.
|
||||
String clientIp = request.getRemoteAddr();
|
||||
|
||||
Bucket bucket = buckets.computeIfAbsent(clientIp,
|
||||
k -> Bucket.builder()
|
||||
.addLimit(Bandwidth.classic(100, Refill.greedy(100, Duration.ofMinutes(1))))
|
||||
.build());
|
||||
|
||||
if (bucket.tryConsume(1)) {
|
||||
filterChain.doFilter(request, response);
|
||||
} else {
|
||||
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Background Jobs
|
||||
|
||||
Use Spring’s `@Scheduled` or integrate with queues (e.g., Kafka, SQS, RabbitMQ). Keep handlers idempotent and observable.
|
||||
|
||||
## Observability
|
||||
|
||||
- Structured logging (JSON) via Logback encoder
|
||||
- Metrics: Micrometer + Prometheus/OTel
|
||||
- Tracing: Micrometer Tracing with OpenTelemetry or Brave backend
|
||||
|
||||
## Production Defaults
|
||||
|
||||
- Prefer constructor injection, avoid field injection
|
||||
- Enable `spring.mvc.problemdetails.enabled=true` for RFC 7807 errors (Spring Boot 3+)
|
||||
- Configure HikariCP pool sizes for workload, set timeouts
|
||||
- Use `@Transactional(readOnly = true)` for queries
|
||||
- Enforce null-safety via `@NonNull` and `Optional` where appropriate
|
||||
|
||||
**Remember**: Keep controllers thin, services focused, repositories simple, and errors handled centrally. Optimize for maintainability and testability.
|
||||
119
skills/springboot-security/SKILL.md
Normal file
119
skills/springboot-security/SKILL.md
Normal file
@@ -0,0 +1,119 @@
|
||||
---
|
||||
name: springboot-security
|
||||
description: Spring Security best practices for authn/authz, validation, CSRF, secrets, headers, rate limiting, and dependency security in Java Spring Boot services.
|
||||
---
|
||||
|
||||
# Spring Boot Security Review
|
||||
|
||||
Use when adding auth, handling input, creating endpoints, or dealing with secrets.
|
||||
|
||||
## Authentication
|
||||
|
||||
- Prefer stateless JWT or opaque tokens with revocation list
|
||||
- Use `httpOnly`, `Secure`, `SameSite=Strict` cookies for sessions
|
||||
- Validate tokens with `OncePerRequestFilter` or resource server
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class JwtAuthFilter extends OncePerRequestFilter {
|
||||
private final JwtService jwtService;
|
||||
|
||||
public JwtAuthFilter(JwtService jwtService) {
|
||||
this.jwtService = jwtService;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain chain) throws ServletException, IOException {
|
||||
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (header != null && header.startsWith("Bearer ")) {
|
||||
String token = header.substring(7);
|
||||
Authentication auth = jwtService.authenticate(token);
|
||||
SecurityContextHolder.getContext().setAuthentication(auth);
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Authorization
|
||||
|
||||
- Enable method security: `@EnableMethodSecurity`
|
||||
- Use `@PreAuthorize("hasRole('ADMIN')")` or `@PreAuthorize("@authz.canEdit(#id)")`
|
||||
- Deny by default; expose only required scopes
|
||||
|
||||
## Input Validation
|
||||
|
||||
- Use Bean Validation with `@Valid` on controllers
|
||||
- Apply constraints on DTOs: `@NotBlank`, `@Email`, `@Size`, custom validators
|
||||
- Sanitize any HTML with a whitelist before rendering
|
||||
|
||||
## SQL Injection Prevention
|
||||
|
||||
- Use Spring Data repositories or parameterized queries
|
||||
- For native queries, use `:param` bindings; never concatenate strings
|
||||
|
||||
## CSRF Protection
|
||||
|
||||
- For browser session apps, keep CSRF enabled; include token in forms/headers
|
||||
- For pure APIs with Bearer tokens, disable CSRF and rely on stateless auth
|
||||
|
||||
```java
|
||||
http
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
|
||||
```
|
||||
|
||||
## Secrets Management
|
||||
|
||||
- No secrets in source; load from env or vault
|
||||
- Keep `application.yml` free of credentials; use placeholders
|
||||
- Rotate tokens and DB credentials regularly
|
||||
|
||||
## Security Headers
|
||||
|
||||
```java
|
||||
http
|
||||
.headers(headers -> headers
|
||||
.contentSecurityPolicy(csp -> csp
|
||||
.policyDirectives("default-src 'self'"))
|
||||
.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)
|
||||
.xssProtection(Customizer.withDefaults())
|
||||
.referrerPolicy(rp -> rp.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.NO_REFERRER)));
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
- Apply Bucket4j or gateway-level limits on expensive endpoints
|
||||
- Log and alert on bursts; return 429 with retry hints
|
||||
|
||||
## Dependency Security
|
||||
|
||||
- Run OWASP Dependency Check / Snyk in CI
|
||||
- Keep Spring Boot and Spring Security on supported versions
|
||||
- Fail builds on known CVEs
|
||||
|
||||
## Logging and PII
|
||||
|
||||
- Never log secrets, tokens, passwords, or full PAN data
|
||||
- Redact sensitive fields; use structured JSON logging
|
||||
|
||||
## File Uploads
|
||||
|
||||
- Validate size, content type, and extension
|
||||
- Store outside web root; scan if required
|
||||
|
||||
## Checklist Before Release
|
||||
|
||||
- [ ] Auth tokens validated and expired correctly
|
||||
- [ ] Authorization guards on every sensitive path
|
||||
- [ ] All inputs validated and sanitized
|
||||
- [ ] No string-concatenated SQL
|
||||
- [ ] CSRF posture correct for app type
|
||||
- [ ] Secrets externalized; none committed
|
||||
- [ ] Security headers configured
|
||||
- [ ] Rate limiting on APIs
|
||||
- [ ] Dependencies scanned and up to date
|
||||
- [ ] Logs free of sensitive data
|
||||
|
||||
**Remember**: Deny by default, validate inputs, least privilege, and secure-by-configuration first.
|
||||
157
skills/springboot-tdd/SKILL.md
Normal file
157
skills/springboot-tdd/SKILL.md
Normal file
@@ -0,0 +1,157 @@
|
||||
---
|
||||
name: springboot-tdd
|
||||
description: Test-driven development for Spring Boot using JUnit 5, Mockito, MockMvc, Testcontainers, and JaCoCo. Use when adding features, fixing bugs, or refactoring.
|
||||
---
|
||||
|
||||
# Spring Boot TDD Workflow
|
||||
|
||||
TDD guidance for Spring Boot services with 80%+ coverage (unit + integration).
|
||||
|
||||
## When to Use
|
||||
|
||||
- New features or endpoints
|
||||
- Bug fixes or refactors
|
||||
- Adding data access logic or security rules
|
||||
|
||||
## Workflow
|
||||
|
||||
1) Write tests first (they should fail)
|
||||
2) Implement minimal code to pass
|
||||
3) Refactor with tests green
|
||||
4) Enforce coverage (JaCoCo)
|
||||
|
||||
## Unit Tests (JUnit 5 + Mockito)
|
||||
|
||||
```java
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MarketServiceTest {
|
||||
@Mock MarketRepository repo;
|
||||
@InjectMocks MarketService service;
|
||||
|
||||
@Test
|
||||
void createsMarket() {
|
||||
CreateMarketRequest req = new CreateMarketRequest("name", "desc", Instant.now(), List.of("cat"));
|
||||
when(repo.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
Market result = service.create(req);
|
||||
|
||||
assertThat(result.name()).isEqualTo("name");
|
||||
verify(repo).save(any());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Patterns:
|
||||
- Arrange-Act-Assert
|
||||
- Avoid partial mocks; prefer explicit stubbing
|
||||
- Use `@ParameterizedTest` for variants
|
||||
|
||||
## Web Layer Tests (MockMvc)
|
||||
|
||||
```java
|
||||
@WebMvcTest(MarketController.class)
|
||||
class MarketControllerTest {
|
||||
@Autowired MockMvc mockMvc;
|
||||
@MockBean MarketService marketService;
|
||||
|
||||
@Test
|
||||
void returnsMarkets() throws Exception {
|
||||
when(marketService.list(any())).thenReturn(Page.empty());
|
||||
|
||||
mockMvc.perform(get("/api/markets"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.content").isArray());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Integration Tests (SpringBootTest)
|
||||
|
||||
```java
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
class MarketIntegrationTest {
|
||||
@Autowired MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
void createsMarket() throws Exception {
|
||||
mockMvc.perform(post("/api/markets")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"name":"Test","description":"Desc","endDate":"2030-01-01T00:00:00Z","categories":["general"]}
|
||||
"""))
|
||||
.andExpect(status().isCreated());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Persistence Tests (DataJpaTest)
|
||||
|
||||
```java
|
||||
@DataJpaTest
|
||||
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
|
||||
@Import(TestContainersConfig.class)
|
||||
class MarketRepositoryTest {
|
||||
@Autowired MarketRepository repo;
|
||||
|
||||
@Test
|
||||
void savesAndFinds() {
|
||||
MarketEntity entity = new MarketEntity();
|
||||
entity.setName("Test");
|
||||
repo.save(entity);
|
||||
|
||||
Optional<MarketEntity> found = repo.findByName("Test");
|
||||
assertThat(found).isPresent();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testcontainers
|
||||
|
||||
- Use reusable containers for Postgres/Redis to mirror production
|
||||
- Wire via `@DynamicPropertySource` to inject JDBC URLs into Spring context
|
||||
|
||||
## Coverage (JaCoCo)
|
||||
|
||||
Maven snippet:
|
||||
```xml
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>0.8.14</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals><goal>prepare-agent</goal></goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>report</id>
|
||||
<phase>verify</phase>
|
||||
<goals><goal>report</goal></goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
```
|
||||
|
||||
## Assertions
|
||||
|
||||
- Prefer AssertJ (`assertThat`) for readability
|
||||
- For JSON responses, use `jsonPath`
|
||||
- For exceptions: `assertThatThrownBy(...)`
|
||||
|
||||
## Test Data Builders
|
||||
|
||||
```java
|
||||
class MarketBuilder {
|
||||
private String name = "Test";
|
||||
MarketBuilder withName(String name) { this.name = name; return this; }
|
||||
Market build() { return new Market(null, name, MarketStatus.ACTIVE); }
|
||||
}
|
||||
```
|
||||
|
||||
## CI Commands
|
||||
|
||||
- Maven: `mvn -T 4 test` or `mvn verify`
|
||||
- Gradle: `./gradlew test jacocoTestReport`
|
||||
|
||||
**Remember**: Keep tests fast, isolated, and deterministic. Test behavior, not implementation details.
|
||||
100
skills/springboot-verification/SKILL.md
Normal file
100
skills/springboot-verification/SKILL.md
Normal file
@@ -0,0 +1,100 @@
|
||||
---
|
||||
name: springboot-verification
|
||||
description: Verification loop for Spring Boot projects: build, static analysis, tests with coverage, security scans, and diff review before release or PR.
|
||||
---
|
||||
|
||||
# Spring Boot Verification Loop
|
||||
|
||||
Run before PRs, after major changes, and pre-deploy.
|
||||
|
||||
## Phase 1: Build
|
||||
|
||||
```bash
|
||||
mvn -T 4 clean verify -DskipTests
|
||||
# or
|
||||
./gradlew clean assemble -x test
|
||||
```
|
||||
|
||||
If build fails, stop and fix.
|
||||
|
||||
## Phase 2: Static Analysis
|
||||
|
||||
Maven (common plugins):
|
||||
```bash
|
||||
mvn -T 4 spotbugs:check pmd:check checkstyle:check
|
||||
```
|
||||
|
||||
Gradle (if configured):
|
||||
```bash
|
||||
./gradlew checkstyleMain pmdMain spotbugsMain
|
||||
```
|
||||
|
||||
## Phase 3: Tests + Coverage
|
||||
|
||||
```bash
|
||||
mvn -T 4 test
|
||||
mvn jacoco:report # verify 80%+ coverage
|
||||
# or
|
||||
./gradlew test jacocoTestReport
|
||||
```
|
||||
|
||||
Report:
|
||||
- Total tests, passed/failed
|
||||
- Coverage % (lines/branches)
|
||||
|
||||
## Phase 4: Security Scan
|
||||
|
||||
```bash
|
||||
# Dependency CVEs
|
||||
mvn org.owasp:dependency-check-maven:check
|
||||
# or
|
||||
./gradlew dependencyCheckAnalyze
|
||||
|
||||
# Secrets (git)
|
||||
git secrets --scan # if configured
|
||||
```
|
||||
|
||||
## Phase 5: Lint/Format (optional gate)
|
||||
|
||||
```bash
|
||||
mvn spotless:apply # if using Spotless plugin
|
||||
./gradlew spotlessApply
|
||||
```
|
||||
|
||||
## Phase 6: Diff Review
|
||||
|
||||
```bash
|
||||
git diff --stat
|
||||
git diff
|
||||
```
|
||||
|
||||
Checklist:
|
||||
- No debugging logs left (`System.out`, `log.debug` without guards)
|
||||
- Meaningful errors and HTTP statuses
|
||||
- Transactions and validation present where needed
|
||||
- Config changes documented
|
||||
|
||||
## Output Template
|
||||
|
||||
```
|
||||
VERIFICATION REPORT
|
||||
===================
|
||||
Build: [PASS/FAIL]
|
||||
Static: [PASS/FAIL] (spotbugs/pmd/checkstyle)
|
||||
Tests: [PASS/FAIL] (X/Y passed, Z% coverage)
|
||||
Security: [PASS/FAIL] (CVE findings: N)
|
||||
Diff: [X files changed]
|
||||
|
||||
Overall: [READY / NOT READY]
|
||||
|
||||
Issues to Fix:
|
||||
1. ...
|
||||
2. ...
|
||||
```
|
||||
|
||||
## Continuous Mode
|
||||
|
||||
- Re-run phases on significant changes or every 30–60 minutes in long sessions
|
||||
- Keep a short loop: `mvn -T 4 test` + spotbugs for quick feedback
|
||||
|
||||
**Remember**: Fast feedback beats late surprises. Keep the gate strict—treat warnings as defects in production systems.
|
||||
Reference in New Issue
Block a user