はじめに
今回も引き続きSpringチュートリアルの内容をまとめていきたいと思います
開発環境やIDEAについては下記記事参照
Caching Data with Spring(Spring でデータキャッシング)
日本語サイト
英語サイト
★キャッシュとは
Webサイトやアプリの表示速度を速くするために、一時的にデータを使用しているデバイスに保存する仕組み
1:Create a Book Model(ブックモデルを作成する)
- 「本の情報を扱うための設計図(モデル)」をJavaのコードにしたのが「Bookクラス」
package com.example.caching;
// Bookクラス、「本」というデータの設計図(モデル)
public class Book {
private String isbn; // 本の識別番号を保存する変数
private String title; // 本のタイトルを保存する変数
// コンストラクタ。newした際に自動的に呼び出されるメソッド
public Book(String isbn, String title) {
// 左のthis.isbnはこのクラスのフィールド(変数)、右のisbnは外から渡される値(引数)
this.isbn = isbn;
this.title = title;
}
// isbnの値を取得(取り出す)するためのメソッド
public String getIsbn() {
return isbn;
}
// isbnの値を設定(変更)するためのメソッド
public String setIsbn(String isbn) {
this.isbn = isbn;
}
// titleの値を取得(取り出す)するためのメソッド
public String getTitle() {
return title;
}
// titleの値を設定(変更)するためのメソッド
public String setTitle(String title) {
this.title = title;
}
// このBookオブジェクトの中身を文字列に変換して返すメソッド
@Override //これは書かなくても動くらしい
public String toString(){
return "Book{" + "isbn='" + isbn + '\'' +", title='" + title + '\'' + '}';
}
}
★コンストラクタ部分について
Book myBook = new Book("1234567890", "Java入門");
- 例えば使う側で以下のように記述
"1234567890"が引数のisbn
"Java入門"が引数のtitle
としてコンストラクタに渡され、this.isbnとthis.titleにそれぞれセットされる
★本の更新や取得について、具体例
Book book1 = new Book("978-1234567890", "Spring入門");
Book book2 = new Book("978-0987654321", "Java完全版");
例:book1のタイトルを取得したい時
String title1 = book1.getTitle();
System.out.println(title1);
結果は "Spring入門"
例:book2のisbnを取得したい時
String isbn2 = book2.getIsbn();
System.out.println(isbn2);
結果は"978-0987654321"
例:book1のタイトルを更新
book1.setTitle("Spring完全版");
★toString() メソッドを書かないとどうなるのか
- このメソッドは文字列として表現するためのモノ
- なぜ使うかについて、
System.out.println(book1);のようにオブジェクトを出力すると、Javaは自動的にそのオブジェクトのtoString()メソッドを呼び出しているから
public class Book {
private String isbn;
private String title;
public Book(String isbn, String title) {
this.isbn = isbn;
this.title = title;
}
// ★ここにtoString()がない!
public static void main(String[] args) {
Book book1 = new Book("978-1234567890", "Spring入門");
System.out.println(book1); // 👈 デフォルト表示「Book@1b6d3586 ← クラス名 + ハッシュコード」
}
}
public class Book {
private String isbn;
private String title;
public Book(String isbn, String title) {
this.isbn = isbn;
this.title = title;
}
// toStringを定義
@Override
public String toString() {
return "Book{isbn='" + isbn + "', title='" + title + "'}";
}
public static void main(String[] args) {
Book book1 = new Book("978-1234567890", "Spring入門");
System.out.println(book1); // 👈 文字列表示される!→ Book{isbn='978-1234567890', title='Spring入門'}
}
}
★オーバーライドについて
- すべてのJavaクラスは暗黙的にObjectクラスを継承している。
- Object クラスには
toString()メソッドがある - Bookクラスは Object クラスの
toString()を「自分用に書き換えている(オーバーライドしている)」
★isbnとは
書籍を識別するための国際的なコード
国際標準図書番号ともいう(International Standard Book Number)
2:Create a Book Repository(ブックリポジトリを作成する)
ここでは、モデル(Book)を操作する役割を持つ
※interfaceとimplement合わせてリポジトリ
★インターフェース部分(リポジトリ設計図)
package com.example.caching;
// Book Repositoryを使うクラスは、String型のisbnをもらって、Book型のオブジェクトを返す
public interface BookRepository {
// Isbnを使って本を取得する。isbnは引数。外から渡された値。
Book getByIsbn(String isbn);
}
- 「このインターフェースを使うなら、
getByIsbn(String isbn)という機能を絶対に持っててね!」というルール。
★実装部分(その設計図に実際の動作を定義したもの)
package com.example.caching;
import org.springframework.stereotype.Component;
@Component
public class SimpleBookRepository implements BookRepository {
@Override
public Book getByIsbn(String isbn) {
// わざと遅延を入れる処理
simulateSlowService();
return new Book(isbn, "Some book");
}
// Don't do this at home
private void simulateSlowService() {
try {
long time = 3000L;
Thread.sleep(time);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
}
★implementとは
- interface(インターフェイス)とは?→ 「こういう機能があるよ」と決めたルール(契約書)
- implements(実装する)とは?→ そのルール通りに、ちゃんと中身(行動)を書くこと
★リポジトリかどうかの判断
- 名前に Repository が含まれている
- interface になっていることが多い
- データを取得・保存するメソッドがある
Using the Repository(リポジトリの使用)
次に、リポジトリを接続し、それを使用していくつかの書籍にアクセスする必要がある。
src/main/java/com/example/caching/CachingApplication.javaのキャッシュアノテーションを有効にする前のコードは割愛。
package com.example.caching;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component // アプリ起動時に自動的に実行されるクラスとして登録
public class AppRunner implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(AppRunner.class);
private final BookRepository bookRepository;
// DI(依存性注入):Springがリポジトリのインスタンスを自動で渡してくれる
public AppRunner(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
@Override
public void run(String... args) throws Exception {
// アプリ起動後に自動で呼ばれるメソッド
logger.info(".... Fetching books"); // ログ出力(標準出力ではなくログ)
// 本を取得する処理(同じISBNでも何度も呼び出す)
logger.info("isbn-1234 --> " + bookRepository.getByIsbn("isbn-1234"));
logger.info("isbn-4567 --> " + bookRepository.getByIsbn("isbn-4567"));
logger.info("isbn-1234 --> " + bookRepository.getByIsbn("isbn-1234"));
logger.info("isbn-4567 --> " + bookRepository.getByIsbn("isbn-4567"));
logger.info("isbn-1234 --> " + bookRepository.getByIsbn("isbn-1234"));
logger.info("isbn-1234 --> " + bookRepository.getByIsbn("isbn-1234"));
}
}
- Spring Boot アプリが起動すると、
AppRunner.run()が自動で呼び出される - 中で
bookRepository.getByIsbn()を使って、何度も本を検索 - 同じ ISBN(例: "isbn-1234")でも何度も検索
- この時、キャッシュが無効だと毎回3秒かかる → キャッシュを有効にすると2回目以降はすぐ返ってくる!
Enable caching(キャッシュを使用可能にする)
SimpleBookRepositoryでキャッシュを有効にして、ブックがbooksキャッシュ内にキャッシュされるようにすることができる
package com.example.caching;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;
@Component // DI登録(Springに「使う部品です」と知らせる)
public class SimpleBookRepository implements BookRepository {
@Override
@Cacheable("books") // キャッシュ対象のメソッド。「books」という名前のキャッシュに保存
public Book getByIsbn(String isbn) {
simulateSlowService(); // 3秒間待つ(遅い処理)
return new Book(isbn, "Some book"); // 同じISBNなら同じ本を返す
}
// わざと遅くするメソッド(実際にはAPIやDBアクセスを想定)
private void simulateSlowService() {
try {
Thread.sleep(3000L); // 3秒スリープ
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
}
キャッシュアノテーションの処理を有効にする必要がある
package com.example.caching;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication // SpringBoot用の便利なアノテーション(下記3つの機能をまとめている)
@EnableCaching // キャッシュ機能を有効にする
public class CachingApplication {
public static void main(String[] args) {
// アプリケーションを起動する
SpringApplication.run(CachingApplication.class, args);
}
}
★@ SpringBootApplicationの中身
-
@ Configuration:設定クラスですと伝える -
@ EnableAutoConfiguration:Springが自動的に設定(たとえばWebアプリならサーバー起動) -
@ ComponentScan:同じパッケージ内の他のクラスを探して登録(DI対象に)
?@Cacheable をどちらから先に追加すべき?順番に決まりはあるのか?
- 順番に「厳密な決まり」はないけど、考え方はある
- 1:
@EnableCachingを追加(アプリ全体に「キャッシュ使いますよ」と知らせる) - 2:
@Cacheableを追加(どこでキャッシュを使うか指定)
↓
@EnableCachingがないと、@Cacheableは無視される
?「どのメソッドに @Cacheable を付けるべきか?」の考え方
- 何度も同じ入力で呼ばれて、結果が変わらない処理に対して使うのが基本。
- つまり、入力(引数)に対して結果(戻り値)が同じで、しかも処理が重い(時間がかかる)なら、キャッシュする価値がある。
↓
今回でいうと下記部分が対象
@Override
public Book getByIsbn(String isbn) {
simulateSlowService();
return new Book(isbn, "Some book");
}
↓なぜこのメソッドなのか?
| 条件 | 該当しているか | 説明 |
|---|---|---|
| 引数がある | ✅ |
String isbn:入力が明確 |
| 出力が同じ | ✅ | 同じISBNなら同じ本が返る:new Book(isbn, "Some book")
|
| 時間がかかる | ✅ |
Thread.sleep(3000) で遅い:パフォーマンスに影響あり |
| 副作用がない | ✅ | データを変更していない:安心してキャッシュできる |
?「"books" という文字列がキャッシュの保存先(Mapのキー名)になること」について
@Cacheable("books")
public Book getByIsbn(String isbn)
「このメソッドが呼ばれたとき、結果をキャッシュ(=保存)しておいて、同じISBNでもう一度呼ばれたら、保存された結果をすぐ返してね!」そして、「そのキャッシュは books という名前の場所に入れておいてね!」
↓
キャッシュは内部的に「Map(辞書)」のような構造で保存(この Map に「ISBN → Book の情報」という形で保存)
Map<String, Book> booksCache = new HashMap<>();
↓
具体例:ISBNで本を探すとき
bookRepository.getByIsbn("isbn-1234");
‼️このとき最初の呼び出しでは、キャッシュに何もないので以下のような感じになります
"books" キャッシュ領域(Mapみたいなもの):
⛔ 見つからない → 実際に処理実行 → Bookオブジェクト作成 → 保存
保存後の状態:
books = {
"isbn-1234": Book{isbn='isbn-1234', title='Some book'}
}
‼️次に同じ ISBN を呼ぶ
bookRepository.getByIsbn("isbn-1234");
↓すでにキャッシュがある状態なので
"books" キャッシュ領域:
✅ isbn-1234 はある! → キャッシュから即返却!
だから、simulateSlowService() の遅延処理がスキップされる!
ここで「books」という名前の役割について
キャッシュの中には、複数のグループを作れ、この「グループ名」が "books"。つまり、「キャッシュをどこに保存するか」を区別する名前
books = { "isbn-1234": Book1, "isbn-5678": Book2 }
users = { "user-001": User1, "user-002": User2 }
orders = { "order-01": Order1 }
↓例えると
「books」はキャッシュの"引き出しの名前"
その中に「isbn」というキーで本の情報をしまっている
↓
キャッシュの全体:
└─ books 引き出し(Mapみたいな場所)
├─ "isbn-1234" : BookA
├─ "isbn-5678" : BookB
└─ users 引き出し
├─ "user-001" : UserX