mcpField
S
spring-boot-development

spring-boot-development

github.com/manutej開発62534 回インストール40 週間前に更新
リポジトリ

spring-boot-development について

spring-boot-developmentは、Spring Bootアプリ開発を扱うスキルです。自動設定、依存性注入、REST API、データアクセス、セキュリティを網羅し、企業向けバックエンドやマイクロサービスの開発者を対象とします。

主な機能

  1. 01自動設定と依存性注入
  2. 02Spring MVCによるREST API
  3. 03Spring Data JPAのデータアクセス
  4. 04アプリケーションと環境の設定
  5. 05Spring Securityの認証・認可
  6. 06テストと開発の実践方法

活用シーン

  1. 01企業向けREST APIの構築
  2. 02データベース連携アプリの開発
  3. 03認証と認可の設定
  4. 04サービスの単体テストと統合テスト

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/manutej/luxor-claude-marketplace --skill spring-boot-development

インストール後、Claude Code で/spring-boot-developmentを入力して読み込まれたか確認します。

同梱ファイルEXAMPLES.mdREADME.md

詳細

Spring Boot 開発スキル

このスキルでは、公式の Spring Boot ドキュメントに基づき、自動構成、依存性注入、REST API、Spring Data、Spring Security、エンタープライズ Java のパターンを使用して、最新の Spring Boot アプリケーションを構築するための包括的なガイダンスを提供します。

このスキルを使用する場面

次のような場合に、このスキルを使用してください。

  • エンタープライズ向け REST API やマイクロサービスを構築する
  • Spring MVC を使用して Web アプリケーションを作成する
  • JPA とデータベースを使用してデータ駆動型アプリケーションを開発する
  • Spring Security を使用して認証と認可を実装する
  • Actuator とモニタリングを使用して、本番環境に対応したアプリケーションを構築する
  • Spring Boot を使用してスケーラブルなバックエンドサービスを作成する
  • 従来の Spring から Spring Boot に移行する
  • クラウドネイティブアプリケーションを開発する
  • メッセージングを使用してイベント駆動型システムを構築する
  • バッチ処理アプリケーションを作成する

コアコンセプト

自動構成

Spring Boot は、プロジェクトに追加された依存関係に基づいてアプリケーションを自動的に構成します。これにより、定型的な構成を大幅に削減できます。

自動構成の仕組み:

@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

@SpringBootApplication アノテーションは、次のアノテーションを組み合わせたものです。

  • @Configuration:クラスを Bean 定義のソースとして指定します
  • @EnableAutoConfiguration:Spring Boot の自動構成メカニズムを有効にします
  • @ComponentScan:現在のパッケージとサブパッケージでコンポーネントスキャンを有効にします

条件付き自動構成:

@Configuration
@ConditionalOnClass(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.url")
public class DataSourceAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }
}

自動構成のカスタマイズ:

// Exclude specific auto-configurations
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class MyApplication {
    // ...
}

// Or in application.properties
// spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
依存性注入

Spring の IoC(制御の反転)コンテナーは、オブジェクトの生成と依存性注入を管理します。

コンストラクターインジェクション(推奨):

@Service
public class UserService {

    private final UserRepository userRepository;
    private final EmailService emailService;

    // Constructor injection - recommended approach
    public UserService(UserRepository userRepository, EmailService emailService) {
        this.userRepository = userRepository;
        this.emailService = emailService;
    }

    public User createUser(User user) {
        User saved = userRepository.save(user);
        emailService.sendWelcomeEmail(saved);
        return saved;
    }
}

フィールドインジェクション(非推奨):

@Service
public class UserService {

    @Autowired  // Avoid field injection
    private UserRepository userRepository;

    // Difficult to test and creates tight coupling
}

セッターインジェクション(オプションの依存関係):

@Service
public class UserService {

    private UserRepository userRepository;
    private EmailService emailService;

    @Autowired
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Autowired(required = false)
    public void setEmailService(EmailService emailService) {
        this.emailService = emailService;
    }
}

コンポーネントのステレオタイプ:

@Component  // Generic component
public class MyComponent { }

@Service    // Business logic layer
public class MyService { }

@Repository // Data access layer
public class MyRepository { }

@Controller // Presentation layer (web)
public class MyController { }

@RestController // REST API controller
public class MyRestController { }
Spring Web(REST API)

Spring MVC のアノテーションを使用して、RESTful Web サービスを構築します。

基本的な REST コントローラー:

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping
    public List<User> getAllUsers() {
        return userService.findAll();
    }

    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        return userService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    public ResponseEntity<User> createUser(@RequestBody @Valid User user) {
        User created = userService.save(user);
        URI location = ServletUriComponentsBuilder
            .fromCurrentRequest()
            .path("/{id}")
            .buildAndExpand(created.getId())
            .toUri();
        return ResponseEntity.created(location).body(created);
    }

    @PutMapping("/{id}")
    public ResponseEntity<User> updateUser(@PathVariable Long id,
                                          @RequestBody @Valid User user) {
        return userService.update(id, user)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        if (userService.delete(id)) {
            return ResponseEntity.noContent().build();
        }
        return ResponseEntity.notFound().build();
    }
}

リクエストマッピングのバリエーション:

@RestController
@RequestMapping("/api/products")
public class ProductController {

    // Query parameters
    @GetMapping("/search")
    public List<Product> search(@RequestParam String name,
                               @RequestParam(required = false) String category) {
        return productService.search(name, category);
    }

    // Multiple path variables
    @GetMapping("/categories/{categoryId}/products/{productId}")
    public Product getProductInCategory(@PathVariable Long categoryId,
                                       @PathVariable Long productId) {
        return productService.findInCategory(categoryId, productId);
    }

    // Request headers
    @GetMapping("/{id}")
    public Product getProduct(@PathVariable Long id,
                             @RequestHeader("Accept-Language") String language) {
        return productService.find(id, language);
    }

    // Matrix variables
    @GetMapping("/{id}")
    public Product getProductWithMatrix(@PathVariable Long id,
                                       @MatrixVariable Map<String, String> filters) {
        return productService.findWithFilters(id, filters);
    }
}

レスポンスの処理:

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    // Return different status codes
    @PostMapping
    public ResponseEntity<Order> createOrder(@RequestBody Order order) {
        Order created = orderService.create(order);
        return ResponseEntity.status(HttpStatus.CREATED).body(created);
    }

    // Custom headers
    @GetMapping("/{id}")
    public ResponseEntity<Order> getOrder(@PathVariable Long id) {
        Order order = orderService.findById(id);
        return ResponseEntity.ok()
            .header("X-Order-Version", order.getVersion().toString())
            .body(order);
    }

    // No content response
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteOrder(@PathVariable Long id) {
        orderService.delete(id);
        return ResponseEntity.noContent().build();
    }
}
Spring Data JPA

Spring Data JPA は、データベースアクセスのためのリポジトリー抽象化を提供します。

エンティティの定義:

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private String name;

    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @Column(name = "updated_at")
    private LocalDateTime updatedAt;

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Order> orders = new ArrayList<>();

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "department_id")
    private Department department;

    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
        updatedAt = LocalDateTime.now();
    }

    @PreUpdate
    protected void onUpdate() {
        updatedAt = LocalDateTime.now();
    }

    // Getters and setters
}

リポジトリーインターフェース:

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    // Query method - Spring Data generates implementation
    Optional<User> findByEmail(String email);

    List<User> findByNameContaining(String name);

    List<User> findByDepartmentId(Long departmentId);

    // Custom JPQL query
    @Query("SELECT u FROM User u WHERE u.email = ?1")
    Optional<User> findByEmailQuery(String email);

    // Named parameters
    @Query("SELECT u FROM User u WHERE u.name LIKE %:name% AND u.department.id = :deptId")
    List<User> searchByNameAndDepartment(@Param("name") String name,
                                        @Param("deptId") Long deptId);

    // Native SQL query
    @Query(value = "SELECT * FROM users WHERE email = ?1", nativeQuery = true)
    Optional<User> findByEmailNative(String email);

    // Modifying query
    @Modifying
    @Query("UPDATE User u SET u.name = :name WHERE u.id = :id")
    int updateUserName(@Param("id") Long id, @Param("name") String name);

    // Pagination and sorting
    Page<User> findByDepartmentId(Long departmentId, Pageable pageable);

    List<User> findByNameContaining(String name, Sort sort);
}

リポジトリーの使用:

@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public Optional<User> findById(Long id) {
        return userRepository.findById(id);
    }

    public User save(User user) {
        return userRepository.save(user);
    }

    public List<User> findAll() {
        return userRepository.findAll();
    }

    public Page<User> findAll(int page, int size) {
        Pageable pageable = PageRequest.of(page, size, Sort.by("name"));
        return userRepository.findAll(pageable);
    }

    public boolean delete(Long id) {
        if (userRepository.existsById(id)) {
            userRepository.deleteById(id);
            return true;
        }
        return false;
    }
}

リレーションシップ:

// One-to-Many
@Entity
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();
}

// Many-to-Many
@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToMany
    @JoinTable(
        name = "student_course",
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    private Set<Course> courses = new HashSet<>();
}
構成

Spring Boot では、構成に application.properties または application.yml を使用します。

アプリケーションプロパティ:

# Server configuration
server.port=8080
server.servlet.context-path=/api

# Database configuration
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=user
spring.datasource.password=password
spring.datasource.driver-class-name=org.postgresql.Driver

# JPA configuration
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

# Logging
logging.level.root=INFO
logging.level.com.example=DEBUG
logging.level.org.springframework.web=DEBUG
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n

# Custom properties
app.name=My Application
app.version=1.0.0

アプリケーション YAML:

server:
  port: 8080
  servlet:
    context-path: /api

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: user
    password: password
    driver-class-name: org.postgresql.Driver

  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        dialect: org.hibernate.dialect.PostgreSQLDialect

logging:
  level:
    root: INFO
    com.example: DEBUG
    org.springframework.web: DEBUG

app:
  name: My Application
  version: 1.0.0

構成プロパティクラス:

@Configuration
@ConfigurationProperties(prefix = "app")
public class AppConfig {

    private String name;
    private String version;
    private Security security = new Security();

    public static class Security {
        private int tokenExpiration = 3600;
        private String secretKey;

        // Getters and setters
    }

    // Getters and setters
}

// Usage
@Service
public class MyService {

    private final AppConfig appConfig;

    public MyService(AppConfig appConfig) {
        this.appConfig = appConfig;
    }

    public void printConfig() {
        System.out.println("App: " + appConfig.getName());
        System.out.println("Version: " + appConfig.getVersion());
    }
}

環境固有の構成:

# application.properties (default)
spring.profiles.active=dev

# application-dev.properties
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb_dev
logging.level.root=DEBUG

# application-prod.properties
spring.datasource.url=jdbc:postgresql://prod-server:5432/mydb_prod
logging.level.root=WARN

プロファイル固有の Bean:

@Configuration
public class DatabaseConfig {

    @Bean
    @Profile("dev")
    public DataSource devDataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .build();
    }

    @Bean
    @Profile("prod")
    public DataSource prodDataSource() {
        return DataSourceBuilder.create().build();
    }
}
Spring Security

アプリケーションに認証と認可を実装します。

基本的なセキュリティ構成:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers("/api/users/**").hasAnyRole("USER", "ADMIN")
                .anyRequest().authenticated()
            )
            .httpBasic();

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

インメモリー認証:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
        UserDetails user = User.builder()
            .username("user")
            .password(passwordEncoder.encode("password"))
            .roles("USER")
            .build();

        UserDetails admin = User.builder()
            .username("admin")
            .password(passwordEncoder.encode("admin"))
            .roles("ADMIN", "USER")
            .build();

        return new InMemoryUserDetailsManager(user, admin);
    }
}

データベース認証:

@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByEmail(username)
            .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));

        return org.springframework.security.core.userdetails.User.builder()
            .username(user.getEmail())
            .password(user.getPassword())
            .roles(user.getRoles().toArray(new String[0]))
            .build();
    }
}

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final CustomUserDetailsService userDetailsService;

    public SecurityConfig(CustomUserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider(PasswordEncoder passwordEncoder) {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder);
        return provider;
    }
}

JWT 認証:

@Component
public class JwtTokenProvider {

    @Value("${app.security.jwt.secret}")
    private String jwtSecret;

    @Value("${app.security.jwt.expiration}")
    private int jwtExpiration;

    public String generateToken(Authentication authentication) {
        UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();

        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtExpiration);

        return Jwts.builder()
            .setSubject(Long.toString(userPrincipal.getId()))
            .setIssuedAt(now)
            .setExpiration(expiryDate)
            .signWith(SignatureAlgorithm.HS512, jwtSecret)
            .compact();
    }

    public Long getUserIdFromJWT(String token) {
        Claims claims = Jwts.parser()
            .setSigningKey(jwtSecret)
            .parseClaimsJws(token)
            .getBody();

        return Long.parseLong(claims.getSubject());
    }

    public boolean validateToken(String authToken) {
        try {
            Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
            return true;
        } catch (SignatureException | MalformedJwtException | ExpiredJwtException |
                 UnsupportedJwtException | IllegalArgumentException ex) {
            return false;
        }
    }
}

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenProvider tokenProvider;
    private final CustomUserDetailsService customUserDetailsService;

    public JwtAuthenticationFilter(JwtTokenProvider tokenProvider,
                                  CustomUserDetailsService customUserDetailsService) {
        this.tokenProvider = tokenProvider;
        this.customUserDetailsService = customUserDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                   HttpServletResponse response,
                                   FilterChain filterChain) throws ServletException, IOException {
        try {
            String jwt = getJwtFromRequest(request);

            if (jwt != null && tokenProvider.validateToken(jwt)) {
                Long userId = tokenProvider.getUserIdFromJWT(jwt);

                UserDetails userDetails = customUserDetailsService.loadUserById(userId);
                UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities()
                    );

                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception ex) {
            logger.error("Could not set user authentication", ex);
        }

        filterChain.doFilter(request, response);
    }

    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

API リファレンス

よく使うアノテーション

Spring の基本アノテーション:

  • @SpringBootApplication:メインアプリケーションクラス
  • @Component:汎用コンポーネント
  • @Service:サービス層コンポーネント
  • @Repository:データアクセス層コンポーネント
  • @Configuration:設定クラス
  • @Bean:Bean 定義メソッド
  • @Autowired:依存性注入
  • @Value:プロパティ値を注入
  • @Profile:プロファイルに基づく条件付き Bean

Web アノテーション:

  • @RestController:REST API コントローラー
  • @Controller:MVC コントローラー
  • @RequestMapping:HTTP リクエストをマッピング
  • @GetMapping:GET リクエストをマッピング
  • @PostMapping:POST リクエストをマッピング
  • @PutMapping:PUT リクエストをマッピング
  • @DeleteMapping:DELETE リクエストをマッピング
  • @PatchMapping:PATCH リクエストをマッピング
  • @PathVariable:パス変数を抽出
  • @RequestParam:クエリパラメーターを抽出
  • @RequestBody:リクエストボディを抽出
  • @RequestHeader:リクエストヘッダーを抽出
  • @ResponseStatus:レスポンスステータスを設定

データアノテーション:

  • @Entity:JPA エンティティ
  • @Table:テーブルマッピング
  • @Id:主キー
  • @GeneratedValue:自動生成値
  • @Column:カラムマッピング
  • @OneToOne:1 対 1 のリレーションシップ
  • @OneToMany:1 対多のリレーションシップ
  • @ManyToOne:多対 1 のリレーションシップ
  • @ManyToMany:多対多のリレーションシップ
  • @JoinColumn:結合カラム
  • @JoinTable:結合テーブル

バリデーションアノテーション:

  • @Valid:バリデーションを有効化
  • @NotNull:フィールドを null にできないようにする
  • @NotEmpty:フィールドを空にできないようにする
  • @NotBlank:フィールドを空白のみにできないようにする
  • @Size:文字列またはコレクションのサイズ
  • @Min:最小値
  • @Max:最大値
  • @Email:メール形式
  • @Pattern:正規表現パターン

トランザクションアノテーション:

  • @Transactional:トランザクション管理を有効化
  • @Transactional(readOnly = true):読み取り専用トランザクション

セキュリティアノテーション:

  • @EnableWebSecurity:セキュリティを有効化
  • @PreAuthorize:メソッドレベルの認可
  • @PostAuthorize:メソッド実行後の認可
  • @Secured:ロールベースのアクセス制御

非同期処理とスケジューリング:

  • @EnableAsync:非同期処理を有効化
  • @Async:非同期メソッド
  • @EnableScheduling:スケジューリングを有効化
  • @Scheduled:スケジュール実行メソッド

ワークフローパターン

REST API 設計パターン

完全な CRUD REST API:

// Entity
@Entity
@Table(name = "products")
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank(message = "Name is required")
    private String name;

    @NotBlank(message = "Description is required")
    private String description;

    @NotNull(message = "Price is required")
    @Min(value = 0, message = "Price must be positive")
    private BigDecimal price;

    @NotNull(message = "Stock is required")
    @Min(value = 0, message = "Stock must be positive")
    private Integer stock;

    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;

    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
        updatedAt = LocalDateTime.now();
    }

    @PreUpdate
    protected void onUpdate() {
        updatedAt = LocalDateTime.now();
    }

    // Getters and setters
}

// Repository
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
    List<Product> findByNameContaining(String name);
    List<Product> findByPriceBetween(BigDecimal minPrice, BigDecimal maxPrice);
}

// Service
@Service
@Transactional
public class ProductService {

    private final ProductRepository productRepository;

    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Transactional(readOnly = true)
    public Page<Product> findAll(Pageable pageable) {
        return productRepository.findAll(pageable);
    }

    @Transactional(readOnly = true)
    public Optional<Product> findById(Long id) {
        return productRepository.findById(id);
    }

    public Product create(Product product) {
        return productRepository.save(product);
    }

    public Optional<Product> update(Long id, Product productDetails) {
        return productRepository.findById(id)
            .map(product -> {
                product.setName(productDetails.getName());
                product.setDescription(productDetails.getDescription());
                product.setPrice(productDetails.getPrice());
                product.setStock(productDetails.getStock());
                return productRepository.save(product);
            });
    }

    public boolean delete(Long id) {
        return productRepository.findById(id)
            .map(product -> {
                productRepository.delete(product);
                return true;
            })
            .orElse(false);
    }
}

// Controller
@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductService productService;

    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping
    public Page<Product> getAllProducts(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(defaultValue = "id") String sortBy) {
        Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
        return productService.findAll(pageable);
    }

    @GetMapping("/{id}")
    public ResponseEntity<Product> getProductById(@PathVariable Long id) {
        return productService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    public ResponseEntity<Product> createProduct(@Valid @RequestBody Product product) {
        Product created = productService.create(product);
        URI location = ServletUriComponentsBuilder
            .fromCurrentRequest()
            .path("/{id}")
            .buildAndExpand(created.getId())
            .toUri();
        return ResponseEntity.created(location).body(created);
    }

    @PutMapping("/{id}")
    public ResponseEntity<Product> updateProduct(
            @PathVariable Long id,
            @Valid @RequestBody Product product) {
        return productService.update(id, product)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
        if (productService.delete(id)) {
            return ResponseEntity.noContent().build();
        }
        return ResponseEntity.notFound().build();
    }
}
例外処理パターン

グローバル例外ハンドラー:

// Custom exceptions
public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

public class BadRequestException extends RuntimeException {
    public BadRequestException(String message) {
        super(message);
    }
}

// Error response
public class ErrorResponse {
    private LocalDateTime timestamp;
    private int status;
    private String error;
    private String message;
    private String path;

    // Constructors, getters, setters
}

// Global exception handler
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleResourceNotFound(
            ResourceNotFoundException ex,
            WebRequest request) {
        ErrorResponse error = new ErrorResponse(
            LocalDateTime.now(),
            HttpStatus.NOT_FOUND.value(),
            "Not Found",
            ex.getMessage(),
            request.getDescription(false).replace("uri=", "")
        );
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(BadRequestException.class)
    public ResponseEntity<ErrorResponse> handleBadRequest(
            BadRequestException ex,
            WebRequest request) {
        ErrorResponse error = new ErrorResponse(
            LocalDateTime.now(),
            HttpStatus.BAD_REQUEST.value(),
            "Bad Request",
            ex.getMessage(),
            request.getDescription(false).replace("uri=", "")
        );
        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, Object>> handleValidationErrors(
            MethodArgumentNotValidException ex) {
        Map<String, Object> errors = new HashMap<>();
        errors.put("timestamp", LocalDateTime.now());
        errors.put("status", HttpStatus.BAD_REQUEST.value());

        Map<String, String> fieldErrors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error ->
            fieldErrors.put(error.getField(), error.getDefaultMessage())
        );
        errors.put("errors", fieldErrors);

        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGlobalException(
            Exception ex,
            WebRequest request) {
        ErrorResponse error = new ErrorResponse(
            LocalDateTime.now(),
            HttpStatus.INTERNAL_SERVER_ERROR.value(),
            "Internal Server Error",
            ex.getMessage(),
            request.getDescription(false).replace("uri=", "")
        );
        return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}
データベース統合パターン

データベースの完全なセットアップ:

// application.yml
/*
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: user
    password: password
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: true
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect
*/

// Flyway migrations (db/migration/V1__Create_users_table.sql)
/*
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    name VARCHAR(255) NOT NULL,
    password VARCHAR(255) NOT NULL,
    created_at TIMESTAMP NOT NULL,
    updated_at TIMESTAMP NOT NULL
);

CREATE INDEX idx_users_email ON users(email);
*/

// Entity with auditing
@Entity
@Table(name = "users")
@EntityListeners(AuditingEntityListener.class)
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private String password;

    @CreatedDate
    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;

    // Getters and setters
}

// Enable JPA auditing
@Configuration
@EnableJpaAuditing
public class JpaConfig {
}
テストパターン

ユニットテスト:

@SpringBootTest
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    void testFindById_Success() {
        User user = new User();
        user.setId(1L);
        user.setEmail("[email protected]");

        when(userRepository.findById(1L)).thenReturn(Optional.of(user));

        Optional<User> result = userService.findById(1L);

        assertTrue(result.isPresent());
        assertEquals("[email protected]", result.get().getEmail());
        verify(userRepository, times(1)).findById(1L);
    }

    @Test
    void testFindById_NotFound() {
        when(userRepository.findById(1L)).thenReturn(Optional.empty());

        Optional<User> result = userService.findById(1L);

        assertFalse(result.isPresent());
    }
}

統合テスト:

@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class UserControllerIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private UserRepository userRepository;

    @Test
    void testCreateUser_Success() throws Exception {
        User user = new User();
        user.setEmail("[email protected]");
        user.setName("Test User");

        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(user)))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.email").value("[email protected]"))
            .andExpect(jsonPath("$.name").value("Test User"));
    }

    @Test
    void testGetUser_Success() throws Exception {
        User user = new User();
        user.setEmail("[email protected]");
        user.setName("Test User");
        User saved = userRepository.save(user);

        mockMvc.perform(get("/api/users/" + saved.getId()))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.id").value(saved.getId()))
            .andExpect(jsonPath("$.email").value("[email protected]"));
    }

    @Test
    void testGetUser_NotFound() throws Exception {
        mockMvc.perform(get("/api/users/999"))
            .andExpect(status().isNotFound());
    }
}

ベストプラクティス

1. コンストラクターインジェクションを使用する

コンストラクターインジェクションは、依存性注入に推奨される方法です。

// Good - Constructor injection
@Service
public class UserService {
    private final UserRepository userRepository;
    private final EmailService emailService;

    public UserService(UserRepository userRepository, EmailService emailService) {
        this.userRepository = userRepository;
        this.emailService = emailService;
    }
}

// Bad - Field injection
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
}
2. API リクエスト/レスポンスには DTO を使用する

REST API を通じてエンティティを直接公開しないでください。

// DTO
public class UserDTO {
    private Long id;
    private String email;
    private String name;

    // No password field exposed
    // Getters and setters
}

// Mapper
@Component
public class UserMapper {
    public UserDTO toDTO(User user) {
        UserDTO dto = new UserDTO();
        dto.setId(user.getId());
        dto.setEmail(user.getEmail());
        dto.setName(user.getName());
        return dto;
    }

    public User toEntity(UserDTO dto) {
        User user = new User();
        user.setEmail(dto.getEmail());
        user.setName(dto.getName());
        return user;
    }
}

// Controller
@RestController
@RequestMapping("/api/users")
public class UserController {
    private final UserService userService;
    private final UserMapper userMapper;

    @GetMapping("/{id}")
    public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
        return userService.findById(id)
            .map(userMapper::toDTO)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }
}
3. バリデーションを使用する

入力データは必ず検証してください。

// Entity with validation
@Entity
public class User {
    @NotBlank(message = "Email is required")
    @Email(message = "Email should be valid")
    private String email;

    @NotBlank(message = "Name is required")
    @Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters")
    private String name;

    @NotBlank(message = "Password is required")
    @Size(min = 8, message = "Password must be at least 8 characters")
    private String password;
}

// Controller
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
    // Validation happens automatically
    return ResponseEntity.ok(userService.save(user));
}
4. トランザクションを適切に使用する

サービスメソッドに適切なトランザクション設定を指定してください。

@Service
@Transactional
public class OrderService {

    @Transactional(readOnly = true)
    public List<Order> findAll() {
        return orderRepository.findAll();
    }

    @Transactional
    public Order createOrder(Order order) {
        // Multiple database operations in one transaction
        Order saved = orderRepository.save(order);
        inventoryService.decreaseStock(order.getItems());
        emailService.sendOrderConfirmation(saved);
        return saved;
    }
}
5. ページネーションを使用する

大規模なデータセットには必ずページネーションを適用してください。

@GetMapping
public Page<Product> getProducts(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "20") int size,
        @RequestParam(defaultValue = "id") String sortBy) {
    Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
    return productService.findAll(pageable);
}
6. 例外をグローバルに処理する

例外処理を一元化するには、@RestControllerAdvice を使用してください。

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
            .body(new ErrorResponse(ex.getMessage()));
    }
}
7. ロギングを使用する

アプリケーション全体で適切なロギングを実装してください。

@Service
public class UserService {
    private static final Logger logger = LoggerFactory.getLogger(UserService.class);

    public User createUser(User user) {
        logger.info("Creating user with email: {}", user.getEmail());
        try {
            User saved = userRepository.save(user);
            logger.info("User created successfully with id: {}", saved.getId());
            return saved;
        } catch (Exception e) {
            logger.error("Error creating user: {}", e.getMessage(), e);
            throw e;
        }
    }
}
8. エンドポイントを保護する

適切な認証と認可を実装してください。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer().jwt();

        return http.build();
    }
}
9. データベースマイグレーションを使用する

データベースのバージョン管理には、Flyway または Liquibase を使用してください。

-- V1__Create_users_table.sql
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    name VARCHAR(255) NOT NULL,
    created_at TIMESTAMP NOT NULL
);

-- V2__Add_password_column.sql
ALTER TABLE users ADD COLUMN password VARCHAR(255);
10. アプリケーションを監視する

監視には Spring Boot Actuator を使用してください。

# application.properties
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always

例

以下の EXAMPLES.md で、次の内容を含む詳細なコード例を確認してください:

  • Spring Boot アプリケーションの基本
  • CRUD 操作を備えた REST API
  • JPA によるデータベース統合
  • カスタムクエリと仕様
  • リクエストのバリデーション
  • 例外処理
  • JWT による認証
  • ロールベースの認可
  • ファイルのアップロード/ダウンロード
  • Redis によるキャッシュ
  • 非同期処理
  • スケジュールタスク
  • 複数データベースの設定
  • Actuator と監視
  • Docker によるデプロイ

概要

この Spring Boot 開発スキルでは、次の内容を扱います。

  1. 自動構成:依存関係に基づく自動構成
  2. 依存性注入:IoC コンテナー、コンストラクターインジェクション、コンポーネントのステレオタイプ
  3. REST API:コントローラー、リクエストマッピング、レスポンス処理
  4. Spring Data JPA:エンティティ、リポジトリ、関連、クエリ
  5. 構成:プロパティ、YAML、プロファイル、カスタムプロパティ
  6. セキュリティ:認証、認可、JWT、ロールベースのアクセス制御
  7. 例外処理:グローバル例外処理、カスタム例外
  8. テスト:ユニットテスト、統合テスト、MockMvc
  9. ベストプラクティス:DTO、バリデーション、トランザクション、ページネーション、ロギング
  10. 本番環境対応:Actuator、監視、データベースマイグレーション、デプロイ

パターンとサンプルは公式 Spring Boot ドキュメント(信頼度スコア:7.5)に基づいており、現代的なエンタープライズ Java 開発の実践方法を示しています。

コメント