Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Spring Boot architecture patterns for REST APIs, layered services, JPA, caching, async, and exception handling.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
SKILL.md
1---2name: springboot-patterns3description: Spring Boot architecture patterns, REST API design, layered services, data access, caching, async processing, and logging. Use for Java Spring Boot backend work.4---56# Spring Boot 開発パターン78スケーラブルで本番グレードのサービスのためのSpring BootアーキテクチャとAPIパターン。910## REST API構造1112```java13@RestController14@RequestMapping("/api/markets")15@Validated16class MarketController {17private final MarketService marketService;1819MarketController(MarketService marketService) {20this.marketService = marketService;21}2223@GetMapping24ResponseEntity<Page<MarketResponse>> list(25@RequestParam(defaultValue = "0") int page,26@RequestParam(defaultValue = "20") int size) {27Page<Market> markets = marketService.list(PageRequest.of(page, size));28return ResponseEntity.ok(markets.map(MarketResponse::from));29}3031@PostMapping32ResponseEntity<MarketResponse> create(@Valid @RequestBody CreateMarketRequest request) {33Market market = marketService.create(request);34return ResponseEntity.status(HttpStatus.CREATED).body(MarketResponse::from(market));35}36}37```3839## リポジトリパターン(Spring Data JPA)4041```java42public interface MarketRepository extends JpaRepository<MarketEntity, Long> {43@Query("select m from MarketEntity m where m.status = :status order by m.volume desc")44List<MarketEntity> findActive(@Param("status") MarketStatus status, Pageable pageable);45}46```4748## トランザクション付きサービスレイヤー4950```java51@Service52public class MarketService {53private final MarketRepository repo;5455public MarketService(MarketRepository repo) {56this.repo = repo;57}5859@Transactional60public Market create(CreateMarketRequest request) {61MarketEntity entity = MarketEntity.from(request);62MarketEntity saved = repo.save(entity);63return Market.from(saved);64}65}66```6768## DTOと検証6970```java71public record CreateMarketRequest(72@NotBlank @Size(max = 200) String name,73@NotBlank @Size(max = 2000) String description,74@NotNull @FutureOrPresent Instant endDate,75@NotEmpty List<@NotBlank String> categories) {}7677public record MarketResponse(Long id, String name, MarketStatus status) {78static MarketResponse from(Market market) {79return new MarketResponse(market.id(), market.name(), market.status());80}81}82```8384## 例外ハンドリング8586```java87@ControllerAdvice88class GlobalExceptionHandler {89@ExceptionHandler(MethodArgumentNotValidException.class)90ResponseEntity<ApiError> handleValidation(MethodArgumentNotValidException ex) {91String message = ex.getBindingResult().getFieldErrors().stream()92.map(e -> e.getField() + ": " + e.getDefaultMessage())93.collect(Collectors.joining(", "));94return ResponseEntity.badRequest().body(ApiError.validation(message));95}9697@ExceptionHandler(AccessDeniedException.class)98ResponseEntity<ApiError> handleAccessDenied() {99return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ApiError.of("Forbidden"));100}101102@ExceptionHandler(Exception.class)103ResponseEntity<ApiError> handleGeneric(Exception ex) {104// スタックトレース付きで予期しないエラーをログ105return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)106.body(ApiError.of("Internal server error"));107}108}109```110111## キャッシング112113構成クラスで`@EnableCaching`が必要です。114115```java116@Service117public class MarketCacheService {118private final MarketRepository repo;119120public MarketCacheService(MarketRepository repo) {121this.repo = repo;122}123124@Cacheable(value = "market", key = "#id")125public Market getById(Long id) {126return repo.findById(id)127.map(Market::from)128.orElseThrow(() -> new EntityNotFoundException("Market not found"));129}130131@CacheEvict(value = "market", key = "#id")132public void evict(Long id) {}133}134```135136## 非同期処理137138構成クラスで`@EnableAsync`が必要です。139140```java141@Service142public class NotificationService {143@Async144public CompletableFuture<Void> sendAsync(Notification notification) {145// メール/SMS送信146return CompletableFuture.completedFuture(null);147}148}149```150151## ロギング(SLF4J)152153```java154@Service155public class ReportService {156private static final Logger log = LoggerFactory.getLogger(ReportService.class);157158public Report generate(Long marketId) {159log.info("generate_report marketId={}", marketId);160try {161// ロジック162} catch (Exception ex) {163log.error("generate_report_failed marketId={}", marketId, ex);164throw ex;165}166return new Report();167}168}169```170171## ミドルウェア / フィルター172173```java174@Component175public class RequestLoggingFilter extends OncePerRequestFilter {176private static final Logger log = LoggerFactory.getLogger(RequestLoggingFilter.class);177178@Override179protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,180FilterChain filterChain) throws ServletException, IOException {181long start = System.currentTimeMillis();182try {183filterChain.doFilter(request, response);184} finally {185long duration = System.currentTimeMillis() - start;186log.info("req method={} uri={} status={} durationMs={}",187request.getMethod(), request.getRequestURI(), response.getStatus(), duration);188}189}190}191```192193## ページネーションとソート194195```java196PageRequest page = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending());197Page<Market> results = marketService.list(page);198```199200## エラー回復力のある外部呼び出し201202```java203public <T> T withRetry(Supplier<T> supplier, int maxRetries) {204int attempts = 0;205while (true) {206try {207return supplier.get();208} catch (Exception ex) {209attempts++;210if (attempts >= maxRetries) {211throw ex;212}213try {214Thread.sleep((long) Math.pow(2, attempts) * 100L);215} catch (InterruptedException ie) {216Thread.currentThread().interrupt();217throw ex;218}219}220}221}222```223224## レート制限(Filter + Bucket4j)225226**セキュリティノート**: `X-Forwarded-For`ヘッダーはデフォルトでは信頼できません。クライアントがそれを偽装できるためです。227転送ヘッダーは次の場合のみ使用してください:2281. アプリが信頼できるリバースプロキシ(nginx、AWS ALBなど)の背後にある2292. `ForwardedHeaderFilter`をBeanとして登録済み2303. application propertiesで`server.forward-headers-strategy=NATIVE`または`FRAMEWORK`を設定済み2314. プロキシが`X-Forwarded-For`ヘッダーを上書き(追加ではなく)するよう設定済み232233`ForwardedHeaderFilter`が適切に構成されている場合、`request.getRemoteAddr()`は転送ヘッダーから正しいクライアントIPを自動的に返します。この構成がない場合は、`request.getRemoteAddr()`を直接使用してください。これは直接接続IPを返し、唯一信頼できる値です。234235```java236@Component237public class RateLimitFilter extends OncePerRequestFilter {238private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();239240/*241* セキュリティ: このフィルターはレート制限のためにクライアントを識別するために242* request.getRemoteAddr()を使用します。243*244* アプリケーションがリバースプロキシ(nginx、AWS ALBなど)の背後にある場合、245* 正確なクライアントIP検出のために転送ヘッダーを適切に処理するようSpringを246* 設定する必要があります:247*248* 1. application.properties/yamlで server.forward-headers-strategy=NATIVE249* (クラウドプラットフォーム用)またはFRAMEWORKを設定250* 2. FRAMEWORK戦略を使用する場合、ForwardedHeaderFilterを登録:251*252* @Bean253* ForwardedHeaderFilter forwardedHeaderFilter() {254* return new ForwardedHeaderFilter();255* }256*257* 3. プロキシが偽装を防ぐためにX-Forwarded-Forヘッダーを上書き(追加ではなく)258* することを確認259* 4. コンテナに応じてserver.tomcat.remoteip.trusted-proxiesまたは同等を設定260*261* この構成なしでは、request.getRemoteAddr()はクライアントIPではなくプロキシIPを返します。262* X-Forwarded-Forを直接読み取らないでください。信頼できるプロキシ処理なしでは簡単に偽装できます。263*/264@Override265protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,266FilterChain filterChain) throws ServletException, IOException {267// ForwardedHeaderFilterが構成されている場合は正しいクライアントIPを返す268// getRemoteAddr()を使用。そうでなければ直接接続IPを返す。269// X-Forwarded-Forヘッダーを適切なプロキシ構成なしで直接信頼しない。270String clientIp = request.getRemoteAddr();271272Bucket bucket = buckets.computeIfAbsent(clientIp,273k -> Bucket.builder()274.addLimit(Bandwidth.classic(100, Refill.greedy(100, Duration.ofMinutes(1))))275.build());276277if (bucket.tryConsume(1)) {278filterChain.doFilter(request, response);279} else {280response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());281}282}283}284```285286## バックグラウンドジョブ287288Springの`@Scheduled`を使用するか、キュー(Kafka、SQS、RabbitMQなど)と統合します。ハンドラーをべき等かつ観測可能に保ちます。289290## 可観測性291292- 構造化ロギング(JSON)via Logbackエンコーダー293- メトリクス: Micrometer + Prometheus/OTel294- トレーシング: Micrometer TracingとOpenTelemetryまたはBraveバックエンド295296## 本番デフォルト297298- コンストラクタインジェクションを優先、フィールドインジェクションを避ける299- RFC 7807エラーのために`spring.mvc.problemdetails.enabled=true`を有効化(Spring Boot 3+)300- ワークロードに応じてHikariCPプールサイズを構成、タイムアウトを設定301- クエリに`@Transactional(readOnly = true)`を使用302- `@NonNull`と`Optional`で適切にnull安全性を強制303304**覚えておいてください**: コントローラーは薄く、サービスは焦点を絞り、リポジトリはシンプルに、エラーは集中的に処理します。保守性とテスト可能性のために最適化してください。305