Spring (Java) チートシート

cheatsheetWeb開発

DI (Dependency Injection) コンテナ

Spring Core の中心的な機能で、オブジェクト間の依存関係を外部から注入します。

Springコンテナに管理させるオブジェクト(Bean)を定義する方法です。

定義方法 概要 主なアノテーション/要素 コード例 (抜粋)
XMLベース定義 設定ファイル (XML) にBean定義を記述します。伝統的な方法です。 <bean>, <constructor-arg>, <property>
<beans xmlns="http://www.springframework.org/schema/beans" ...>
    <bean id="myService" class="com.example.MyServiceImpl">
        <constructor-arg ref="myRepository"/>
        <property name="message" value="Hello"/>
    </bean>
    <bean id="myRepository" class="com.example.MyRepositoryImpl"/>
</beans>
JavaConfigベース定義 Javaクラス内に @Configuration アノテーションを付与し、@Bean アノテーションを付与したメソッドでBeanを定義します。現在主流の方法です。 @Configuration, @Bean, @Import
@Configuration
public class AppConfig {
    @Bean
    public MyService myService(MyRepository myRepository) {
        MyServiceImpl service = new MyServiceImpl(myRepository);
        service.setMessage("Hello");
        return service;
    }
    @Bean
    public MyRepository myRepository() {
        return new MyRepositoryImpl();
    }
}
アノテーションベース定義 (コンポーネントスキャン) クラスに @Component やその派生アノテーション (@Service, @Repository, @Controller など) を付与し、コンポーネントスキャン機能で自動的にBeanとして登録します。 @Component, @Service, @Repository, @Controller, @RestController, @Configuration, @ComponentScan
@Service // @Componentの特殊化アノテーション
public class MyServiceImpl implements MyService {
    // ...
}

@Configuration
@ComponentScan(basePackages = "com.example") // スキャン対象のパッケージを指定
public class AppConfig {
    // ... JavaConfigによる他のBean定義も可能
}

Bean間の依存関係を注入する方法です。

インジェクション方法 概要 主なアノテーション コード例
コンストラクタインジェクション 推奨される方法 👍。依存関係をコンストラクタの引数で受け取ります。不変性 (Immutability) を保証しやすく、依存関係が明確になります。Spring 4.3以降、対象クラスのコンストラクタが一つだけの場合、@Autowired は省略可能です。 @Autowired (省略可能な場合あり)
@Service
public class MyService {
    private final MyRepository repository;
    // @Autowired // コンストラクタが1つの場合、省略可能
    public MyService(MyRepository repository) {
        this.repository = repository;
    }
    // ...
}
セッターインジェクション セッターメソッド経由で依存関係を注入します。任意 (Optional) の依存関係や、循環参照を解決する場合に使用されることがあります。 @Autowired
@Service
public class MyService {
    private MyRepository repository;
    @Autowired
    public void setRepository(MyRepository repository) {
        this.repository = repository;
    }
    // ...
}
フィールドインジェクション フィールドに直接 @Autowired を付与して注入します。コードは簡潔になりますが、テストがしにくくなる、不変性が保証できないなどのデメリットがあり、非推奨 👎 とされることが多いです。 @Autowired
@Service
public class MyService {
    @Autowired
    private MyRepository repository; // 非推奨
    // ...
}

Beanインスタンスの生成範囲を定義します。

スコープ名 概要 アノテーション/設定
singleton (デフォルト) Springコンテナごとに単一のインスタンスが生成・共有されます。 @Scope("singleton") または省略
prototype Beanがリクエストされるたびに新しいインスタンスが生成されます。 @Scope("prototype")
request HTTPリクエストごとに新しいインスタンスが生成されます。(Webアプリケーション環境のみ) @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS), @RequestScope
session HTTPセッションごとに新しいインスタンスが生成されます。(Webアプリケーション環境のみ) @Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS), @SessionScope
application ServletContextごとに単一のインスタンスが生成されます。(Webアプリケーション環境のみ) @Scope(value = "application", proxyMode = ScopedProxyMode.TARGET_CLASS), @ApplicationScope
websocket WebSocketセッションごとに新しいインスタンスが生成されます。(Websocket環境のみ) @Scope("websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)

request, session, application, websocket スコープをシングルトンBeanに注入する場合、通常はプロキシモードの設定が必要です。

Beanの初期化後や破棄前に特定の処理を実行するためのフックです。

  • 初期化コールバック:
    • アノテーション: @PostConstruct (JSR-250) – 推奨
    • インターフェース: InitializingBean (afterPropertiesSet() メソッドを実装)
    • XML/JavaConfig: init-method 属性 / @Bean(initMethod = "...")
  • 破棄コールバック:
    • アノテーション: @PreDestroy (JSR-250) – 推奨
    • インターフェース: DisposableBean (destroy() メソッドを実装)
    • XML/JavaConfig: destroy-method 属性 / @Bean(destroyMethod = "...")
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

@Service
public class LifecycleDemoBean {
    @PostConstruct
    public void initialize() {
        System.out.println("Beanが初期化されました! ✨");
    }
    @PreDestroy
    public void cleanup() {
        System.out.println("Beanが破棄されます... 🗑️");
    }
    // ...
}

特定の環境(開発、テスト、本番など)でのみ有効になるBeanや設定を定義します。

// 開発環境でのみ有効なデータソース設定
@Configuration
@Profile("development")
public class DevDataSourceConfig {
    @Bean
    public DataSource dataSource() {
        // H2などのインメモリDB設定
        System.out.println("開発用データソースを設定します (H2) 🛠️");
        return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).build();
    }
}

// 本番環境でのみ有効なデータソース設定
@Configuration
@Profile("production")
public class ProdDataSourceConfig {
    @Bean
    public DataSource dataSource() {
        // 本番DBへの接続設定
        System.out.println("本番用データソースを設定します 🏭");
        // DataSourceBuilderなどを使用して設定
        return DataSourceBuilder.create()./* ... */.build();
    }
}

有効化は、環境変数 spring.profiles.active や JVM引数 -Dspring.profiles.active=development などで行います。

特定の条件を満たす場合にのみBeanを生成します。@Profile@Conditional の特殊なケースです。

// 特定のプロパティが存在する場合にのみBeanを生成
@Configuration
public class ConditionalConfig {
    @Bean
    @ConditionalOnProperty(name = "feature.toggle.new-service", havingValue = "true")
    public NewService newService() {
        return new NewServiceImpl();
    }

    // 特定のクラスがクラスパスに存在する場合にのみBeanを生成
    @Bean
    @ConditionalOnClass(name = "com.example.ExternalLibrary")
    public ExternalService externalService() {
        return new ExternalServiceImpl();
    }

    // 特定のBeanが存在しない場合にのみBeanを生成
    @Bean
    @ConditionalOnMissingBean(DataSource.class)
    public DataSource defaultDataSource() {
        // デフォルトのデータソース設定
        return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).build();
    }
}

Spring Bootでは、@ConditionalOn... という便利なアノテーションが多く提供されています。

Spring MVC (Webフレームワーク) 🌐

Webアプリケーションを構築するためのフレームワークです。リクエスト処理、ビューレンダリングなどを担当します。

ユーザーからのリクエストを受け付け、処理を行うコンポーネントです。

  • @Controller: 通常のWebアプリケーション用。メソッドの戻り値は通常ビュー名(String)や ModelAndView
  • @RestController: RESTful Webサービス用。@Controller@ResponseBody を組み合わせたもの。メソッドの戻り値は直接レスポンスボディ(JSON/XMLなど)になります。
@Controller // 通常のWebアプリ用
public class WebController {
    @GetMapping("/hello")
    public String hello(Model model) {
        model.addAttribute("message", "Hello from Controller!");
        return "helloView"; // ビュー名を返す (例: helloView.html)
    }
}

@RestController // REST API用
@RequestMapping("/api/users")
public class UserApiController {
    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        // ユーザー情報を取得して返す (自動的にJSONなどに変換される)
        return new User(id, "Taro Yamada");
    }
}

HTTPリクエストのURL、メソッドなどを特定のコントローラーメソッドに紐付けます。

アノテーション 概要 コード例
@RequestMapping 基本的なマッピング。path (または value), method, params, headers 属性などで詳細な条件を指定可能。クラスレベル、メソッドレベルで使用可能。 @RequestMapping(value = "/legacy", method = RequestMethod.GET)
@GetMapping HTTP GETリクエスト用のショートカット。@RequestMapping(method = RequestMethod.GET) と同等。 @GetMapping("/users")
@PostMapping HTTP POSTリクエスト用のショートカット。 @PostMapping("/users")
@PutMapping HTTP PUTリクエスト用のショートカット。 @PutMapping("/users/{id}")
@DeleteMapping HTTP DELETEリクエスト用のショートカット。 @DeleteMapping("/users/{id}")
@PatchMapping HTTP PATCHリクエスト用のショートカット。 @PatchMapping("/users/{id}")
@RestController
@RequestMapping("/items") // クラスレベルでパスのプレフィックスを指定
public class ItemController {
    @GetMapping // GET /items
    public List<Item> getItems() { /* ... */ }

    @PostMapping // POST /items
    public ResponseEntity<Void> createItem(@RequestBody Item item) { /* ... */ }

    @GetMapping("/{itemId}") // GET /items/{itemId} (パス変数)
    public Item getItemById(@PathVariable String itemId) { /* ... */ }

    @GetMapping(params = "type=special") // GET /items?type=special (リクエストパラメータ条件)
    public List<Item> getSpecialItems() { /* ... */ }
}

HTTPリクエストに含まれる情報をコントローラーメソッドで受け取る方法です。

アノテーション 概要 コード例
@RequestParam クエリパラメータやフォームデータを取得します。name (または value), required, defaultValue 属性を指定可能。
// GET /search?q=keyword&page=1
@GetMapping("/search")
public String search(
    @RequestParam("q") String query,
    @RequestParam(name = "page", required = false, defaultValue = "1") int page) {
    // ...
    return "searchResults";
}
@PathVariable URLパスの一部を動的な値として取得します (例: /users/{id}id)。
// GET /users/123
@GetMapping("/users/{userId}")
public User getUser(@PathVariable("userId") Long id) {
    // ...
    return userService.findById(id);
}
@RequestBody リクエストボディの内容(通常はJSONやXML)をJavaオブジェクトにデシリアライズして受け取ります。HttpMessageConverter が使用されます。
// POST /users (リクエストボディに {"name": "Test", "email": "test@example.com"} )
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User newUser) {
    User createdUser = userService.create(newUser);
    return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
}
@RequestHeader リクエストヘッダーの値を取得します。
@GetMapping("/info")
public String getInfo(@RequestHeader("User-Agent") String userAgent) {
    // ...
    return "User agent: " + userAgent;
}
@CookieValue クッキーの値を取得します。
@GetMapping("/welcome")
public String welcome(@CookieValue(name = "sessionId", required = false) String sessionId) {
    // ...
    return "welcome";
}
@ModelAttribute リクエストパラメータやフォームデータをオブジェクトにバインドします。また、モデルに属性を追加するためにも使用されます。
// POST /register (フォームデータ: username=test&password=pwd)
@PostMapping("/register")
public String registerUser(@ModelAttribute UserRegistrationForm form) {
    // formオブジェクトに "test" と "pwd" がバインドされる
    userService.register(form);
    return "redirect:/login";
}

// このメソッドの戻り値は自動的にモデルに追加される
@ModelAttribute("commonAttribute")
public String addCommonAttribute() {
    return "This is common";
}

コントローラーメソッドからクライアントへのレスポンスを生成する方法です。

  • ビュー名を返す (String): @Controller で最も一般的。ViewResolverによって実際のビュー(HTML, JSPなど)が解決されます。Model オブジェクトにデータを追加してビューに渡します。
  • ModelAndView オブジェクト: ビュー名とモデルデータを一緒に保持するオブジェクト。
  • @ResponseBody アノテーション: メソッドの戻り値を直接レスポンスボディとして書き込みます (JSON/XMLなど)。HttpMessageConverter が使用されます。@RestController では暗黙的に付与されます。
  • ResponseEntity<T>: レスポンスボディ、HTTPステータスコード、レスポンスヘッダーを完全に制御したい場合に使用します。
  • void: レスポンスを直接 HttpServletResponse に書き込む場合など。
@Controller
@RequestMapping("/response")
public class ResponseDemoController {

    // 1. ビュー名を返す
    @GetMapping("/view")
    public String showView(Model model) {
        model.addAttribute("data", "Data for View");
        return "myView"; // "myView.html" などが解決される
    }

    // 2. ModelAndView を返す
    @GetMapping("/modelandview")
    public ModelAndView showModelAndView() {
        ModelAndView mav = new ModelAndView("myView");
        mav.addObject("data", "Data via ModelAndView");
        return mav;
    }

    // 3. @ResponseBody で直接データを返す (JSONなど)
    @GetMapping("/data")
    @ResponseBody // @RestController なら不要
    public Map<String, String> getData() {
        return Map.of("key", "value"); // JacksonライブラリがあればJSONに変換される
    }

    // 4. ResponseEntity で詳細を制御
    @GetMapping("/entity/{id}")
    public ResponseEntity<User> getUserEntity(@PathVariable Long id) {
        User user = userService.findById(id);
        if (user != null) {
            return ResponseEntity.ok(user); // ステータス 200 OK と Userオブジェクト
        } else {
            return ResponseEntity.notFound().build(); // ステータス 404 Not Found
        }
    }

    // 5. void (直接レスポンスに書き込む例 - あまり一般的ではない)
    @GetMapping("/direct")
    public void writeDirect(HttpServletResponse response) throws IOException {
        response.setContentType("text/plain");
        response.getWriter().write("Direct response writing.");
    }
}

リクエストデータ (@RequestBody@ModelAttribute で受け取ったオブジェクト) の妥当性チェックを行います。

  1. hibernate-validator などの Bean Validation 実装ライブラリを依存関係に追加します。
  2. 検証対象のクラスのフィールドにバリデーションアノテーション (@NotNull, @Size, @Email, @Pattern など) を付与します。
  3. コントローラーメソッドの引数に @Valid (または @Validated) アノテーションを付与します。
  4. BindingResult (または Errors) 引数を @Valid の直後に追加して、検証結果を受け取ります。
// 検証対象クラス (DTOなど)
import javax.validation.constraints.*;

public class UserInput {
    @NotBlank(message = "名前は必須です")
    @Size(min = 2, max = 50, message = "名前は2文字以上50文字以下です")
    private String name;

    @NotNull(message = "年齢は必須です")
    @Min(value = 0, message = "年齢は0以上です")
    private Integer age;

    @Email(message = "有効なメールアドレス形式ではありません")
    private String email;
    // getter/setter
}

// コントローラー
import org.springframework.validation.BindingResult;
import javax.validation.Valid;

@RestController
@RequestMapping("/validate")
public class ValidationController {
    @PostMapping("/users")
    public ResponseEntity<?> createUser(
            @Valid @RequestBody UserInput userInput, // @Validで検証実行
            BindingResult bindingResult) { // 検証結果を受け取る

        if (bindingResult.hasErrors()) {
            // エラーがある場合、エラー情報を返す (例: 400 Bad Request)
            List<String> errors = bindingResult.getAllErrors().stream()
                    .map(err -> err.getDefaultMessage())
                    .collect(Collectors.toList());
            return ResponseEntity.badRequest().body(errors);
        }

        // 検証OKの場合の処理
        // userService.create(userInput);
        return ResponseEntity.status(HttpStatus.CREATED).body("User created successfully!");
    }
}

コントローラーで発生した例外を捕捉し、適切なレスポンスを返す仕組みです。

  • @ExceptionHandler: 特定のコントローラー内で特定の例外を処理するメソッドに付与します。
  • @ControllerAdvice / @RestControllerAdvice: アプリケーション全体、または特定のパッケージ配下のコントローラーで発生する例外を一元的に処理するクラスに付与します。@ExceptionHandler と組み合わせて使用します。@RestControllerAdvice@ControllerAdvice + @ResponseBody です。 推奨 👍
  • ResponseStatusException: プログラム的に特定のHTTPステータスでレスポンスを返したい場合にスローします。
// グローバルな例外ハンドラ
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import java.util.NoSuchElementException;

@ControllerAdvice // または @RestControllerAdvice
public class GlobalExceptionHandler {

    // 特定の例外 (例: NoSuchElementException) を捕捉
    @ExceptionHandler(NoSuchElementException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND) // レスポンスステータスを指定
    public ResponseEntity<String> handleNotFound(NoSuchElementException ex) {
        // エラーレスポンスのボディを生成
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body("リソースが見つかりません: " + ex.getMessage());
    }

    // 引数バリデーションエラー (MethodArgumentNotValidException) を捕捉
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, List<String>>> handleValidationExceptions(
            MethodArgumentNotValidException ex) {
        Map<String, List<String>> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.computeIfAbsent(fieldName, k -> new ArrayList<>()).add(errorMessage);
        });
        return ResponseEntity.badRequest().body(errors);
    }

    // その他の予期せぬ例外を捕捉
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResponseEntity<String> handleGenericException(Exception ex) {
        // ログ出力など
        System.err.println("予期せぬエラーが発生しました: " + ex.getMessage());
        ex.printStackTrace();
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("サーバー内部エラーが発生しました。");
    }
}

// コントローラー内で ResponseStatusException を使う例
@GetMapping("/items/{id}")
public Item getItem(@PathVariable Long id) {
    Item item = itemService.findById(id);
    if (item == null) {
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Item not found with id: " + id);
    }
    return item;
}

Spring Data JPA (データアクセス) 💾

JPA (Java Persistence API) を利用したデータアクセス層の実装を大幅に簡略化します。

JpaRepository などの Spring Data 提供のインターフェースを継承するだけで、基本的なCRUD操作(作成, 読み取り, 更新, 削除)メソッドが自動的に実装されます。

  1. エンティティクラス (@Entity) を定義します。
  2. JpaRepository<エンティティクラス, 主キーの型> を継承したインターフェースを作成します。
  3. 必要に応じてカスタムクエリメソッドを追加します。
// エンティティクラス
import javax.persistence.*;

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String email;
    // getters and setters
}

// リポジトリインターフェース
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
    // 基本的な CRUD メソッド (save, findById, findAll, deleteById など) は自動実装

    // --- カスタムクエリメソッド (メソッド名規約) ---
    // Email でユーザーを検索
    Optional<User> findByEmail(String email);

    // Username を含むユーザーを検索 (部分一致、大文字小文字無視)
    List<User> findByUsernameContainingIgnoreCase(String keyword);

    // 特定の Email を持ち、ID が指定値より大きいユーザーを検索
    List<User> findByEmailAndIdGreaterThan(String email, Long id);

    // --- @Query アノテーションによるカスタムクエリ ---
    @Query("SELECT u FROM User u WHERE u.username = ?1") // JPQL (Java Persistence Query Language)
    User findByUsernameExactly(String username);

    @Query(value = "SELECT * FROM users u WHERE u.email LIKE %?1%", nativeQuery = true) // ネイティブ SQL
    List<User> findByEmailEndingWithNative(String suffix);
}

// サービス層での利用例
@Service
public class UserService {
    private final UserRepository userRepository;
    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User createUser(User user) {
        return userRepository.save(user); // 保存 (新規/更新)
    }

    public Optional<User> findUserById(Long id) {
        return userRepository.findById(id); // IDで検索
    }

    public List<User> searchUsers(String keyword) {
        return userRepository.findByUsernameContainingIgnoreCase(keyword); // メソッド名規約クエリ
    }
}

大量のデータを効率的に扱うためのページング処理とソート機能を提供します。

  • メソッドの引数に Pageable インターフェースを追加します。
  • メソッドの戻り値を Page<エンティティクラス> にします。
  • Pageable オブジェクトは PageRequest.of(ページ番号, 1ページの件数, Sort) で作成します。
  • Sort オブジェクトでソート条件を指定します (Sort.by("プロパティ名").ascending()/descending())。
// リポジトリインターフェースのメソッド例
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;

public interface ProductRepository extends JpaRepository<Product, Long> {
    // カテゴリ名で検索し、価格でソートしてページング
    Page<Product> findByCategory(String category, Pageable pageable);

    // JPQL でのページング
    @Query("SELECT p FROM Product p WHERE p.price > ?1")
    Page<Product> findByPriceGreaterThan(double minPrice, Pageable pageable);
}

// サービス層での利用例
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;

@Service
public class ProductService {
    private final ProductRepository productRepository;
    // constructor injection

    public Page<Product> findProductsByCategoryPaginated(String category, int page, int size, String sortBy, String direction) {
        Sort sort = Sort.by(direction.equalsIgnoreCase("desc") ? Sort.Direction.DESC : Sort.Direction.ASC, sortBy);
        Pageable pageable = PageRequest.of(page, size, sort);
        return productRepository.findByCategory(category, pageable);
    }
}

// コントローラーでの利用例
@GetMapping("/products")
public Page<Product> getProducts(
        @RequestParam String category,
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "10") int size,
        @RequestParam(defaultValue = "price") String sortBy,
        @RequestParam(defaultValue = "asc") String direction) {
    return productService.findProductsByCategoryPaginated(category, page, size, sortBy, direction);
    // Page オブジェクトには、コンテンツリスト、総件数、総ページ数などの情報が含まれる
}

一連のデータベース操作をアトミック(不可分)に実行するための仕組みです。メソッドまたはクラスに @Transactional アノテーションを付与します。

  • デフォルトでは、RuntimeException および Error が発生した場合にロールバックされます。
  • rollbackFor / noRollbackFor 属性でロールバック対象/対象外の例外を指定できます。
  • readOnly = true を指定すると、読み取り専用トランザクションとなり、パフォーマンスが向上する場合があります (更新操作は不可)。
  • propagation 属性でトランザクションの伝播動作(既存のトランザクションに参加するか、新規に開始するかなど)を制御できます (例: Propagation.REQUIRED (デフォルト), Propagation.REQUIRES_NEW)。
  • isolation 属性でトランザクションの分離レベルを指定できます (例: Isolation.DEFAULT, Isolation.READ_COMMITTED)。
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.annotation.Propagation;
import java.io.IOException;

@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;
    // constructor injection

    @Transactional // デフォルト設定 (RuntimeExceptionでロールバック)
    public Order placeOrder(Long productId, int quantity) {
        // 1. 商品在庫を確認・更新 (ロックが必要な場合あり)
        Product product = productRepository.findById(productId)
                .orElseThrow(() -> new NoSuchElementException("Product not found"));
        if (product.getStock() < quantity) {
            throw new RuntimeException("在庫不足です"); // これでロールバックされる
        }
        product.setStock(product.getStock() - quantity);
        productRepository.save(product); // 在庫更新

        // 2. 注文を作成
        Order newOrder = new Order(/* ... */);
        return orderRepository.save(newOrder); // 注文保存
        // このメソッド全体が1つのトランザクション
    }

    @Transactional(readOnly = true) // 読み取り専用
    public List<Order> findOrdersByUser(Long userId) {
        return orderRepository.findByUserId(userId);
    }

    @Transactional(rollbackFor = {IOException.class, SQLException.class}, noRollbackFor = MyBusinessException.class)
    public void complexOperation() throws IOException, SQLException {
        // IOException や SQLException が発生したらロールバック
        // MyBusinessException が発生してもロールバックしない
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW) // 常に新しいトランザクションを開始
    public void auditLog(String message) {
        // 監査ログを別トランザクションで記録
    }
}

注意: @Transactional は public メソッドに付与する必要があり、同じクラス内のメソッド呼び出しではデフォルトではAOPプロキシを経由しないため、トランザクション制御が効かない場合があります。

Spring Boot (アプリケーション開発の簡略化) 🚀

Springアプリケーションの開発を迅速かつ容易にするためのフレームワークです。「設定より規約」の原則に基づき、多くの定型的な設定を自動化します。

特定の機能を利用するために必要なライブラリ群をまとめたものです。pom.xml (Maven) や build.gradle (Gradle) に追加するだけで、関連ライブラリが自動的に導入され、設定されます。

スターター名 主な機能
spring-boot-starter-web Spring MVC、組み込みTomcat (デフォルト)、RESTful Webサービス開発に必要な基本機能。Jackson (JSON処理) も含まれる。
spring-boot-starter-data-jpa Spring Data JPA、Hibernate (デフォルトJPA実装)、JDBC、トランザクション管理。
spring-boot-starter-security Spring Security による認証・認可機能。基本的なセキュリティ設定が自動で適用される。
spring-boot-starter-test JUnit 5, Spring Test, AssertJ, Hamcrest, Mockito, JsonPath など、テストに必要なライブラリ。
spring-boot-starter-actuator アプリケーションの監視・管理機能 (ヘルスチェック、メトリクス、環境情報など) を提供するエンドポイント。
spring-boot-starter-thymeleaf Thymeleaf テンプレートエンジンとの統合。
spring-boot-starter-validation Hibernate Validator を含む Bean Validation 機能。
spring-boot-starter-jdbc JDBC によるデータベースアクセス、JdbcTemplate
spring-boot-starter-webflux リアクティブWebフレームワーク Spring WebFlux、組み込みNetty (デフォルト)。
spring-boot-starter-cache Springのキャッシュ抽象化機能。

例 (Maven pom.xml):

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <!-- 他のスターター ... -->
</dependencies>

クラスパス上のライブラリや既存のBean定義に基づいて、Springアプリケーションの 設定を自動的に行います。例えば、spring-boot-starter-data-jpa があれば、データソースやEntityManagerFactoryなどが自動で設定されます。@SpringBootApplication アノテーションに含まれています。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication // = @Configuration + @EnableAutoConfiguration + @ComponentScan
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

特定の自動構成を無効化したい場合は @SpringBootApplication(exclude={DataSourceAutoConfiguration.class}) のように指定します。

アプリケーションの挙動を外部ファイルで設定します。デフォルトでは src/main/resources 配下の application.properties または application.yml が読み込まれます。

application.properties (キー=値 形式):

# サーバーポート
server.port=8080
# データベース接続情報
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=dbuser
spring.datasource.password=dbpass
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# JPA/Hibernate設定
spring.jpa.hibernate.ddl-auto=update # (create, create-drop, update, validate, none)
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# ログレベル設定
logging.level.org.springframework.web=DEBUG
logging.level.com.example=INFO
# カスタムプロパティ
myapp.feature.enabled=true
myapp.service.url=https://api.example.com

application.yml (YAML 形式):

server:
  port: 8080
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb
    username: dbuser
    password: dbpass
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true
logging:
  level:
    org:
      springframework:
        web: DEBUG
    com:
      example: INFO
myapp:
  feature:
    enabled: true
  service:
    url: https://api.example.com

プロファイル固有の設定ファイル (application-development.properties, application-production.yml など) を作成し、spring.profiles.active プロパティで切り替えることができます。

カスタムプロパティをJavaコードで読み込むには @Value アノテーションや @ConfigurationProperties を使用します。

// @Value を使用
@Service
public class MyService {
    @Value("${myapp.service.url}")
    private String serviceUrl;

    @Value("${myapp.feature.enabled:false}") // デフォルト値指定
    private boolean featureEnabled;
    // ...
}

// @ConfigurationProperties を使用 (タイプセーフ)
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component; // または @Configuration

@Component // または @Configuration
@ConfigurationProperties(prefix = "myapp") // "myapp" プレフィックスのプロパティをバインド
public class MyAppProperties {
    private Feature feature = new Feature(); // ネストしたプロパティ
    private Service service = new Service();
    // getters and setters for feature and service

    public static class Feature {
        private boolean enabled;
        // getter and setter for enabled
    }
    public static class Service {
        private String url;
        // getter and setter for url
    }
}

// 利用側
@RestController
public class MyController {
    private final MyAppProperties myAppProperties;
    public MyController(MyAppProperties myAppProperties) { // コンストラクタインジェクション
        this.myAppProperties = myAppProperties;
        System.out.println("Service URL: " + myAppProperties.getService().getUrl());
        System.out.println("Feature Enabled: " + myAppProperties.getFeature().isEnabled());
    }
}

Spring Bootは、デフォルトで組み込みのWebサーバー (Tomcat, Jetty, Undertow) を含んでいます。これにより、アプリケーションを単一の実行可能JARファイルとしてパッケージングし、別途Webサーバーをインストール・設定することなく実行できます。

  • デフォルトはTomcat。spring-boot-starter-web に含まれます。
  • Jettyを使いたい場合: spring-boot-starter-web からTomcatを除外し、spring-boot-starter-jetty を追加します。
  • Undertowを使いたい場合: 同様にTomcatを除外し、spring-boot-starter-undertow を追加します。

アプリケーションの監視と管理のための機能を提供します。spring-boot-starter-actuator を依存関係に追加すると、様々なエンドポイントが利用可能になります。

主なエンドポイント (デフォルトパス: /actuator/...):

エンドポイント 機能 デフォルト公開
/health アプリケーションのヘルス情報(DB接続、ディスク空き容量など)を表示。 ✅ (Web経由)
/info アプリケーションの一般情報(ビルド情報、Git情報など)を表示。 ✅ (Web経由)
/metrics アプリケーションの各種メトリクス(JVMメモリ使用量、HTTPリクエスト数、CPU使用率など)を表示。
/env Spring Environment のプロパティ一覧を表示。
/loggers アプリケーションのロガーとそのレベルを表示・変更。
/beans Springコンテナ内のBean定義一覧を表示。
/mappings @RequestMapping のマッピング情報一覧を表示。
/configprops @ConfigurationProperties Beanの一覧を表示。
/threaddump JVMのスレッドダンプを取得。
/heapdump JVMのヒープダンプを取得(ファイルダウンロード)。
/shutdown アプリケーションをシャットダウン (デフォルト無効)。

エンドポイントの公開設定は application.properties / application.yml で行います。

# すべてのエンドポイントをWeb経由で公開 (セキュリティに注意!)
management.endpoints.web.exposure.include=*
# 特定のエンドポイントのみ公開
# management.endpoints.web.exposure.include=health,info,metrics
# 特定のエンドポイントを非公開
# management.endpoints.web.exposure.exclude=env,beans
# Actuatorのエンドポイント用ポートを変更 (デフォルトはアプリケーションと同じ)
# management.server.port=8081
# Actuatorのエンドポイントのベースパスを変更 (デフォルトは /actuator)
# management.endpoints.web.base-path=/manage
# shutdownエンドポイントを有効化
management.endpoint.shutdown.enabled=true

Spring Bootアプリケーションのインテグレーションテストを容易にします。@SpringBootTest アノテーションを使用すると、テスト実行時に実際のSpring Bootアプリケーションコンテキストを起動します。

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.ResponseEntity;
import static org.assertj.core.api.Assertions.assertThat;

// WebEnvironment.RANDOM_PORT: 実際のWebサーバーをランダムなポートで起動
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class MyIntegrationTest {

    @Autowired // DIコンテナからテスト対象のServiceなどをインジェクト可能
    private MyService myService;

    @Autowired // HTTPリクエストを送信するためのテストクライアント
    private TestRestTemplate restTemplate;

    @Test
    void contextLoads() {
        // アプリケーションコンテキストが正常にロードされるか確認
        assertThat(myService).isNotNull();
    }

    @Test
    void testMyServiceLogic() {
        String result = myService.doSomething("test");
        assertThat(result).isEqualTo("Processed: test");
    }

    @Test
    void testGreetingEndpoint() {
        // GET /greeting エンドポイントをテスト
        ResponseEntity<String> response = restTemplate.getForEntity("/greeting", String.class);
        assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();
        assertThat(response.getBody()).contains("Hello, World!");
    }
}

特定の層に特化したテストスライスアノテーションもあります。

  • @WebMvcTest: Spring MVCコントローラー層のみをテスト (@Controller, @ControllerAdvice など)。Webサーバーは起動せず、MockMvc を使用。
  • @DataJpaTest: JPAリポジトリ層のみをテスト (@Entity, @Repository)。インメモリDB (H2など) を自動構成。
  • @RestClientTest: RestTemplateWebClient を使用したクライアントサイドのテスト。
  • @JsonTest: JSONのシリアライズ/デシリアライズをテスト。

Spring Security (セキュリティ) 🔒

アプリケーションの認証(Authentication)と認可(Authorization)を提供します。

WebSecurityConfigurerAdapter を継承したクラス (Spring Security 5.7 未満) または SecurityFilterChain Bean を定義 (Spring Security 5.7 以降、推奨 👍) して設定します。

SecurityFilterChain Bean (推奨):

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity // Spring Security を有効化
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/css/**", "/js/**", "/images/**", "/public/**", "/login").permitAll() // 認証不要なパス
                .requestMatchers("/admin/**").hasRole("ADMIN") // /admin/** は ADMIN ロールが必要
                .anyRequest().authenticated() // その他のリクエストは認証が必要
            )
            .formLogin(formLogin -> formLogin // フォームベース認証を有効化
                .loginPage("/login") // カスタムログインページ (指定しない場合はデフォルト)
                .permitAll() // ログインページは全員アクセス可能
                .defaultSuccessUrl("/home", true) // ログイン成功後のリダイレクト先
            )
            .logout(logout -> logout // ログアウト機能を有効化
                .logoutSuccessUrl("/login?logout") // ログアウト成功後のリダイレクト先
                .permitAll()
            )
            .httpBasic(withDefaults()); // Basic認証も有効化 (任意)
            // .csrf(csrf -> csrf.disable()); // CSRF保護を無効化する場合 (非推奨)

        return http.build();
    }

    // パスワードエンコーダーのBean定義 (必須)
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // BCryptを推奨
    }

    // インメモリユーザー定義 (テスト用)
    // 本番では UserDetailsService を実装してDBなどからユーザー情報を取得する
    @Bean
    public InMemoryUserDetailsManager 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("adminpass"))
            .roles("ADMIN", "USER") // 複数のロールを持つことも可能
            .build();
        return new InMemoryUserDetailsManager(user, admin);
    }
}
  • UserDetailsService: ユーザー名を受け取り、ユーザー情報 (UserDetails) を返すインターフェース。データベースやLDAPなどからユーザー情報を取得する処理を実装します。
  • PasswordEncoder: パスワードを安全にハッシュ化し、入力されたパスワードとハッシュ値を比較するためのインターフェース。BCryptPasswordEncoder が推奨されます。
// UserDetailsService の実装例 (JPAを使用)
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Collections; // or stream roles/authorities from User entity

@Service
public class JpaUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository; // Spring Data JPAリポジトリ

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

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        com.example.myapp.entity.User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("ユーザーが見つかりません: " + username));

        // DBから取得したUserエンティティを Spring Security の UserDetails に変換
        return org.springframework.security.core.userdetails.User
                .withUsername(user.getUsername())
                .password(user.getPassword()) // DBにはエンコード済みのパスワードを保存
                .authorities(Collections.singletonList(() -> "ROLE_" + user.getRole())) // "ROLE_ADMIN", "ROLE_USER" など
                // .roles(user.getRole()) // .roles("ADMIN") は .authorities("ROLE_ADMIN") と同等
                .accountExpired(!user.isAccountNonExpired())
                .accountLocked(!user.isAccountNonLocked())
                .credentialsExpired(!user.isCredentialsNonExpired())
                .disabled(!user.isEnabled())
                .build();
    }
}

サービス層などのメソッド呼び出しに対して、実行前後に認可チェックを行います。@EnableMethodSecurity (または古い @EnableGlobalMethodSecurity) アノテーションで有効化します。

  • @PreAuthorize("hasRole('ADMIN')"): メソッド実行前に評価。権限がない場合はメソッドは実行されない。
  • @PostAuthorize("returnObject.owner == authentication.name"): メソッド実行後に評価。戻り値 (returnObject) や認証情報 (authentication) を使用できる。
  • @Secured("ROLE_ADMIN"): JSR-250 標準。単純なロールチェック。
  • @RolesAllowed("ADMIN"): JSR-250 標準。@Secured とほぼ同等。
  • @PreFilter / @PostFilter: コレクションや配列の引数/戻り値に対してフィルタリングを行う。
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

@Service
@EnableMethodSecurity // メソッドセキュリティを有効化 (Configurationクラスでも可)
public class ProtectedService {

    @PreAuthorize("hasRole('ADMIN')") // ADMINロールを持つユーザーのみ実行可能
    public void performAdminTask() {
        System.out.println("管理者タスクを実行中...");
    }

    @PreAuthorize("hasAuthority('permission:read')") // 'permission:read' 権限を持つユーザーのみ
    public String readSensitiveData() {
        return "機密データ";
    }

    // メソッド引数 #userId と認証ユーザー名が一致する場合のみ実行可能
    @PreAuthorize("#userId == authentication.principal.username or hasRole('ADMIN')")
    public UserProfile getUserProfile(String userId) {
        // ... profile lookup
        return new UserProfile(userId, "...");
    }

    // メソッド実行後、戻り値の owner が認証ユーザー名と一致するかチェック
    @PostAuthorize("returnObject.owner == authentication.name")
    public Document getDocument(Long id) {
        // ... document lookup
        return documentRepository.findById(id).orElse(null);
    }
}

AOP (Aspect-Oriented Programming) ✨

ログ出力、トランザクション管理、セキュリティチェックなど、アプリケーション全体にまたがる横断的な関心事 (Cross-cutting Concerns) をモジュール化する技術です。

  • アスペクト (Aspect): 横断的な関心事を実装したモジュール (クラス)。@Aspect アノテーションを付与。
  • ジョインポイント (Join Point): アスペクトを適用可能なプログラムの実行地点 (メソッド呼び出し、フィールドアクセスなど)。Spring AOPでは主にメソッド実行。
  • ポイントカット (Pointcut): どこのジョインポイントにアドバイスを適用するかを定義する式。@Pointcut アノテーションを使用。
  • アドバイス (Advice): ジョインポイントで実行されるアスペクトの処理。実行タイミングに応じて種類がある (@Before, @After, @Around など)。
  • ターゲットオブジェクト (Target Object): アドバイスが適用されるオブジェクト。
  • AOPプロキシ (AOP Proxy): ターゲットオブジェクトをラップし、アドバイスの実行を制御するオブジェクト (JDK Dynamic Proxy または CGLIB)。
  • ウィービング (Weaving): アスペクトをターゲットオブジェクトに組み込んでAOPプロキシを作成するプロセス。Spring AOPでは実行時に行われる。
  1. spring-boot-starter-aop 依存関係を追加 (Spring Bootの場合)。
  2. アスペクトクラスに @Aspect@Component (または他のステレオタイプアノテーション) を付与。
  3. @Pointcut でアドバイスを適用する対象メソッドを指定する式を定義。
  4. 実行タイミングに応じたアドバイスアノテーション (@Before, @AfterReturning, @AfterThrowing, @After, @Around) を付与したメソッドを定義し、その中で横断的処理を実装。

ポイントカット式の例:

  • execution(* com.example.service.*.*(..)): com.example.service パッケージ内の全クラスの全メソッド。
  • execution(public String com.example.service.MyService.getData(Long)): 特定のメソッド。
  • within(com.example.service..*): com.example.service パッケージおよびそのサブパッケージ内の全メソッド。
  • @annotation(com.example.annotation.Loggable): @Loggable アノテーションが付与されたメソッド。
  • bean(*Service): Bean名が “Service” で終わるBeanの全メソッド。
  • 式の結合: && (AND), || (OR), ! (NOT)
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Arrays;

@Aspect // このクラスがアスペクトであることを示す
@Component // DIコンテナで管理されるようにする
public class LoggingAspect {

    private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);

    // ポイントカット定義: com.example.service パッケージ配下のすべてのpublicメソッドを対象
    @Pointcut("execution(public * com.example.service..*.*(..))")
    private void serviceLayerExecution() {}

    // ポイントカット定義: @Loggable アノテーションが付与されたメソッドを対象
    @Pointcut("@annotation(com.example.annotation.Loggable)")
    private void loggableMethod() {}

    // Beforeアドバイス: ターゲットメソッド実行前に実行
    @Before("serviceLayerExecution()") // 上で定義したポイントカット名を参照
    public void logBefore(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().toShortString();
        Object[] args = joinPoint.getArgs();
        logger.info("メソッド開始: {} 引数: {}", methodName, Arrays.toString(args));
    }

    // AfterReturningアドバイス: ターゲットメソッドが正常に終了した後に実行
    @AfterReturning(pointcut = "serviceLayerExecution()", returning = "result")
    public void logAfterReturning(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().toShortString();
        logger.info("メソッド正常終了: {} 戻り値: {}", methodName, result);
    }

    // AfterThrowingアドバイス: ターゲットメソッドが例外をスローした後に実行
    @AfterThrowing(pointcut = "serviceLayerExecution()", throwing = "exception")
    public void logAfterThrowing(JoinPoint joinPoint, Throwable exception) {
        String methodName = joinPoint.getSignature().toShortString();
        logger.error("メソッド例外発生: {} 例外: {}", methodName, exception.getMessage());
    }

    // Afterアドバイス: ターゲットメソッドの実行後 (正常終了/例外発生問わず) に実行 (finallyのような動作)
    @After("serviceLayerExecution()")
    public void logAfter(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().toShortString();
        logger.info("メソッド終了 (finally): {}", methodName);
    }

    // Aroundアドバイス: ターゲットメソッドの実行前後を完全に制御 (最も強力)
    // ProceedingJoinPoint を引数に取り、 pjp.proceed() でターゲットメソッドを実行する必要がある
    @Around("loggableMethod()") // @Loggable アノテーションが付いたメソッドに適用
    public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
        String methodName = pjp.getSignature().toShortString();
        long startTime = System.currentTimeMillis();
        logger.info("Around - メソッド開始: {}", methodName);
        Object result;
        try {
            result = pjp.proceed(); // ターゲットメソッドの実行
            long elapsedTime = System.currentTimeMillis() - startTime;
            logger.info("Around - メソッド正常終了: {} 実行時間: {} ms 戻り値: {}", methodName, elapsedTime, result);
        } catch (Throwable throwable) {
            long elapsedTime = System.currentTimeMillis() - startTime;
            logger.error("Around - メソッド例外発生: {} 実行時間: {} ms 例外: {}", methodName, elapsedTime, throwable.getMessage());
            throw throwable; // 例外を再スローしないと呼び出し元に伝わらない
        }
        return result;
    }
}

// アノテーションの定義例
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
}

// サービス例
@Service
public class MySampleService {
    public String processData(String data) {
        System.out.println("データ処理中: " + data);
        return "Processed: " + data;
    }

    @Loggable // このメソッドに @Around アドバイスが適用される
    public int calculate(int a, int b) {
        System.out.println("計算中...");
        if (b == 0) throw new IllegalArgumentException("ゼロ除算エラー");
        return a / b;
    }
}

テスト 🧪

Springアプリケーションの品質を保証するためのテスト手法です。

テスト種類 概要 主なアノテーション/クラス 主なライブラリ
単体テスト (Unit Test) クラスやメソッド単体の機能をテスト。依存オブジェクトはモック化する。Springコンテキストは通常起動しない。 – (Plain JUnit Test) JUnit 5, Mockito, AssertJ
インテグレーションテスト (Integration Test) 複数のコンポーネントを組み合わせてテスト。Springコンテキストを起動し、DIやAOPなども含めてテストする。 @SpringBootTest, @ExtendWith(SpringExtension.class) Spring Test, JUnit 5, AssertJ
Webレイヤーテスト MVCコントローラーのテスト。Webサーバーを起動せず、リクエスト/レスポンスをモック化してテストする。 @WebMvcTest(controllers = MyController.class), @AutoConfigureMockMvc Spring Test, MockMvc, JUnit 5, Mockito, AssertJ
データアクセス層テスト JPAリポジトリのテスト。通常、インメモリデータベース (H2など) を使用する。 @DataJpaTest, @AutoConfigureTestDatabase Spring Test, Spring Data JPA, H2, JUnit 5, AssertJ
RESTクライアントテスト RestTemplateWebClient を使用した外部API呼び出し部分をテスト。 @RestClientTest(components = MyRestClient.class), MockRestServiceServer Spring Test, JUnit 5, AssertJ

依存コンポーネントをモック化し、テスト対象クラスのロジックのみを検証します。

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static org.mockito.Mockito.*;
import static org.assertj.core.api.Assertions.*;

@ExtendWith(MockitoExtension.class) // Mockitoのアノテーションを有効化
class MyServiceUnitTest {

    @Mock // モックオブジェクトを作成
    private UserRepository userRepository;

    @Mock
    private ExternalApiClient externalApiClient;

    @InjectMocks // @Mock アノテーションが付いたモックを注入するテスト対象クラス
    private MyService myService;

    @Test
    void findUserById_UserExists_ReturnsUserDto() {
        // --- Arrange (準備) ---
        Long userId = 1L;
        User userEntity = new User(userId, "testuser", "encodedPassword", "USER");
        // モックの振る舞いを定義 (userRepository.findById(1L) が呼ばれたら userEntity を含む Optional を返す)
        when(userRepository.findById(userId)).thenReturn(Optional.of(userEntity));

        // --- Act (実行) ---
        Optional<UserDto> result = myService.findUserById(userId);

        // --- Assert (検証) ---
        assertThat(result).isPresent();
        assertThat(result.get().getId()).isEqualTo(userId);
        assertThat(result.get().getUsername()).isEqualTo("testuser");

        // userRepository.findById(1L) が1回呼ばれたことを検証
        verify(userRepository, times(1)).findById(userId);
        // externalApiClient は呼ばれていないことを検証
        verifyNoInteractions(externalApiClient);
    }

    @Test
    void findUserById_UserNotExists_ReturnsEmptyOptional() {
        // --- Arrange ---
        Long userId = 2L;
        when(userRepository.findById(userId)).thenReturn(Optional.empty()); // ユーザーが見つからないケース

        // --- Act ---
        Optional<UserDto> result = myService.findUserById(userId);

        // --- Assert ---
        assertThat(result).isNotPresent();
        verify(userRepository, times(1)).findById(userId);
    }
}

アプリケーションコンテキスト全体または一部をロードしてテストします。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional; // テスト後のDB状態をロールバックするため
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;

@SpringBootTest
@Transactional // 各テストメソッド後にトランザクションをロールバック (DBテストの場合に有効)
class UserServiceIntegrationTest {

    @Autowired
    private UserService userService; // 実際の Service Bean をインジェクト

    @Autowired
    private UserRepository userRepository; // 実際の Repository Bean も利用可能

    @Test
    void createUser_Success() {
        // --- Arrange ---
        UserDto newUserDto = new UserDto(null, "integrationTestUser", "test@example.com");

        // --- Act ---
        UserDto createdUser = userService.createUser(newUserDto);

        // --- Assert ---
        assertThat(createdUser).isNotNull();
        assertThat(createdUser.getId()).isNotNull();
        assertThat(createdUser.getUsername()).isEqualTo("integrationTestUser");

        // DBに実際に保存されたか確認 (任意)
        Optional<User> found = userRepository.findById(createdUser.getId());
        assertThat(found).isPresent();
        assertThat(found.get().getEmail()).isEqualTo("test@example.com");
    }
}

コントローラーの動作を、HTTPリクエスト/レスポンスを模倣してテストします。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Optional;
import static org.mockito.BDDMockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.hamcrest.Matchers.*; // for jsonPath matchers

// UserController のみをテスト対象とする
@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc; // HTTPリクエストをシミュレートする

    @MockBean // Controller が依存する Service をモック化
    private UserService userService;

    @Test
    void getUserById_UserExists_ShouldReturnUserJson() throws Exception {
        // --- Arrange ---
        Long userId = 1L;
        UserDto userDto = new UserDto(userId, "mockUser", "mock@example.com");
        // モックの振る舞いを定義 (BDDMockitoスタイル)
        given(userService.findUserById(userId)).willReturn(Optional.of(userDto));

        // --- Act & Assert ---
        mockMvc.perform(get("/api/users/{id}", userId) // GET /api/users/1 を実行
                .accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk()) // HTTPステータスが 200 OK であることを期待
            .andExpect(content().contentType(MediaType.APPLICATION_JSON)) // レスポンスのContent-Typeを期待
            .andExpect(jsonPath("$.id", is(userId.intValue()))) // JSONの id フィールドを期待
            .andExpect(jsonPath("$.username", is("mockUser"))); // JSONの username フィールドを期待
    }

    @Test
    void getUserById_UserNotExists_ShouldReturnNotFound() throws Exception {
        // --- Arrange ---
        Long userId = 2L;
        given(userService.findUserById(userId)).willReturn(Optional.empty());

        // --- Act & Assert ---
        mockMvc.perform(get("/api/users/{id}", userId)
                .accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isNotFound()); // HTTPステータスが 404 Not Found であることを期待
    }

    @Test
    void createUser_ValidInput_ShouldReturnCreated() throws Exception {
        // --- Arrange ---
        UserDto inputDto = new UserDto(null, "newUser", "new@example.com");
        UserDto createdDto = new UserDto(10L, "newUser", "new@example.com");
        given(userService.createUser(any(UserDto.class))).willReturn(createdDto); // any() マッチャーを使用

        // --- Act & Assert ---
        mockMvc.perform(post("/api/users") // POST /api/users を実行
                .contentType(MediaType.APPLICATION_JSON) // リクエストボディの Content-Type
                .content("{\"username\": \"newUser\", \"email\": \"new@example.com\"}")) // リクエストボディ(JSON)
            .andExpect(status().isCreated()) // HTTPステータスが 201 Created であることを期待
            .andExpect(header().exists("Location")) // Location ヘッダーの存在を期待 (任意)
            .andExpect(jsonPath("$.id", is(10)))
            .andExpect(jsonPath("$.username", is("newUser")));
    }
}

テスト実行時に特定のプロファイルを有効化し、テスト用の設定 (例: インメモリDB) を使用します。

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import javax.sql.DataSource;
import static org.assertj.core.api.Assertions.*;

@SpringBootTest
@ActiveProfiles("test") // "test" プロファイルを有効化 (application-test.properties/.yml が読み込まれる)
class DatabaseConnectionTest {

    @Autowired
    private DataSource dataSource; // "test" プロファイルで設定されたDataSourceがインジェクトされる

    @Test
    void testDataSourceIsH2() throws Exception {
        // application-test.properties で H2 を設定していると想定
        String driverName = dataSource.getConnection().getMetaData().getDriverName();
        assertThat(driverName).containsIgnoringCase("H2");
        System.out.println("テスト用DBドライバ: " + driverName + " ✅");
    }
}

src/test/resources/application-test.properties:

# テスト用プロファイル設定
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=create-drop # テストごとにスキーマを再作成
spring.jpa.show-sql=false
logging.level.org.hibernate.SQL=DEBUG # SQLログをデバッグレベルで出力 (任意)

その他便利な機能 🛠️

メソッドの実行結果をキャッシュし、次回以降の呼び出しを高速化します。@EnableCaching で有効化し、@Cacheable, @CacheEvict, @CachePut などのアノテーションを使用します。

  • @Cacheable("cacheName"): メソッドの戻り値をキャッシュ。同じ引数で呼ばれたらキャッシュから返す。
  • @CacheEvict("cacheName", key="#arg"): キャッシュエントリを削除。allEntries=true で全削除。
  • @CachePut("cacheName", key="#result.id"): メソッドは常に実行し、戻り値でキャッシュを更新。
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Service;

@Configuration
@EnableCaching // キャッシュ機能を有効化
public class CacheConfig {
    // CacheManager の Bean 定義 (例: ConcurrentMapCacheManager, EhCacheCacheManager, RedisCacheManager など)
    // Spring Boot が適切な CacheManager を自動構成してくれる場合もある
}

@Service
public class ProductServiceWithCache {
    // ... repository injection

    @Cacheable("products") // "products" という名前のキャッシュ領域を使用
    public Product findProductById(Long id) {
        System.out.println("DBから商品情報を取得します (ID: " + id + ")");
        // 時間のかかる処理 (DBアクセスなど)
        return productRepository.findById(id).orElse(null);
    }

    @CacheEvict(value = "products", key = "#product.id") // key は SpEL で指定
    public Product updateProduct(Product product) {
        System.out.println("商品情報を更新し、キャッシュを削除します (ID: " + product.getId() + ")");
        return productRepository.save(product);
    }

    @CacheEvict(value = "products", allEntries = true) // "products" キャッシュの全エントリを削除
    public void clearProductCache() {
        System.out.println("商品キャッシュをすべてクリアしました。");
    }
}

指定したスケジュールでメソッドを定期実行します。@EnableScheduling で有効化し、@Scheduled アノテーションを使用します。

  • fixedRate: 前回の実行開始時刻から指定ミリ秒後に実行。
  • fixedDelay: 前回の実行終了時刻から指定ミリ秒後に実行。
  • cron: cron 式でスケジュールを指定。
  • initialDelay: 最初の実行までの遅延時間 (ミリ秒)。
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;

@Configuration
@EnableScheduling // スケジューリングを有効化
public class SchedulingConfig {
}

@Component
public class ScheduledTasks {
    private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);

    // 5秒ごとに実行 (前回の開始から5秒後)
    @Scheduled(fixedRate = 5000)
    public void reportCurrentTimeFixedRate() {
        log.info("Fixed Rate タスク実行: {}", LocalDateTime.now());
        // Thread.sleep(1000); // この処理時間も次の開始時間に影響する
    }

    // 10秒ごとに実行 (前回の終了から10秒後)
    @Scheduled(fixedDelay = 10000)
    public void reportCurrentTimeFixedDelay() {
        log.info("Fixed Delay タスク実行: {}", LocalDateTime.now());
        try {
            Thread.sleep(2000); // この処理時間は次の開始時間に影響しない
        } catch (InterruptedException e) { /* ... */ }
    }

    // 毎日午前3時に実行 (cron式)
    @Scheduled(cron = "0 0 3 * * ?") // 秒 分 時 日 月 曜日
    public void runDailyBatch() {
        log.info("日次バッチ処理を実行します: {}", LocalDateTime.now());
        // ... バッチ処理 ...
    }

    // 起動後3秒待ってから、15秒ごと (fixedDelay) に実行
    @Scheduled(initialDelay = 3000, fixedDelay = 15000)
    public void delayedTask() {
        log.info("遅延実行タスク: {}", LocalDateTime.now());
    }
}

注意: デフォルトではシングルスレッドで実行されるため、実行時間の長いタスクがあると他のタスクの実行が遅延する可能性があります。必要に応じてタスク実行用のスレッドプールを設定します。

ノンブロッキングI/OをベースとしたリアクティブなWebアプリケーションを構築するためのフレームワークです。高負荷・高同時接続性が求められるシステムに適しています。

  • spring-boot-starter-webflux を使用。
  • Reactor (Mono, Flux) や RxJava などのリアクティブライブラリを利用。
  • アノテーションベース (Spring MVC風) と関数型エンドポイント (Functional Endpoints) の2つのプログラミングモデルを提供。
  • デフォルトの組み込みサーバーは Netty。
// アノテーションベースのコントローラー例
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Duration;

@RestController
@RequestMapping("/reactive/users")
public class ReactiveUserController {

    private final ReactiveUserRepository userRepository; // リアクティブ対応リポジトリ (例: Spring Data R2DBC)

    public ReactiveUserController(ReactiveUserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // 全ユーザーをストリームとして返す (非同期)
    @GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE) // Server-Sent Events
    public Flux<User> getAllUsersStream() {
        return userRepository.findAll().delayElements(Duration.ofSeconds(1)); // 1秒ごとに返す
    }

    // IDでユーザーを検索 (非同期)
    @GetMapping("/{id}")
    public Mono<ResponseEntity<User>> getUserById(@PathVariable Long id) {
        return userRepository.findById(id)
                .map(user -> ResponseEntity.ok(user)) // 見つかった場合
                .defaultIfEmpty(ResponseEntity.notFound().build()); // 見つからなかった場合
    }

    // ユーザーを作成 (非同期)
    @PostMapping
    public Mono<ResponseEntity<User>> createUser(@RequestBody Mono<User> userMono) {
        return userMono
                .flatMap(userRepository::save) // Monoの中身を取り出して保存
                .map(savedUser -> ResponseEntity.status(HttpStatus.CREATED).body(savedUser));
    }
}

コメント

タイトルとURLをコピーしました