DI (Dependency Injection) コンテナ
Spring Core の中心的な機能で、オブジェクト間の依存関係を外部から注入します。
Bean定義方法
Springコンテナに管理させるオブジェクト(Bean)を定義する方法です。
定義方法 | 概要 | 主なアノテーション/要素 | コード例 (抜粋) |
---|---|---|---|
XMLベース定義 | 設定ファイル (XML) にBean定義を記述します。伝統的な方法です。 | <bean> , <constructor-arg> , <property> |
|
JavaConfigベース定義 | Javaクラス内に @Configuration アノテーションを付与し、@Bean アノテーションを付与したメソッドでBeanを定義します。現在主流の方法です。 | @Configuration , @Bean , @Import |
|
アノテーションベース定義 (コンポーネントスキャン) | クラスに @Component やその派生アノテーション (@Service , @Repository , @Controller など) を付与し、コンポーネントスキャン機能で自動的にBeanとして登録します。 | @Component , @Service , @Repository , @Controller , @RestController , @Configuration , @ComponentScan |
|
インジェクション方法
Bean間の依存関係を注入する方法です。
インジェクション方法 | 概要 | 主なアノテーション | コード例 |
---|---|---|---|
コンストラクタインジェクション | 推奨される方法 。依存関係をコンストラクタの引数で受け取ります。不変性 (Immutability) を保証しやすく、依存関係が明確になります。Spring 4.3以降、対象クラスのコンストラクタが一つだけの場合、@Autowired は省略可能です。 | @Autowired (省略可能な場合あり) |
|
セッターインジェクション | セッターメソッド経由で依存関係を注入します。任意 (Optional) の依存関係や、循環参照を解決する場合に使用されることがあります。 | @Autowired |
|
フィールドインジェクション | フィールドに直接 @Autowired を付与して注入します。コードは簡潔になりますが、テストがしにくくなる、不変性が保証できないなどのデメリットがあり、非推奨 とされることが多いです。 | @Autowired |
|
Beanスコープ
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が破棄されます... "); } // ...
}
プロファイル (@Profile)
特定の環境(開発、テスト、本番など)でのみ有効になる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 (@Conditional)
特定の条件を満たす場合にのみ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 属性を指定可能。 |
|
@PathVariable | URLパスの一部を動的な値として取得します (例: /users/{id} の id )。 |
|
@RequestBody | リクエストボディの内容(通常はJSONやXML)をJavaオブジェクトにデシリアライズして受け取ります。HttpMessageConverter が使用されます。 |
|
@RequestHeader | リクエストヘッダーの値を取得します。 |
|
@CookieValue | クッキーの値を取得します。 |
|
@ModelAttribute | リクエストパラメータやフォームデータをオブジェクトにバインドします。また、モデルに属性を追加するためにも使用されます。 |
|
レスポンス生成
コントローラーメソッドからクライアントへのレスポンスを生成する方法です。
- ビュー名を返す (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."); }
}
バリデーション (JSR-380 / Jakarta Bean Validation)
リクエストデータ (@RequestBody
や @ModelAttribute
で受け取ったオブジェクト) の妥当性チェックを行います。
hibernate-validator
などの Bean Validation 実装ライブラリを依存関係に追加します。- 検証対象のクラスのフィールドにバリデーションアノテーション (
@NotNull
,@Size
,@Email
,@Pattern
など) を付与します。 - コントローラーメソッドの引数に
@Valid
(または@Validated
) アノテーションを付与します。 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操作(作成, 読み取り, 更新, 削除)メソッドが自動的に実装されます。
- エンティティクラス (
@Entity
) を定義します。 JpaRepository<エンティティクラス, 主キーの型>
を継承したインターフェースを作成します。- 必要に応じてカスタムクエリメソッドを追加します。
// エンティティクラス
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)
一連のデータベース操作をアトミック(不可分)に実行するための仕組みです。メソッドまたはクラスに @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アプリケーションの開発を迅速かつ容易にするためのフレームワークです。「設定より規約」の原則に基づき、多くの定型的な設定を自動化します。
スターター依存関係 (Starters)
特定の機能を利用するために必要なライブラリ群をまとめたものです。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>
自動構成 (@EnableAutoConfiguration)
クラスパス上のライブラリや既存の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
を追加します。
Actuator
アプリケーションの監視と管理のための機能を提供します。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
テスト (@SpringBootTest)
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
:RestTemplate
やWebClient
を使用したクライアントサイドのテスト。@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, PasswordEncoder)
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では実行時に行われる。
アスペクト定義とアドバイス
spring-boot-starter-aop
依存関係を追加 (Spring Bootの場合)。- アスペクトクラスに
@Aspect
と@Component
(または他のステレオタイプアノテーション) を付与。 @Pointcut
でアドバイスを適用する対象メソッドを指定する式を定義。- 実行タイミングに応じたアドバイスアノテーション (
@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クライアントテスト | RestTemplate や WebClient を使用した外部API呼び出し部分をテスト。 | @RestClientTest(components = MyRestClient.class) , MockRestServiceServer | Spring Test, JUnit 5, AssertJ |
単体テスト (Mockito)
依存コンポーネントをモック化し、テスト対象クラスのロジックのみを検証します。
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); }
}
インテグレーションテスト (@SpringBootTest)
アプリケーションコンテキスト全体または一部をロードしてテストします。
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"); }
}
Webレイヤーテスト (@WebMvcTest, MockMvc)
コントローラーの動作を、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"))); }
}
テスト用プロファイル (@ActiveProfiles)
テスト実行時に特定のプロファイルを有効化し、テスト用の設定 (例: インメモリ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ログをデバッグレベルで出力 (任意)
その他便利な機能
Spring Cache
メソッドの実行結果をキャッシュし、次回以降の呼び出しを高速化します。@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("商品キャッシュをすべてクリアしました。"); }
}
Spring Scheduling
指定したスケジュールでメソッドを定期実行します。@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()); }
}
注意: デフォルトではシングルスレッドで実行されるため、実行時間の長いタスクがあると他のタスクの実行が遅延する可能性があります。必要に応じてタスク実行用のスレッドプールを設定します。
Spring WebFlux (リアクティブプログラミング)
ノンブロッキング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)); }
}