實用的Cache套件--EHCache

羅國榮 2018/12/26 14:00:00
6734

實用的Cache套件--EHCache


簡介

本文將藉由實例,說明如何在 Spring Boot 專案整合 EHCache,如何使用 Cache Annotation 以及注意事項,如何注入 CacheManager 介面和操作 Cache 介面彌補 Annotation 的不足。

作者

羅國榮


前言

EHCache 是一個純Java的Cache套件,是Hibernate中默認的CacheProvider。它使用JVM的Heap記憶體進行緩存,超過 Heap 記憶體可以設置緩存到磁碟。
 
Spring Cache 提供了四個 Annotation (@CacheConfig、@Cacheable、@CachePut、@CacheEvict),讓置入(CachePut)、讀取(Cacheable)或驅除(CacheEvict) Cache更方便。
 
Spring Cache 也定義了 CacheManager 和 Cache 介面,藉以統一不同的 Cache 套件的調用方式。其中 CacheManager 是各種 Cache 套件的抽象介面,而 Cache 介面包含 Cache 套件的各種操作。使用 CacheManager 和 Cache 介面能夠更細部地操作 Cache,處理 Annotation 不足之處。
 
本文將藉由實例,說明如何在 Spring Boot 專案整合 EHCache,如何使用 Cache Annotation 以及注意事項,如何注入 CacheManager 介面和操作 Cache 介面彌補 Annotation 的不足。
 

Spring Boot整合EHCache

列出整合步驟如下:

(1)新建Spring Boot (Maven) Project
(2)編寫pom.xml加入EHCache相關依賴
(3)編寫Spring Boot Application類別及打開Cache機制
(4)配置application.properties
(5)編寫ehcache.xml設定檔
(6)編寫User Bean
(7)編寫User Repository
(8)編寫User Service
(9)編寫User Controller 
(10)運行專案

整合步驟說明

1. 新建Spring Boot (Maven) Project

建立一個名為SpringCache的專案,專案以Spring Boot + Maven框架建構,使用JPA和Cache機制,並提供CRUD RESTful API供調用。
 
專案架構圖:
 

2. 編寫pom.xml加入EHCache相關依賴

在 pom.xml 加入依賴,不需要在 <dependency/> 內指定 <version/> ,因 Spring Boot 會自動下載與 Spring (版本)最適合的套件。

		<dependency>
			<groupId>javax.cache</groupId>
			<artifactId>cache-api</artifactId>
		</dependency>
		<dependency>
			<groupId>org.ehcache</groupId>
			<artifactId>ehcache</artifactId>
		</dependency>

 

3. 編寫Spring Boot Application類別及打開Cache機制

 SpringCacheApplication.java
package com.example.cache;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class SpringCacheApplication {

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

在類別名稱上方添加「@EnableCaching」,就可以打開 Cache 機制。

 

4. 配置application.properties

spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&useSSL=false
spring.datasource.username=!@#$
spring.datasource.password=%^&*
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

spring.jpa.database=MYSQL
spring.jpa.properties.hibernate.show_sql=true

spring.cache.jcache.config=classpath:ehcache.xml

重點放在「spring.cache.jcache.config=classpath:ehcache.xml」這行,指定Cache的設定檔為ehcache.xml。

 

5. 編寫ehcache.xml設定檔

ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://www.ehcache.org/v3"
	xmlns:jsr107="http://www.ehcache.org/v3/jsr107"
	xsi:schemaLocation="http://www.ehcache.org/v3 
	                        http://www.ehcache.org/schema/ehcache-core-3.2.xsd
	                        http://www.ehcache.org/v3/jsr107
	                        http://www.ehcache.org/schema/ehcache-107-ext-3.2.xsd">

	<persistence directory="java.io.tmpdir" />

	<cache-template name="default">
		<resources>
			<heap unit="entries">100</heap>
			<offheap unit="MB">128</offheap>
		</resources>
	</cache-template>

	<cache alias="Users" uses-template="default">
		<expiry>
			<ttl unit="seconds">30</ttl>
		</expiry>
	</cache>

	<cache alias="Orders" uses-template="default">
		<expiry>
			<ttl unit="seconds">600</ttl>
		</expiry>
	</cache>

</config>

在src\main\resources目錄下,增加 ehcache.xml 設定檔。

設定檔內定義了一個"default"樣板(cache-template),該樣板可以在 Heap 記憶體區緩存100個物件,在 Heap 記憶體區外則可緩存 128MB 大小的資料。該"default"樣板可以被覆寫,延伸地加入其他參數。

設定檔內也定義了二個 Cache,分別命名為 "Users" 和 "Orders" :

"Users" Cache 的緩存有效時間設定為30秒;當資料放入 "Users" Cache 區,超過30秒該資料會失效,不再有 Cache 效用。(緩存有效時間應依據實際的情況設定,在此是為了觀察 Cache 失效才設定很短的時間)

"Orders" Cache 的緩存有效時間設定為600秒;當資料放入 "Orders" Cache 區,超過600秒該資料會失效,不再有 Cache 效用。(此例,緩存有效時間的設定比較接近實際的應用)

 

Hint:

緩存有效時間是非常有用的設定,可以更有效率的使用 Cache 區,不會導致資料一旦放入 Cache 區,就一直佔用而不釋出空間。

 

6. 編寫User Bean

User.java
package com.example.cache;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "system_user")
public class User implements Serializable {

	private static final long serialVersionUID = 4244312981614344972L;

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "id")
	private Integer id;

	@Column(name = "name")
	private String name;

	@Column(name = "age")
	private Integer age;


	//getters and setters

}
因為緩存不只存放在記憶體,也可以存放在磁碟,所以 Entity Bean 皆要實作 java.io.Serializable 介面,否則在緩存資料時會有 java.io.NotSerializableException 被拋出。
例: org.ehcache.core.spi.store.StoreAccessException: org.ehcache.spi.serialization.SerializerException: java.io.NotSerializableException: com.example.cache.User
 

Hint:

一個類別的物件要想序列化成功,必須滿足兩個條件:

1. 該類別必須實作 java.io.Serializable 介面。

2. 該類別的所有屬性必須是可序列化的。如果有一個屬性不是可序列化的,則該屬性必須注明是短暫(transient)的。例如: public transient int SSN;

 

Hint:

建議: 類別的屬性其型別不要使用原生型別宣告,例如:int  改用   Integer,    boolean  改用  Boolean,包裹性的類別可以更好地表示"無值"(null)的狀態 原生型別是無法表示   null   這種狀態的。好處在那兒?就留給各位看倌自行測試、思考。
 

7. 編寫User Repository

UserRepository.java
package com.example.cache;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

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

}

UserRepository 繼承 JpaRepository 介面,很容易的就獲得 CRUD 功能。

 

8. 編寫User Service

UserService.java
package com.example.cache;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@CacheConfig(cacheNames = "Users")
@Service
public class UserService {

	@Autowired
	private UserRepository userRepository;
	
	@Autowired
	private CacheManager cacheManager;
    
	/**
	 * 新增用户
	 */
	@CachePut(key = "#user.id")
	public User insertUser(User user) {
		System.out.println("\n----------正在執行insertUser()----------");
		user = this.userRepository.save(user);
		return user;
	}

	/**
	 * 查詢用户
	 */
	public List<User> findAll() {
		System.out.println("\n----------正在執行findAll()----------");
		return this.userRepository.findAll();
	}

	/**
	 * 經由id查詢單個用户
	 */
	@Cacheable(key = "#id")
	public User findUserById(Integer id) {
		System.out.println("\n----------正在執行findUserById()----------");
		User user = this.userRepository.findOne(id);
		return user;
	}

	/**
	 * 修改單個用户
	 */
	@CachePut(key = "#user.id")
	public User updateUser(User user) {
		System.out.println("\n----------正在執行updateUser()----------");
		return this.userRepository.save(user);
	}

	/**
	 * 經由id刪除單個用户
	 */
	@CacheEvict(key = "#id")
	public void deleteUserById(Integer id) {
		System.out.println("\n----------正在執行deleteUserById()----------");
		this.userRepository.delete(id);
	}

	/**
	 *  刪除單個用户
	 * @param user
	 */
	@CacheEvict(key = "#user.id")
	public void deleteUser(User user) {
		System.out.println("\n----------正在執行deleteUser()----------");
		this.userRepository.delete(user);
	}

	/**
	 *  刪除多個用户
	 * @param user
	 */
	public void deleteUsers(List<User> users) {
		System.out.println("\n----------正在執行deleteUsers()----------");
		
		// 透過cacheManager移除快取物件
		Cache cache = cacheManager.getCache("Users");
 		for (User user : users) {
			cache.evict(user.getId());
		}
		
		this.userRepository.delete(users);
	}

}

UserService 是一個重要的類別,需要分解各個重點部分進行說明。

Spring Cache 主要提供了四個註解(Annotation)用以支援Cache機制:
  1. @CacheConfig
  2. @CachePut
  3. @Cacheable
  4. @CacheEvict
@CacheConfig 是從Spring 4.1提供的,之前在每一個用到 @Cacheable 註解時,都要加上 value 參數用以指定 Cache 名稱。現在可用 @CacheConfig 標註在類別名稱上,統一指定 value 的值,@Cacheable 註解就可以省略 value 參數。下例表示 此類別內的緩存資料皆放到名稱為 Users 的 Cache 區內。 此處的 cacheNames 的參數值需與 ehcache.xml 設定檔內所設定的 cache-alias 名稱相同,才會具有設定參數的效果。
@CacheConfig(cacheNames = "Users")
@Service
public class UserService {
 
@CachePut 可以標記在一個方法(method)上,也可以標記在一個類別(class)上,而主要用途是標記在方法上。當標記的方法在執行前不會去檢查 Cache 區中是否存在之前執行過的結果,而是 每次都會執行該方法,並將執行結果以鍵值對的形式存入指定的 Cache 區中。 insert 資料或是 update 資料相似的方法都具有一定要執行的特性,執行完後才進行緩存資料的動作,就需要使用 @CachePut 而不能使用 @Cacheable。
@CachePut(key = "#user.id")
public User insertUser(User user) {
    user = this.userRepository.save(user);
    return user;
}
上例表示以 User 物件的 id 屬性做為緩存資料的 key 值,當下次有同樣的 user.id 值的 User 物件被查詢,就可以從 Cache 區取出相同 user.id 值的 User 物件。
 
@Cacheable 可以標記在一個方法(method)上,也可以標記在一個類別(class)上。當標記在一個方法上時表示該方法是支援快取的,當標記在一個類別上時則表示該類所有的方法都是支援快取的。對於一個支援快取的方法,Spring會在其被呼叫後將其返回值緩存起來,以保證下次利用同樣的引數來執行該方法時可以直接從 Cache 區中獲取結果,而 不需要再次執行該方法。 純粹 select 資料的方法不需要每次都執行,就可以使用 @Cacheable,直接從 Cache 區取得資料而不執行該方法。
@Cacheable(key = "#id")
public User findUserById(Integer id) {
    User user = this.userRepository.findOne(id);
    return user;
}
上例表示以輸入參數 id 做為緩存資料的 key 值將 User 物件放入 Cache 區。
 
@CacheEvict 是用來標記在需要清除快取元素的方法(method)或類別(class),主要標記在方法上。當標記在一個類別上時表示其中所有的方法的執行都會觸發快取的清除操作。
@CacheEvict(key = "#user.id")
public void deleteUser(User user) {
    this.userRepository.delete(user);
}
上例表示以 User 物件的 id 屬性做為緩存資料的 key 值,當 delete(user) 執行完成後,從 Cache 區驅除 key 值相同的緩存資料。

 

善用上述的四個註解,就可以很容易地使用 Cache 機制,而不用直接操作 Cache 機制相關的物件。但是如果碰到使用註解卻無法處理某些需求時,仍然需要藉助 Cache 機制相關的物件來處理,底下就來談談如何注入 CacheManager 介面和操作Cache介面彌補Annotation的不足

(1) 注入 CacheManager 介面

@Autowired
private CacheManager cacheManager; 
注意:自動注入的對象是 org.springframework.cache.CacheManager,而不是 org.springframework.cache.ehcache.EhCacheCacheManager。當系統啟動後,Spring Boot 已經幫忙準備好 EHCache 的 CacheManager,所以注入 CacheManager 才是正確的,如果注入 EhCacheCacheManager 反而會出現異常錯誤。(摸索很久才試出來 ^^|||)
 
(2) 操作  CacheManager 介面 和 Cache介面
/**
 *  刪除多個用户
 * @param user
 */
public void deleteUsers(List<User> users) {
    // 透過cacheManager移除快取物件
    Cache cache = cacheManager.getCache("Users");
    for (User user : users) {
        cache.evict(user.getId());
    }
	
    this.userRepository.delete(users);
}

上例的情境:刪除多筆資料的同時也驅除多筆緩存資料。單是使用 @CacheEvict 註解不容易完成需求。但是借助 CacheManager 介面和 Cache 介面就很容易。

Cache cache = cacheManager.getCache("Users"); // 取出名稱為 Users 的 Cache 區

cache.evict(user.getId()); // 驅除 key 為 user.getId() 的緩存資料

 

Hint:

使用 Spring Cache 註解建議一定要加上 key 屬性。指明緩存資料的 key 值不依賴 Spring Cache 自動產生 key 值,可以避免很多無預期的地雷
 
Hint:
Spring 生成 UserService 是使用代理模式生成的,若是內部調用方法(method) @Cacheable 會失效,請參考下列程式碼。問題出在 User user = this.findUserById(id); 這行。
public User updateUser(Integer id) {
	User user = this.findUserById(id);
	return this.userRepository.save(user);
}
詳細說明,可參考此篇文章 https://jax-work-archive.blogspot.com/2015/04/spring-cache-ehcache.html
 

9. 編寫User Controller 

UserController.java
package com.example.cache;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "/user")
public class UserController {

	@Autowired
	private UserService userService;

	/**
	 * 新增User
	 * 
	 * @param user
	 * @return
	 */
	@RequestMapping(value = "/post", method = RequestMethod.POST)
	public User saveUser(@RequestBody User user) {
		return userService.insertUser(user);
	}

	/**
	 * 查詢所有User
	 * 
	 * @return
	 */
	@RequestMapping(value = "/findAll", method = RequestMethod.GET)
	public List<User> findAll() {
		return userService.findAll();
	}

	/**
	 * 使用ID查詢User
	 * 
	 * @param id
	 * @return
	 */
	@RequestMapping(value = "/get/{id}", method = RequestMethod.GET)
	public User getUser(@PathVariable Integer id) {
		return userService.findUserById(id);
	}

	/**
	 * 更新使用者
	 * 
	 * @param id
	 * @param user
	 * @return
	 */
	@RequestMapping(value = "/put/{id}/{name}/{age}", method = RequestMethod.PUT)
	public User putUser(@PathVariable Integer id, @PathVariable String name, @PathVariable Integer age) {
		User user = new User();
		user.setId(id);
		user.setName(name);
		user.setAge(age);

		return userService.updateUser(user);
	}

	/**
	 * 使用ID刪除User
	 * 
	 * @param id
	 * @return
	 */
	@RequestMapping(value = "/delete/{id}", method = RequestMethod.DELETE)
	public String deleteUser(@PathVariable Integer id) {
		userService.deleteUserById(id);

		return "deleteUserById is finish.";
	}

	/**
	 * 刪除User
	 * 
	 * @param user
	 * @return
	 */
	@RequestMapping(value = "/delete", method = RequestMethod.DELETE)
	public String deleteUser(@RequestBody User user) {
		userService.deleteUser(user);

		return "deleteUser is finish.";
	}

	/**
	 * 使用ID刪除多個User
	 * 
	 * @param id1
	 * @param id2
	 * @return
	 */
	@RequestMapping(value = "/delete/{id1}/{id2}", method = RequestMethod.DELETE)
	public String deleteUser(@PathVariable Integer id1, @PathVariable Integer id2) {
		User user1 = userService.findUserById(id1);
		User user2 = userService.findUserById(id2);

		List<User> users = new ArrayList<>();
		users.add(user1);
		users.add(user2);

		userService.deleteUsers(users);

		return "deleteUsers is finish.";
	}

}

使用 @RestController 註解,申明方法(method)具有 RESTful 特性,編寫方法時按照 RESTful 特性就可以。

 

10. 運行專案

啟動專案的方式,可以參考下圖。
 
 
 
啟動過程中,在 Console 可以看到與 EHCache 有關的訊息,表示 ehcache.xml 設定生效,Spring Boot 已能夠將 Cache 機制建立起來。 
2018-12-26 04:29:50.450  INFO 6732 --- [           main] o.t.o.p.UpfrontAllocatingPageSource      : Allocating 128MB in chunks
2018-12-26 04:29:50.564  INFO 6732 --- [           main] org.ehcache.core.EhcacheManager          : Cache 'Orders' created in EhcacheManager.
2018-12-26 04:29:50.569  INFO 6732 --- [           main] o.t.o.p.UpfrontAllocatingPageSource      : Allocating 128MB in chunks
2018-12-26 04:29:50.673  INFO 6732 --- [           main] org.ehcache.core.EhcacheManager          : Cache 'Users' created in EhcacheManager.
2018-12-26 04:29:50.689  INFO 6732 --- [           main] org.ehcache.jsr107.Eh107CacheManager     : Registering Ehcache MBean javax.cache:type=CacheStatistics,CacheManager=file./D./workspace-sts-3.9.5.RELEASE/SpringCache/target/classes/ehcache.xml,Cache=Orders
2018-12-26 04:29:50.692  INFO 6732 --- [           main] org.ehcache.jsr107.Eh107CacheManager     : Registering Ehcache MBean javax.cache:type=CacheStatistics,CacheManager=file./D./workspace-sts-3.9.5.RELEASE/SpringCache/target/classes/ehcache.xml,Cache=Users

 

開始測試

用 Postman 調用 RESTful API,在 Console 區觀察出 Cache 機制是否產生效用?

(1) http://localhost:8080/user/findAll

<<Postman>>
[
    {
        "id": 1,
        "name": "user1",
        "age": 21
    },

    {
        "id": 2,
        "name": "User2",
        "age": 22
    }
]

<<Console>>
----------正在執行findAll()----------
Hibernate: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.name as name3_0_ from system_user user0_
(2) http://localhost:8080/user/get/1  指定查詢 id=1 的資料
<<Postman>>
[
    {
        "id": 1,
        "name": "user1",
        "age": 21
    }
]

<<Console>>
----------正在執行findUserById()----------
Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from system_user user0_ where user0_.id=?

 (3) 再次調用 http://localhost:8080/user/get/1

<<Postman>>
[
    {
        "id": 1,
        "name": "user1",
        "age": 21
    }
]

<<Console>>


//緩存有效,Console 沒有任何輸出,表示是從 Cache 區將資料取出

(4) 等待 30 秒,再次調用 http://localhost:8080/user/get/1

<<Postman>>
[
    {
        "id": 1,
        "name": "user1",
        "age": 21
    }
]

<<Console>>
----------正在執行findUserById()----------
Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from system_user user0_ where user0_.id=?

// 等待30秒,緩存已經失效,再次從資料庫取出資料

 

結語

本文說明至此告一個段落,希望能夠讓大家更了解 Spring Cache 的使用。

 

羅國榮