Spring Boot 快速集成 Ehcache3

4,903 阅读6分钟

Spring Boot 快速集成 Ehcache3

前言

在互联网服务端架构中,缓存的作用是尤为重要的,无论是基于服务器的内存缓存如 Redis,还是 基于 JVM 的内存缓存如 Ehcache ,在高并发场景中承载着巨大的流量,本文主要针对 JVM 内存框架 Ehcache 3 进行简单地练习,基于Spring Boot 集成 Ehcache 3 搭建一个简单的项目,来实现程序的内存缓存功能支持。

正文

Ehcache 3

Ehcache 是一个开源,具有高性能的 Java 缓存库,由于使用简单,扩展性强,是使用最广泛的 Java 缓存框架,同时具备了内存缓存和磁盘缓存的能力,最新的版本是 Ehcache 3.6。

集成步骤

  1. 首先创建一个基本的 Spring Boot 程序取名为 springboot-ehcache,(版本为 2.1.3,以 maven 作为构建工具,不选择任何依赖。(本项目采用 IDEA 2018.5)

  2. 在项目的 pom.xml 里添加 Ehcache 3 依赖,选择合适的版本,这里采用了3.0.0。

pom.xml

<dependency>
  <groupId>org.ehcache</groupId>
  <artifactId>ehcache</artifactId>
  <version>3.0.0</version>    
 </dependency>
  1. 在项目的 pom.xml 里添加 JSR-107 API 依赖

    关于 JSR-107 API:Java 缓存规范的文档 API,类似 JDBC 规范。

    pom.xml

    <dependency>
        <groupId>javax.cache</groupId>
        <artifactId>cache-api</artifactId>
    </dependency>
    
  2. 添加 Spring Boot 依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId> 
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId> 
        </dependency>
    </dependencies>
    

    前两个依赖是 Spring Boot 程序 创建时默认有的,这里的 spring-boot-starter-cache 就是使用 Spring 框架的缓存功能,而加入了 spring-boot-starter-web 主要为了引入 Spring MVC,方便测试缓存的效果。

  3. 在程序配置文件 application.properties 中指定 ehcache.xml 的路径,一般放置在当前 classpath 下;这样就让 Spring 缓存启用 Ehcache。

    application.properties

    spring.cache.jcache.config=classpath:ehcache.xml
    
  4. 在项目里启用缓存,有注解和 XML 配置两种方式

  • 使用 @EnableCaching 注解

    // com.one.springbootehcache2.SpringbootEhcacheApplication.java 
    @EnableCaching
    @SpringBootApplication
    public class SpringbootEhcacheApplication
    {
        public static void main(String[] args)
        {
            SpringApplication.run(SpringbootEhcacheApplication.class, args);
        }
    }
    
  • 或者在 Spring 的 XML 文件中添加 <cache:annotation-driven />

    <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:cache="http://www.springframework.org/schema/cache"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">
    
        <cache:annotation-driven />
    </beans>
    
  1. 在需要使用缓存的方法上使用注解 @CacheResult 进行声明,这样一旦调用这个方法,返回的结果就会被缓存,除非缓存被清除掉,下次就不会执行方法的逻辑了。

    PersonService.java

    // com.one.springbootehcache.service.PersonService.java
    @Service
    public class PersonService {
        @CacheResult(cacheName="people")
        Person getPerson(int id) {
            System.out.println("未从缓存读取 " + id);
            switch (id) {
                case 1:
                    return new Person(id, "Steve", "jobs");
                case 2:
                    return new Person(id, "bill", "gates");
                default:
                    return new Person(id, "unknown", "unknown");
            }
        }
    }
    

    Person.java

    // com.one.springbootehcache.domain.Person.java 
    public class Person implements Serializable {
            private int id;
            private String firstName;
            private String lastName;
    
            public Person(int id, String firstName, String lastName) {
                this.id =id;
                this.firstName = firstName;
                this.lastName = lastName;
            }
    
            public int getId() {
                return id;
            }
    
            public void setId(int id) {
                this.id = id;
            }
    
            public String getFirstName() {
                return firstName;
            }
    
            public void setFirstName(String firstName) {
                this.firstName = firstName;
            }
    
            public String getLastName() {
                return lastName;
            }
    
            public void setLastName(String lastName) {
                this.lastName = lastName;
            }
    }
    
    • @CacheResult 必须指定 cacheName,否则 cacheName 默认视为该方法名称。
  2. 在 ehcache.xml 配置基本缓存参数

    ehcache.xml

    <config
        xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
        xmlns='http://www.ehcache.org/v3'  
        xmlns:jsr107='http://www.ehcache.org/v3/jsr107'>  
    
      <service>
        <jsr107:defaults>
          <jsr107:cache name="people" template="heap-cache"/> 
        </jsr107:defaults>
      </service>
    
      <cache-template name="heap-cache">
        <listeners>    
          <listener>
            <class>com.one.springbootehcache.config.EventLogger</class>
            <event-firing-mode>ASYNCHRONOUS</event-firing-mode>
            <event-ordering-mode>UNORDERED</event-ordering-mode>
            <events-to-fire-on>CREATED</events-to-fire-on> 
            <events-to-fire-on>UPDATED</events-to-fire-on> 
            <events-to-fire-on>EXPIRED</events-to-fire-on> 
            <events-to-fire-on>REMOVED</events-to-fire-on> 
          </listener>
        </listeners>
        <resources>
          <heap unit="entries">2000</heap> 
          <offheap unit="MB">100</offheap> 
        </resources>
      </cache-template>
    </config>
    
    • 声明一个名为 people 的缓存,指定 heap-cache 为模板

    • 在缓存模板里配置了日志输入器 EventLogger,用来监听缓存数据变更的事件,例如数据创建,更新,失效等进行事件日志输出。

      //com.one.springbootehcache.config.EventLogger.java
      public class EventLogger implements CacheEventListener<Object, Object> {
      
          private static final Logger LOGGER = LoggerFactory.getLogger(EventLogger.class);
      
          @Override
          public void onEvent(CacheEvent<Object, Object> event) {
              LOGGER.info("Event: " + event.getType() + " Key: " + event.getKey() + " old value: " + event.getOldValue() + " new value: " + event.getNewValue());
          }
      
      }
      
    • 对 CREATED,UPDATED,EXPIRED,REMOVED 这四个事件进行监听。

    • 最后的 resources 元素配置了缓存能容纳的最大对象个数为2000,堆外内存容量为100M。

  3. 实现 JCacheManagerCustomizer.customize(CacheManager cacheManager) 方法在 CacheManager 使用之前,创建我们配置文件定义的缓存,并声明了缓存策略为10秒。

    // com.one.springbootehcache.config.CachingSetup.java
    @Component
    public  class CachingSetup implements JCacheManagerCustomizer {
        @Override
        public void customize(CacheManager cacheManager)
        {
          cacheManager.createCache("people", new MutableConfiguration<>()  
            .setExpiryPolicyFactory(TouchedExpiryPolicy.factoryOf(new Duration(SECONDS, 10))) 
            .setStoreByValue(false)
            .setStatisticsEnabled(true));
        }
    }
    
  4. 创建一个控制器 PersonController,进行缓存的测试。

    // com.one.springbootehcache.domain.Person.java
    @RequestMapping("/person")
    @RestController
    public class PersonController {
        private static final Logger LOGGER = LoggerFactory.getLogger(PersonController.class);
    
        @Autowired
        private PersonService personService;
    
        @RequestMapping("/get")
        public Person getPerson(int id) {
            Person person = personService.getPerson(id);
            LOGGER.info("读取到数据 " + person.getFirstName() + "," + person.getLastName());
            return person;
        }
    }
    

    启动程序,快速两次访问 http://localhost:8080/person/get?id=1,可以从控制台看到如下结果:

    可以看出第二次访问时,直接使用的先前缓存的数据。由于缓存过期策略设置为 10秒,过了10秒再访问一次查看日志,可以根据事件日志器看出缓存失效后重新获取的数据,再添加到缓存中去。

到这里,我们的 Ehcache 3 与 Spring Boot 集成整合就算完成了,虽然项目比较简单,但可以基于此参考更详细的 Ehcache 配置来进行扩展。

问题列表

下面是我搭建项目过程中踩到的坑,这里放出来,希望能对同样遇到问题的同学有所参考。

  • **问题一:实体类未实现 java.io.Serializable 接口 **
2019-02-17 15:50:07.606 ERROR 29671 --- [nio-8080-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.data.redis.serializer.SerializationException: Cannot serialize; nested exception is org.springframework.core.serializer.support.SerializationFailedException: Failed to serialize object using DefaultSerializer; nested exception is java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [com.one.springbootehcache.domain.Person]] with root cause 
java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [com.one.springbootehcache.domain.Person] 

解决办法:Ehcahe 需要缓存的实体类必须实现 java.io.Serializable 接口

  • 问题二:注解 @CacheResult 未指定缓存名称

解决办法:@CacheResult 的 cacheName 必须指定配置创建的缓存 ,否则 cacheName 默认为该方法完全名称。

  • 问题三:没有正确定义 事件日志器,导致 cacheManager 创建缓存出错

解决办法:在 ehcache.xml 定义的 Listener 类实现 接口 CacheEventListener

结语

希望文章对你有所帮助,如果觉得还行,不妨点个赞吧。^_^