プロジェクト作成・初期設定
基本的なプロジェクトのセットアップ手順
主要な依存関係
目的 | 依存関係 (Maven/Gradle) | 主な用途 |
---|---|---|
Webアプリケーション開発 | spring-boot-starter-web | RESTful API、MVC Webアプリケーション構築 (Tomcat組込) |
データアクセス (JPA) | spring-boot-starter-data-jpa | Hibernateを利用したORM、リポジトリパターン |
ビューテンプレート (Thymeleaf) | spring-boot-starter-thymeleaf | サーバーサイドHTMLレンダリング |
セキュリティ | spring-boot-starter-security | 認証・認可機能の実装 |
設定プロパティ処理 | spring-boot-configuration-processor | @ConfigurationProperties のメタデータ生成 (IDE補完用) |
テスト | spring-boot-starter-test | JUnit 5, Spring Test, Mockito, AssertJ などを含むテスト基盤 |
運用監視 | spring-boot-starter-actuator | ヘルスチェック、メトリクス、環境情報などのエンドポイント提供 |
リラクティブWeb | spring-boot-starter-webflux | 非同期・ノンブロッキングWebアプリケーション (Netty組込) |
バリデーション | spring-boot-starter-validation | Bean Validation (JSR-380) のサポート |
設定ファイル
src/main/resources
配下に配置します。
application.properties
# サーバーポート設定
server.port=8080
# アプリケーション名
spring.application.name=my-spring-boot-app
# データベース接続設定 (例: H2)
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
# JPA/Hibernate設定
spring.jpa.hibernate.ddl-auto=update # create, create-drop, validate, none など
spring.jpa.show-sql=true # 実行されるSQLをログに出力
# Thymeleaf設定
spring.thymeleaf.cache=false # 開発中はキャッシュ無効化
application.yml
Properties形式の代わりにYAML形式も利用可能です。
server:
port: 8080
spring:
application:
name: my-spring-boot-app
datasource:
url: jdbc:h2:mem:testdb
driverClassName: org.h2.Driver
username: sa
password: password
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: update
show-sql: true
thymeleaf:
cache: false
プロファイル設定
環境ごとに設定を切り替えるためにプロファイルを使用します。
- ファイル名で指定:
application-{profile}.properties
orapplication-{profile}.yml
(例:application-dev.yml
,application-prod.properties
) - YAMLドキュメント区切り:
---
で区切り、spring.config.activate.on-profile: {profile}
を指定
# デフォルト設定 (共通)
server:
port: 8080
spring:
application:
name: my-app
---
# 開発環境 (dev) プロファイル
spring:
config:
activate:
on-profile: dev
datasource:
url: jdbc:h2:mem:devdb
jpa:
show-sql: true
---
# 本番環境 (prod) プロファイル
spring:
config:
activate:
on-profile: prod
datasource:
url: jdbc:mysql://prod-db.example.com:3306/mydb
username: prod_user
password: ${DB_PASSWORD} # 環境変数や外部設定から取得推奨
jpa:
hibernate:
ddl-auto: validate # 本番では validate や none を推奨
有効化するプロファイルは、環境変数 SPRING_PROFILES_ACTIVE
や JVM引数 -Dspring.profiles.active=dev
で指定します。
Web開発 (Spring MVC) 🌐
REST API や Web アプリケーションの構築
コントローラー
アノテーション | 説明 | 主な用途 |
---|---|---|
@RestController | @Controller と @ResponseBody の組み合わせ。各メソッドの戻り値が直接レスポンスボディとなる。 | REST API の開発 |
@Controller | Spring MVC のコントローラーを示す。メソッドの戻り値は通常、ビュー名を表す。 | 従来のMVC Webアプリケーション (Thymeleafなどビューと連携) |
リクエストマッピング
アノテーション | HTTPメソッド | 説明 | 例 |
---|---|---|---|
@RequestMapping | 指定可能 (デフォルトは全メソッド) | クラスレベル、メソッドレベルでパスとHTTPメソッドをマッピング | @RequestMapping("/api/users") @RequestMapping(value = "/{id}", method = RequestMethod.GET) |
@GetMapping | GET | GETリクエスト用のショートカット | @GetMapping("/items") |
@PostMapping | POST | POSTリクエスト用のショートカット | @PostMapping("/items") |
@PutMapping | PUT | PUTリクエスト用のショートカット | @PutMapping("/items/{id}") |
@DeleteMapping | DELETE | DELETEリクエスト用のショートカット | @DeleteMapping("/items/{id}") |
@PatchMapping | PATCH | PATCHリクエスト用のショートカット | @PatchMapping("/items/{id}") |
リクエストデータの取得
アノテーション | 取得対象 | 説明 | 例 |
---|---|---|---|
@PathVariable | パス変数 | URLパスの一部をメソッド引数にバインド | @GetMapping("/users/{userId}") public User getUser(@PathVariable Long userId) |
@RequestParam | クエリパラメータ フォームデータ | リクエストパラメータをメソッド引数にバインド。required , defaultValue 属性あり。 | @GetMapping("/search") public List<Item> search(@RequestParam String query, @RequestParam(required = false, defaultValue = "1") int page) |
@RequestBody | リクエストボディ | リクエストボディ (JSON, XMLなど) をオブジェクトにデシリアライズしてバインド。通常、POSTやPUTで使用。 | @PostMapping("/users") public User createUser(@RequestBody @Valid User newUser) |
@RequestHeader | リクエストヘッダー | 特定のリクエストヘッダーの値をメソッド引数にバインド。 | public void process(@RequestHeader("User-Agent") String userAgent) |
@CookieValue | クッキー | 特定のクッキーの値をメソッド引数にバインド。 | public void handle(@CookieValue("sessionId") String sessionId) |
(アノテーションなし) | フォームデータ クエリパラメータ | リクエストパラメータ名をフィールド名とするオブジェクトにバインド (JavaBean)。 | public String search(SearchForm form) // SearchForm のフィールドにバインド |
レスポンス
戻り値の型 / アノテーション | 説明 | 用途 |
---|---|---|
ResponseEntity<T> | レスポンスボディ、ステータスコード、ヘッダーを完全に制御可能。 | 詳細なレスポンス制御が必要な場合 (REST API) |
T (任意のオブジェクト) + @ResponseBody (@RestController 内では不要) | 戻り値オブジェクトが自動的にJSONなどにシリアライズされ、レスポンスボディとなる。ステータスコードは通常 200 OK。 | REST API のシンプルなレスポンス |
String (@Controller 内) | ビュー名 (例: Thymeleaf テンプレート名) を返す。 | MVC Web アプリケーションの画面遷移 |
void | レスポンスを直接操作する場合 (HttpServletResponse を引数に取るなど)。または、ステータスコードのみ返す場合 (@ResponseStatus と併用)。 | 特定の状況下でのレスポンス制御 |
ModelAndView | モデルデータとビュー名をまとめて返す。 | 従来の Spring MVC スタイル |
@GetMapping("/users/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
Optional<User> userOpt = userService.findById(id);
if (userOpt.isPresent()) {
return ResponseEntity.ok(userOpt.get()); // 200 OK と User オブジェクト
} else {
return ResponseEntity.notFound().build(); // 404 Not Found
}
}
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody @Valid User user) {
User createdUser = userService.createUser(user);
URI location = URI.create("/users/" + createdUser.getId());
return ResponseEntity.created(location).body(createdUser); // 201 Created と Location ヘッダー
}
エラーハンドリング
@ExceptionHandler
: コントローラー内で特定例外を処理するメソッドに付与。@ControllerAdvice
/@RestControllerAdvice
: グローバルな例外ハンドリングクラスに付与。@ExceptionHandler
と組み合わせて使用。ResponseStatusException
: 例外発生時に特定のHTTPステータスコードとメッセージを返すための例外クラス。
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFound(ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse("RESOURCE_NOT_FOUND", ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationExceptions(MethodArgumentNotValidException ex) {
// エラー詳細を取得して整形するなど
String message = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
ErrorResponse error = new ErrorResponse("VALIDATION_ERROR", message);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
// 予期せぬエラー
ErrorResponse error = new ErrorResponse("INTERNAL_SERVER_ERROR", "An unexpected error occurred.");
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
// ErrorResponse DTO (例)
record ErrorResponse(String code, String message) {}
}
// カスタム例外 (例)
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
CORS (Cross-Origin Resource Sharing) 設定
異なるオリジンからのリクエストを許可するための設定。
@CrossOrigin
アノテーション: コントローラーやメソッド単位で設定。- グローバル設定:
WebMvcConfigurer
を実装したクラスで設定。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**") // 対象パスパターン
.allowedOrigins("http://localhost:3000", "https://example.com") //許可するオリジン
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") //許可するHTTPメソッド
.allowedHeaders("*") //許可するヘッダー
.allowCredentials(true) //クッキー許可
.maxAge(3600); // pre-flightリクエストのキャッシュ時間(秒)
}
}
データアクセス (Spring Data JPA) 💾
データベース操作の簡略化
エンティティ定義
JPA の標準アノテーションを使用して、Java クラスをデータベーステーブルにマッピングします。
例: User エンティティ
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity // このクラスがJPAエンティティであることを示す
@Table(name = "users") // 対応するテーブル名を指定 (省略するとクラス名が使われる)
public class User {
@Id // プライマリキーを示す
@GeneratedValue(strategy = GenerationType.IDENTITY) // ID自動生成戦略 (DB依存)
private Long id;
@Column(nullable = false, unique = true, length = 100) // カラム制約
private String username;
@Column(nullable = false)
private String password; // 注意: パスワードはハッシュ化して保存すること
@Column(unique = true)
private String email;
@Column(name = "created_at", updatable = false) // カラム名指定、更新不可
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// コンストラクタ、Getter/Setter、equals/hashCode など (Lombok を使うと簡潔)
@PrePersist // 永続化前に実行
protected void onCreate() {
createdAt = updatedAt = LocalDateTime.now();
}
@PreUpdate // 更新前に実行
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}
リポジトリインターフェース
JpaRepository
またはその親インターフェース (CrudRepository
, PagingAndSortingRepository
) を継承して作成します。
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.List;
@Repository // データアクセス層のコンポーネントであることを示す (省略可能)
public interface UserRepository extends JpaRepository<User, Long> { // <エンティティクラス, 主キーの型>
// メソッド名からクエリを自動生成 (Query Methods)
Optional<User> findByUsername(String username);
List<User> findByEmailContainingIgnoreCase(String email);
List<User> findByCreatedAtAfter(LocalDateTime dateTime);
// exists クエリ
boolean existsByEmail(String email);
// delete クエリ
long deleteByUsername(String username);
}
基本的なCRUD操作
JpaRepository
が提供するメソッドを利用します。
メソッド | 説明 |
---|---|
save(S entity) | エンティティの保存 (新規作成または更新) |
saveAll(Iterable<S> entities) | 複数のエンティティを一括保存 |
findById(ID id) | ID でエンティティを検索 (Optional<T> を返す) |
existsById(ID id) | 指定した ID のエンティティが存在するか確認 |
findAll() | すべてのエンティティを取得 |
findAllById(Iterable<ID> ids) | 複数の ID でエンティティを検索 |
count() | エンティティの総数を取得 |
deleteById(ID id) | ID でエンティティを削除 |
delete(T entity) | エンティティを指定して削除 |
deleteAll(Iterable<? extends T> entities) | 複数のエンティティを一括削除 |
deleteAll() | すべてのエンティティを削除 (注意して使用) |
カスタムクエリ (`@Query`)
メソッド名だけでは表現できない複雑なクエリは JPQL や ネイティブ SQL で記述します。
例: @Query の使用
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface UserRepository extends JpaRepository<User, Long> {
// JPQL (Java Persistence Query Language)
@Query("SELECT u FROM User u WHERE u.email LIKE %:keyword% OR u.username LIKE %:keyword%")
List<User> searchByKeyword(@Param("keyword") String keyword);
// ネイティブ SQL (データベース固有のSQL)
@Query(value = "SELECT * FROM users WHERE YEAR(created_at) = :year", nativeQuery = true)
List<User> findUsersCreatedInYear(@Param("year") int year);
// 更新/削除クエリ ( @Modifying と @Transactional が必要になる場合が多い)
@Modifying
@Query("UPDATE User u SET u.email = :newEmail WHERE u.id = :id")
int updateUserEmail(@Param("id") Long id, @Param("newEmail") String newEmail);
}
ページングとソート
JpaRepository
(または PagingAndSortingRepository
) を継承し、メソッドの引数に Pageable
を追加します。
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ItemRepository extends JpaRepository<Item, Long> {
// 全件検索 (ページング・ソート付き)
Page<Item> findAll(Pageable pageable);
// 条件付き検索 (ページング・ソート付き)
Page<Item> findByCategory(String category, Pageable pageable);
}
// サービス層での利用例
@Service
public class ItemService {
@Autowired
private ItemRepository itemRepository;
public Page<Item> findItems(int page, int size, String sortField, String sortDirection) {
Sort sort = Sort.by(Sort.Direction.fromString(sortDirection), sortField);
Pageable pageable = PageRequest.of(page, size, sort);
return itemRepository.findAll(pageable);
}
}
Pageable
の作成: PageRequest.of(pageNumber, pageSize, sort)
Page
オブジェクトからは、コンテンツリスト (getContent()
)、総ページ数 (getTotalPages()
)、総要素数 (getTotalElements()
) などを取得できます。
トランザクション管理 (`@Transactional`)
複数のデータ操作を一つの作業単位として扱うためのアノテーション。
- 通常、サービスクラスやそのメソッドに付与します。
- メソッドが正常終了すればコミット、例外が発生すればロールバックされます。
readOnly = true
: 読み取り専用トランザクション (最適化)propagation
: トランザクションの伝播動作 (REQUIRED
,REQUIRES_NEW
など)isolation
: トランザクション分離レベル (READ_COMMITTED
,SERIALIZABLE
など)rollbackFor
/noRollbackFor
: ロールバック対象/非対象の例外を指定
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
@Autowired private OrderRepository orderRepository;
@Autowired private OrderItemRepository orderItemRepository;
@Autowired private InventoryService inventoryService; // 在庫管理サービス
@Transactional // このメソッド全体が1つのトランザクションになる
public Order placeOrder(OrderRequest request) {
// 注文情報作成
Order order = new Order(/* ... */);
orderRepository.save(order);
// 注文明細作成 & 在庫引き当て
for (OrderItemRequest itemRequest : request.getItems()) {
inventoryService.decreaseStock(itemRequest.getProductId(), itemRequest.getQuantity()); // 在庫がなければ例外発生
OrderItem item = new OrderItem(order, /* ... */);
orderItemRepository.save(item);
}
// 例外が発生しなければ、ここまでのDB操作がコミットされる
// 例外が発生すれば、全てのDB操作がロールバックされる
return order;
}
@Transactional(readOnly = true) // 読み取り専用
public Order getOrderDetails(Long orderId) {
return orderRepository.findById(orderId)
.orElseThrow(() -> new ResourceNotFoundException("Order not found"));
}
}
@Transactional
を有効にするには、@EnableTransactionManagement
を設定クラスに追加する必要がありますが、Spring Boot では通常自動構成されます。@Transactional
は public メソッドにのみ適用されます。同じクラス内のメソッド呼び出しではトランザクション境界が適用されない場合があるので注意が必要です (プロキシの問題)。DI (Dependency Injection) 💉
コンポーネント間の依存関係の管理
コンポーネントスキャン
@SpringBootApplication
: このアノテーションが付与されたクラスのパッケージ以下を自動的にスキャンします (@ComponentScan
を内包)。@ComponentScan
: スキャン対象のベースパッケージを明示的に指定する場合に使用。
Bean 定義 (ステレオタイプアノテーション)
アノテーション | 説明 | 主な用途 |
---|---|---|
@Component | 汎用的なコンポーネントを示す基本的なアノテーション。 | 特定の層に属さないユーティリティクラスなど。 |
@Service | ビジネスロジック層のコンポーネントを示す。@Component の特殊化。 | サービスクラス。 |
@Repository | データアクセス層 (永続化層) のコンポーネントを示す。@Component の特殊化。例外変換機能を持つ。 | DAO (Data Access Object) や Spring Data JPA リポジトリインターフェース。 |
@Controller | プレゼンテーション層 (Web) のコントローラーを示す。@Component の特殊化。 | Spring MVC コントローラー。 |
@RestController | REST API 用のコントローラーを示す。@Controller + @ResponseBody 。 | REST API コントローラー。 |
@Configuration | 設定クラスを示す。@Bean メソッドを持つことができる。@Component の特殊化。 | Javaベースの設定クラス。 |
Bean のインジェクション (依存性の注入)
方法 | 説明 | 推奨度 | 例 |
---|---|---|---|
コンストラクタインジェクション | クラスのコンストラクタ引数に必要な依存性を定義する。Spring が自動で Bean を探し注入する。 | 推奨 (不変性、テスト容易性) |
|
セッターインジェクション | 依存性を注入するためのセッターメソッドを用意し、@Autowired を付与する。 | 任意依存 (Optional) の場合など |
|
フィールドインジェクション | フィールドに直接 @Autowired を付与する。 | 非推奨 (テスト困難、DIコンテナ依存) |
|
インジェクション候補の限定 (`@Qualifier`)
同じ型の Bean が複数存在する場合、どちらを注入するかを指定します。
例: @Qualifier の使用
// Bean 定義
@Component("emailNotificationService")
public class EmailNotificationService implements NotificationService { ... }
@Component("smsNotificationService")
public class SmsNotificationService implements NotificationService { ... }
// インジェクション
@Service
public class UserRegistrationService {
private final NotificationService notificationService;
// "emailNotificationService" という名前の Bean を注入
public UserRegistrationService(@Qualifier("emailNotificationService") NotificationService notificationService) {
this.notificationService = notificationService;
}
// ...
}
プロファイルによる Bean 切り替え (`@Profile`)
特定のプロファイルがアクティブな場合にのみ Bean を有効化します。
例: @Profile の使用
// 開発環境用ダミーサービス
@Service
@Profile("dev") // "dev" プロファイルが有効な場合のみ Bean として登録
public class DevMailSender implements MailSender { ... }
// 本番環境用実サービス
@Service
@Profile("prod") // "prod" プロファイルが有効な場合のみ Bean として登録
public class ProdMailSender implements MailSender { ... }
// MailSender を利用するクラス
@Service
public class NotificationService {
private final MailSender mailSender;
public NotificationService(MailSender mailSender) { // 実行時のプロファイルに応じて適切な Bean が注入される
this.mailSender = mailSender;
}
// ...
}
設定管理 ⚙️
アプリケーション設定値の扱い方
`@Value` アノテーション
設定ファイル (application.properties
/.yml
) や環境変数から個々のプロパティ値をフィールドにインジェクトします。
@Component
public class AppInfo {
@Value("${spring.application.name}") // application.properties/yml から値を取得
private String appName;
@Value("${server.port}")
private int port;
@Value("${myapp.feature.enabled:false}") // デフォルト値付き (プロパティが存在しない場合 false)
private boolean featureEnabled;
@Value("${JAVA_HOME}") // 環境変数も取得可能
private String javaHome;
// ... Getters or usage
}
`@ConfigurationProperties`
関連するプロパティ群を型安全なオブジェクト (POJO) にまとめてバインドします。
- プレフィックスを指定して、そのプレフィックスで始まるプロパティをマッピングします。
- クラスまたは
@Bean
メソッドに付与します。 @EnableConfigurationProperties(YourPropertiesClass.class)
を設定クラスに追加するか、対象クラスに@Configuration
または@Component
などを付与します。- バリデーション (
@Validated
と Bean Validation アノテーション) も可能です。
myapp:
api:
url: https://api.example.com
timeout-ms: 5000
retry-count: 3
feature:
new-ui:
enabled: true
例: @ConfigurationProperties クラス
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
@ConfigurationProperties(prefix = "myapp") // "myapp" プレフィックスのプロパティをバインド
@Validated // バリデーションを有効化
public class MyAppProperties {
private final Api api = new Api();
private final Feature feature = new Feature();
// Getter が必要
public Api getApi() { return api; }
public Feature getFeature() { return feature; }
// ネストしたプロパティ用の内部クラス
public static class Api {
@NotBlank // バリデーションルール
private String url;
@Positive
private int timeoutMs = 3000; // デフォルト値
@Positive
private int retryCount = 1; // デフォルト値
// Getters and Setters が必要
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public int getTimeoutMs() { return timeoutMs; }
public void setTimeoutMs(int timeoutMs) { this.timeoutMs = timeoutMs; }
public int getRetryCount() { return retryCount; }
public void setRetryCount(int retryCount) { this.retryCount = retryCount; }
}
public static class Feature {
private final NewUi newUi = new NewUi();
public NewUi getNewUi() { return newUi; }
public static class NewUi {
private boolean enabled = false; // デフォルト値
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
}
}
}
// 設定クラスで有効化
@Configuration
@EnableConfigurationProperties(MyAppProperties.class)
public class AppConfig {
// MyAppProperties Bean が利用可能になる
}
// 利用例
@Service
public class SomeService {
private final MyAppProperties props;
public SomeService(MyAppProperties props) {
this.props = props;
String apiUrl = props.getApi().getUrl();
boolean isNewUiEnabled = props.getFeature().getNewUi().isEnabled();
// ...
}
}
設定の優先順位
Spring Boot は様々な場所から設定を読み込み、以下の順序 (上が高優先) で上書きします。
- Devtools のグローバル設定 (
~/.spring-boot-devtools.properties
) - テスト中の
@TestPropertySource
- テスト中の
properties
属性 (@SpringBootTest
など) - コマンドライン引数 (
--server.port=9000
) SPRING_APPLICATION_JSON
からのプロパティ (環境変数やシステムプロパティのインラインJSON)ServletConfig
初期化パラメータServletContext
初期化パラメータjava:comp/env
の JNDI 属性- Java システムプロパティ (
System.getProperties()
) - OS 環境変数
random.*
プロパティ (RandomValuePropertySource
)- jar 外のプロファイル固有
application-{profile}.properties/yml
- jar 内のプロファイル固有
application-{profile}.properties/yml
- jar 外の
application.properties/yml
- jar 内の
application.properties/yml
@Configuration
クラスの@PropertySource
- デフォルトプロパティ (
SpringApplication.setDefaultProperties
)
application.properties
/.yml
、プロファイル固有ファイル、環境変数、コマンドライン引数を主に利用します。外部設定 (Config Server など) を利用すると、さらに柔軟な管理が可能です。セキュリティ (Spring Security) 🛡️
認証と認可の実装
spring-boot-starter-security
依存関係を追加すると、基本的な認証 (HTTP Basic 認証、フォームログイン) が自動構成されます。カスタマイズには設定クラスが必要です。基本的な設定クラス
SecurityFilterChain
Bean を定義して、HTTP セキュリティルールを設定します。
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
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("/public/**", "/h2-console/**").permitAll() // 認証不要なパス
.requestMatchers("/admin/**").hasRole("ADMIN") // ADMIN ロールが必要なパス
.anyRequest().authenticated() // その他のリクエストは認証が必要
)
.formLogin(withDefaults()) // フォームベースログインを有効化 (デフォルト設定)
.httpBasic(withDefaults()) // HTTP Basic 認証を有効化 (デフォルト設定)
.csrf(csrf -> csrf.ignoringRequestMatchers("/h2-console/**")) // H2コンソール用にCSRF無効化 (開発時)
.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.sameOrigin())); // H2コンソール用にX-Frame-Options設定
return http.build();
}
// パスワードエンコーダーの Bean 定義 (必須)
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// --- 認証方法の選択 (以下のいずれか、またはカスタム実装) ---
// 1. インメモリ認証 (テスト・開発用)
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder.encode("password")) // パスワードはエンコードする
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password(passwordEncoder.encode("admin"))
.roles("ADMIN", "USER")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
/*
// 2. JDBC 認証 (データベースを利用) - application.properties に datasource 設定が必要
@Bean
public UserDetailsService jdbcUserDetailsService(DataSource dataSource) {
// デフォルトのスキーマを利用する場合 (users, authorities テーブル)
return new JdbcUserDetailsManager(dataSource);
// カスタムクエリを指定する場合
// JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
// manager.setUsersByUsernameQuery("SELECT username, password, enabled FROM my_users WHERE username = ?");
// manager.setAuthoritiesByUsernameQuery("SELECT username, authority FROM my_authorities WHERE username = ?");
// return manager;
}
*/
/*
// 3. カスタム UserDetailsService (推奨される方法)
// UserRepository を利用してDBからユーザー情報を取得する実装
@Bean
public UserDetailsService customUserDetailsService(UserRepository userRepository) {
return username -> userRepository.findByUsername(username)
.map(user -> org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword()) // DBにはエンコード済みパスワードを保存
.roles("USER") // DBからロール情報を取得するロジックを追加
.build())
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
}
*/
}
主な設定要素
設定メソッド | 説明 |
---|---|
authorizeHttpRequests() | URL パターンごとにアクセス許可ルール (認証要否、ロール/権限) を設定。 |
formLogin() | フォームベースのログイン設定。ログインページ、成功/失敗時のハンドラなどをカスタマイズ可能。 |
httpBasic() | HTTP Basic 認証を設定。 |
logout() | ログアウト機能の設定。ログアウトURL、成功時の遷移先などをカスタマイズ可能。 |
csrf() | CSRF (Cross-Site Request Forgery) 保護の設定。無効化や特定のパスを除外可能。 |
sessionManagement() | セッション管理戦略 (ステートレス、セッション固定攻撃対策など) を設定。 |
oauth2Login() | OAuth 2.0 を利用したログインを設定 (Google, Facebook など)。 |
oauth2ResourceServer() | OAuth 2.0 リソースサーバーとしての設定 (JWT や Opaque Token の検証)。 |
addFilterBefore() / addFilterAfter() | カスタムフィルターをセキュリティフィルターチェーンに追加。JWT 認証フィルターなどで使用。 |
JWT (JSON Web Token) 認証
ステートレス API で一般的に使用される認証方式。
- ユーザーがログインすると、サーバーは JWT を生成して返す。
- クライアントは以降のリクエストで、Authorization ヘッダー (Bearer スキーマ) に JWT を含めて送信する。
- サーバーはリクエストごとに JWT を検証し、ユーザーを認証する。
実装には以下の要素が必要:
- JWT 生成・検証ライブラリ (例:
java-jwt
,jjwt
) - JWT の生成ロジック (ログイン成功時)
- JWT 検証フィルター (リクエストごとに実行され、
SecurityContextHolder
に認証情報を設定) - Spring Security 設定で、JWT フィルターを適切な位置に追加し、デフォルトのフォームログインなどを無効化。
テスト 🧪
アプリケーションの品質保証
spring-boot-starter-test
依存関係を追加すると、JUnit 5, Spring Test, Spring Boot Test, AssertJ, Mockito, Hamcrest, JSONassert, JsonPath などが含まれます。テストは src/test/java
配下に作成します。主要なテスト用アノテーション
アノテーション | 説明 | 主な用途 |
---|---|---|
@SpringBootTest | Spring Boot アプリケーションのコンテキスト全体をロードして統合テストを実行。 | アプリケーション全体の動作確認、コンポーネント間の連携テスト。 |
@WebMvcTest | Spring MVC コンポーネント (コントローラー、@ControllerAdvice など) に焦点を当てたスライステスト。Web レイヤーのみロード。 | コントローラーの単体テスト (リクエスト処理、レスポンス検証)。MockMvc を使用。 |
@DataJpaTest | JPA コンポーネント (リポジトリ、エンティティ) に焦点を当てたスライステスト。インメモリデータベース (H2 など) を自動構成。 | リポジトリの CRUD 操作、カスタムクエリのテスト。TestEntityManager を使用。 |
@RestClientTest | RestTemplate や WebClient を使用したクライアントサイドの REST 呼び出しテスト。 | 外部 API 呼び出しのモック化とテスト。MockRestServiceServer を使用。 |
@JsonTest | JSON のシリアライズ/デシリアライズに焦点を当てたテスト。 | DTO やエンティティの JSON 変換テスト。JacksonTester を使用。 |
@MockBean | Spring コンテキスト内の特定の Bean を Mockito モックに置き換える。 | 依存する Bean (サービス、リポジトリなど) の動作をモック化したい場合 (@SpringBootTest , @WebMvcTest などと併用)。 |
@SpyBean | Spring コンテキスト内の特定の Bean を Mockito スパイに置き換える (実際のメソッドを呼び出しつつ、一部をモック化可能)。 | 実際の Bean の動作を部分的に変更したい場合。 |
@TestPropertySource | テスト実行時に使用するプロパティソースを指定 (ファイルやインラインプロパティ)。 | テスト用の設定値を適用したい場合。 |
@ActiveProfiles | テスト実行時に有効化するプロファイルを指定。 | 特定のプロファイルでの動作をテストしたい場合。 |
@DirtiesContext | テストメソッドまたはクラス実行後にアプリケーションコンテキストを破棄・再作成するようマークする。 | テストがコンテキストの状態を変更し、後続のテストに影響を与える可能性がある場合。 |
テスト例
例: コントローラーテスト (@WebMvcTest)
import org.junit.jupiter.api.Test;
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 static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.hamcrest.Matchers.*;
@WebMvcTest(UserController.class) // UserController をテスト対象とする
public class UserControllerTest {
@Autowired
private MockMvc mockMvc; // MVCリクエストをシミュレート
@MockBean // UserService をモック化
private UserService userService;
@Test
void shouldGetUserById() throws Exception {
User user = new User(1L, "testuser", "test@example.com");
given(userService.findById(1L)).willReturn(Optional.of(user));
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.id", is(1)))
.andExpect(jsonPath("$.username", is("testuser")));
}
@Test
void shouldReturnNotFoundForMissingUser() throws Exception {
given(userService.findById(99L)).willReturn(Optional.empty());
mockMvc.perform(get("/api/users/99"))
.andExpect(status().isNotFound());
}
@Test
void shouldCreateUser() throws Exception {
User newUser = new User(null, "newuser", "new@example.com");
User createdUser = new User(2L, "newuser", "new@example.com");
given(userService.createUser(any(User.class))).willReturn(createdUser);
String userJson = "{\"username\":\"newuser\",\"email\":\"new@example.com\"}"; // Jackson などで生成推奨
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(userJson))
.andExpect(status().isCreated())
.andExpect(header().string("Location", "/api/users/2"))
.andExpect(jsonPath("$.id", is(2)))
.andExpect(jsonPath("$.username", is("newuser")));
}
}
例: リポジトリテスト (@DataJpaTest)
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; // JPA操作用ユーティリティ
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Optional;
@DataJpaTest // JPA関連のコンポーネントのみロード、デフォルトで H2 を使用
public class UserRepositoryTest {
@Autowired
private TestEntityManager entityManager; // テストデータの準備や永続化状態の確認
@Autowired
private UserRepository userRepository;
@Test
void shouldSaveAndFindUser() {
User user = new User(null, "jpatest", "jpa@test.com");
User savedUser = entityManager.persistFlushFind(user); // 保存してDBから取得
Optional<User> foundUserOpt = userRepository.findById(savedUser.getId());
assertThat(foundUserOpt).isPresent();
assertThat(foundUserOpt.get().getUsername()).isEqualTo("jpatest");
}
@Test
void shouldFindByUsername() {
User user1 = new User(null, "findme", "findme@test.com");
entityManager.persistAndFlush(user1);
User user2 = new User(null, "another", "another@test.com");
entityManager.persistAndFlush(user2);
Optional<User> foundUserOpt = userRepository.findByUsername("findme");
assertThat(foundUserOpt).isPresent();
assertThat(foundUserOpt.get().getId()).isEqualTo(user1.getId());
}
@Test
void shouldNotExistForUnknownUsername() {
Optional<User> foundUserOpt = userRepository.findByUsername("unknown");
assertThat(foundUserOpt).isNotPresent();
}
}
テストユーティリティ
MockMvc
: Spring MVC コントローラーのテスト用。HTTP リクエストをシミュレートし、レスポンスを検証。TestRestTemplate
: 実際の HTTP サーバーを起動するテスト (@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
) で、アプリケーションにリクエストを送信。TestEntityManager
: JPA テストでエンティティの永続化、検索、フラッシュなどを実行。AssertJ
: 流れるようなインターフェースを持つアサーションライブラリ (assertThat(...)
)。Mockito
: モックオブジェクト作成ライブラリ (mock()
,when()
,verify()
)。
Actuator (運用監視)
アプリケーションの状態監視と管理
spring-boot-starter-actuator
依存関係を追加すると、様々な監視用エンドポイントが利用可能になります。デフォルトでは /actuator/health
と /actuator/info
のみが公開されています (Web経由の場合)。主要なエンドポイント
application.properties
または .yml
で公開するエンドポイントを設定します。
management:
endpoints:
web:
exposure:
include: health, info, metrics, env, loggers, threaddump, heapdump # 公開するエンドポイントをカンマ区切りで指定 ( '*' ですべて公開)
# exclude: heapdump # 特定のエンドポイントを除外することも可能
endpoint:
health:
show-details: when_authorized # /health の詳細表示権限 (always, never, when_authorized)
# roles: ACTUATOR_ADMIN # 詳細表示に必要なロール (Spring Security連携時)
info:
enabled: true # /info エンドポイントの有効化 (デフォルトで有効)
info: # /info エンドポイントで表示する情報
app:
name: ${spring.application.name}
description: My Awesome Spring Boot Application
version: @project.version@ # Maven/Gradle プロジェクトのバージョン (ビルド時に置換)
# git: # git.properties があればGit情報も表示可能 (要 maven/gradle plugin)
# build: # build-info.properties があればビルド情報も表示可能 (要 maven/gradle plugin)
エンドポイントパス | 説明 | デフォルト公開 (Web) |
---|---|---|
/actuator/health | アプリケーションのヘルスチェック情報 (状態: UP/DOWN、詳細: DB接続、ディスク空き容量など)。 | Yes |
/actuator/info | アプリケーションの基本情報 (ビルド情報、Git情報、カスタム情報など)。 | Yes |
/actuator/metrics | アプリケーションの各種メトリクス情報 (JVMメモリ、CPU使用率、HTTPリクエスト数など)。Micrometer に基づく。 | No |
/actuator/metrics/{metric.name} | 特定のメトリクスの詳細情報を取得。 | No |
/actuator/env | Spring Environment のプロパティ (設定値、環境変数、システムプロパティなど) を表示。機密情報が含まれる可能性あり。 | No |
/actuator/loggers | アプリケーションのロガーとそのレベルを表示・変更可能。 | No |
/actuator/threaddump | JVM のスレッドダンプを取得。 | No |
/actuator/heapdump | JVM のヒープダンプを取得。ファイルサイズが大きい可能性あり。 | No |
/actuator/beans | Spring アプリケーションコンテキストに登録されているすべての Bean を表示。 | No |
/actuator/configprops | すべての @ConfigurationProperties Bean とその値を表示。 | No |
/actuator/mappings | すべての @RequestMapping パスとそのハンドラーメソッドを表示。 | No |
/actuator/scheduledtasks | アプリケーション内のスケジュールされたタスクを表示。 | No |
/actuator/shutdown | アプリケーションを正常にシャットダウン (デフォルトで無効、有効化には management.endpoint.shutdown.enabled=true が必要)。 | No |
セキュリティ設定
Actuator エンドポイントは機密情報を含む可能性があるため、本番環境では適切に保護する必要があります。
- Spring Security を使用して、エンドポイントへのアクセスを認証・認可する (推奨)。
- 異なるポート (
management.server.port
) やパス (management.endpoints.web.base-path
) で公開し、ネットワークレベルでアクセスを制限する。
// SecurityConfig 内に追加
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// ... (他の設定)
@Bean
@Order(1) // 他のセキュリティ設定より優先度を高くする
public SecurityFilterChain actuatorSecurityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/actuator/**") // /actuator/** パスにのみ適用
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/actuator/health", "/actuator/info").permitAll() // healthとinfoは認証不要
.anyRequest().hasRole("ACTUATOR_ADMIN") // 他のActuatorエンドポイントは ACTUATOR_ADMIN ロールが必要
)
.httpBasic(withDefaults()); // Basic認証を使用 (他の認証方法も可)
return http.build();
}
@Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
// ... アプリケーション本体のセキュリティ設定 ...
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(withDefaults());
return http.build();
}
// ... (PasswordEncoder, UserDetailsService の Bean 定義)
// ACTUATOR_ADMIN ロールを持つユーザーを作成する必要がある
}
非同期処理 ⚡
時間のかかる処理のバックグラウンド実行
有効化と基本設定
- 設定クラスに
@EnableAsync
アノテーションを付与。 - 非同期で実行したいメソッド (public) に
@Async
アノテーションを付与。
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
@Configuration
@EnableAsync // 非同期処理を有効化
public class AsyncConfig {
// 必要に応じて TaskExecutor のカスタマイズ Bean を定義
/*
@Bean(name = "taskExecutor")
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
executor.setThreadNamePrefix("Async-");
executor.initialize();
return executor;
}
*/
}
例: 非同期メソッド
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
public class EmailService {
@Async // このメソッドは別のスレッドで実行される
public void sendWelcomeEmail(String toAddress) {
System.out.println("Sending welcome email to " + toAddress + " on thread: " + Thread.currentThread().getName());
// 時間のかかるメール送信処理 (例)
try {
Thread.sleep(2000); // 2秒待機
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Email sent to " + toAddress);
}
@Async("taskExecutor") // 使用する TaskExecutor Bean を指定する場合
public CompletableFuture<Boolean> checkEmailStatus(String emailId) {
System.out.println("Checking email status for " + emailId + " on thread: " + Thread.currentThread().getName());
// 外部API呼び出しなどの時間のかかる処理
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return CompletableFuture.completedFuture(false);
}
boolean success = Math.random() > 0.5; // ダミーの結果
return CompletableFuture.completedFuture(success);
}
}
// 呼び出し側
@RestController
public class UserController {
@Autowired private EmailService emailService;
@PostMapping("/register")
public ResponseEntity<String> registerUser(@RequestBody UserRegistrationRequest request) {
// ... ユーザー登録処理 ...
// メール送信は非同期で実行 (呼び出し元は待たない)
emailService.sendWelcomeEmail(request.getEmail());
// 結果が必要な非同期処理
CompletableFuture<Boolean> statusFuture = emailService.checkEmailStatus("some-id");
// 他の処理を進める...
// ...
// 結果が必要になったら待機 (または非同期でハンドリング)
// try {
// Boolean status = statusFuture.get(); // 結果を同期的に取得 (ブロックする)
// System.out.println("Email status check result: " + status);
// } catch (InterruptedException | ExecutionException e) {
// // エラーハンドリング
// }
return ResponseEntity.ok("User registered, welcome email sending started.");
}
}
注意点
@Async
メソッドは public である必要があります。- 同じクラス内のメソッドから
@Async
メソッドを呼び出しても非同期にはなりません (プロキシ経由で呼び出される必要がある)。別クラスに切り出すか、自身をインジェクションするなどの工夫が必要です。 - 戻り値が
void
の場合、例外は呼び出し元に伝播しません。例外ハンドリングにはAsyncUncaughtExceptionHandler
を実装します。 - 戻り値が
Future
,CompletableFuture
, またはListenableFuture
の場合、呼び出し元で結果や例外を取得できます。 - デフォルトでは
SimpleAsyncTaskExecutor
が使用されますが、スレッドを再利用しないため、本番環境ではThreadPoolTaskExecutor
などでスレッドプールを設定することが推奨されます。
コメント