【Spring Boot】17.緩存

簡介

在講解Springboot相關組件以前,咱們先要了解在java的緩存插件體系,相似於咱們以前學習數據操做的數據源規範同樣,也有一個標準,這個標準叫作JSR107,咱們能夠經過在網上查詢相關的信息。java

JSR107

Java Caching定義了5個核心接口,分別是CachingProvider, CacheManager, Cache, Entry 和 Expiry。mysql

  1. CachingProvider定義了建立、配置、獲取、管理和控制多個CacheManager。一個應用能夠在運行期訪問多個CachingProvider。
  2. CacheManager定義了建立、配置、獲取、管理和控制多個惟一命名的Cache,這些Cache存在於CacheManager的上下文中。一個CacheManager僅被一個CachingProvider所擁有。
  3. Cache是一個相似Map的數據結構並臨時存儲以Key爲索引的值。一個Cache僅被一個CacheManager所擁有。
  4. Entry是一個存儲在Cache中的key-value對。
  5. Expiry 每個存儲在Cache中的條目有一個定義的有效期。一旦超過這個時間,條目爲過時的狀態。一旦過時,條目將不可訪問、更新和刪除。緩存有效期能夠經過ExpiryPolicy設置

1 應用程序緩存架構

圖1

2 spring的緩存抽象

Spring從3.1開始定義了org.springframework.cache.Cache和org.springframework.cache.CacheManager(緩存管理器)接口來統一不一樣的緩存技術;並支持使用JCache(JSR-107)註解簡化咱們開發;web

Cache接口爲緩存的組件規範定義,包含緩存的各類操做集合; Cache接口下Spring提供了各類xxxCache的實現;如RedisCache,EhCacheCache , ConcurrentMapCache等;redis

每次調用須要緩存功能的方法時,Spring會檢查檢查指定參數的指定的目標方法是否已經被調用過;若是有就直接從緩存中獲取方法調用後的結果,若是沒有就調用方法並緩存結果後返回給用戶。下次調用直接從緩存中獲取。 使用Spring緩存抽象時咱們須要關注如下兩點;spring

  1. 肯定方法須要被緩存以及他們的緩存策略
  2. 從緩存中讀取以前緩存存儲的數據

3 經常使用註解

在咱們開發應用程序中,比較經常使用的註解以下所示: ||| |-|-| |Cache |緩存接口,定義緩存操做。實現有:RedisCache、EhCacheCache、ConcurrentMapCache等| |CacheManager |緩存管理器,管理各類緩存(Cache)組件| |@Cacheable |主要針對方法配置,可以根據方法的請求參數對其結果進行緩存| |@CacheEvict |清空緩存| |@CachePut |保證方法被調用,又但願結果被緩存。| |@EnableCaching |開啓基於註解的緩存| |@keyGenerator |緩存數據時key生成策略| |@serialize |緩存數據時value序列化策略|sql

4 緩存使用測試

接下來咱們編寫一個應用程序體驗一下springboot中緩存的使用。docker

  1. 搭建基本環境
  • 建立項目,選擇模塊:cache web mysql mybatis
  • 導入數據庫文件到數據庫joyblack,腳本內容以下(核心章節咱們用的數據庫)
joyblack.yml
/*
Navicat MySQL Data Transfer

Source Server         : docker
Source Server Version : 50505
Source Host           : 10.21.1.47:3306
Source Database       : joyblack

Target Server Type    : MYSQL
Target Server Version : 50505
File Encoding         : 65001

Date: 2018-12-20 09:45:44
*/

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for `department`
-- ----------------------------
DROP TABLE IF EXISTS `department`;
CREATE TABLE `department` (
  `id` int(11) NOT NULL,
  `department_name` varchar(30) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of department
-- ----------------------------
INSERT INTO `department` VALUES ('1', '鄉下冒險者公會');
INSERT INTO `department` VALUES ('2', '城市冒險者公會');

-- ----------------------------
-- Table structure for `user`
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(11) NOT NULL,
  `user_name` varchar(20) NOT NULL,
  `login_name` varchar(20) NOT NULL,
  `department_id` int(11) NOT NULL DEFAULT 1,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', '阿庫婭', 'akuya', '1');
INSERT INTO `user` VALUES ('2', '克里斯汀娜', 'crustina', '1');
INSERT INTO `user` VALUES ('3', '惠惠', 'huihui', '1');
  1. 整合mybatis操做數據庫(核心章節14.整和mybatis),咱們採用基於註解的方式使用。
pom.xml
<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
application.yml
spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://127.0.0.1:3306/joyblack?characterEncoding=utf8&serverTimezone=GMT
    driver-class-name: com.mysql.cj.jdbc.Driver
server:
  port: 8090
mybatis:
  configuration:
    # 開啓駝峯映射規則
    map-underscore-to-camel-case: true
bean/User.class
package com.zhaoyi.aweb.bean;

public class User {
    private Integer id;
    private String loginName;
    private String userName;
    private Integer departmentId;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getLoginName() {
        return loginName;
    }

    public void setLoginName(String loginName) {
        this.loginName = loginName;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public Integer getDepartmentId() {
        return departmentId;
    }

    public void setDepartmentId(Integer departmentId) {
        this.departmentId = departmentId;
    }
}
bean/Department.class
package com.zhaoyi.aweb.bean;

public class Department {
    private Integer id;
    private String departmentName;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getDepartmentName() {
        return departmentName;
    }

    public void setDepartmentName(String departmentName) {
        this.departmentName = departmentName;
    }
}
mapper/UserMapper.class
package com.zhaoyi.aweb.mapper;

import com.zhaoyi.aweb.bean.User;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

public interface UserMapper {

    @Insert(" insert into user(id, login_name, user_name, department_id) values (#{id}, #{loginName}, #{userName}, #{departmentId})")
    int insertUser(User user);

    @Delete("delete from user where id = #{id}")
    int deleteUser(Integer id);

    @Select("select * from user where id = #{id}")
    User getUserById(Integer id);

    @Update("update user set user_name = #{userName} where id = #{id}")
    int updateUser(User user);
}
mapper/DepartmentMapper.class
package com.zhaoyi.aweb.mapper;

import com.zhaoyi.aweb.bean.Department;
import org.apache.ibatis.annotations.*;

public interface DepartmentMapper {

    // insert a derpartment.
    // @Options(useGeneratedKeys = true, keyProperty = "id") may you want get insert data generated id.
    @Insert("insert into department(id,department_name) values(#{id}, #{departmentName})")
    int insertDepartment(Department department);

    // delete a department by id.
    @Insert("delete from department where id = #{id}")
    int deleteDepartment(Integer id);

    // query a department by id.
    @Select("select * from department where id = #{id}")
    Department getDepartmentById(Integer id);

    // update a department information.
    @Update("update department set department_name=#{departmentName} where id=#{id}")
    int updateDepartment(Department department);
}
  1. 因爲mapper中沒有添加@Mapper註解,咱們要告訴springboot掃描對應的mapper包
aweb/SpringBootApplication.class
package com.zhaoyi.aweb;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@MapperScan(value = "com.zhaoyi.aweb.mapper")
@SpringBootApplication
public class AwebApplication {
    public static void main(String[] args) {
        SpringApplication.run(AwebApplication.class, args);
    }

}
  1. 接下來,編寫一個controller測試一下環境是否OK
controller/UserController.class
package com.zhaoyi.aweb.controller;

import com.zhaoyi.aweb.bean.User;
import com.zhaoyi.aweb.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @Autowired
    private UserMapper userMapper;

    @RequestMapping("/user/insert")
    public User insertUser(User user){
        userMapper.insertUser(user);
        return user;
    }

    @RequestMapping("/user/delete/{id}")
    public Integer insertUser(@PathVariable("id") Integer id){
        return userMapper.deleteUser(id);
    }

    @RequestMapping("/user/select/{id}")
    public User getUser(@PathVariable("id") Integer id){
       return userMapper.getUserById(id);
    }

    @RequestMapping("/user/update")
    public User updateUser(User user){
        userMapper.updateUser(user);
        return user;
    }
}

訪問 localhost:8090/user/select/1,獲得:數據庫

{"id":1,"loginName":"akuya","userName":"阿庫婭","departmentId":1}
  1. 編寫一個service

前面咱們至關於複習了一遍以前的操做,咱們如今先作一下改變,通常來講,controller調用的service相關的東西,所以,咱們將對mapper的操做提到servicer一層,咱們添加一個service包:express

service.UserService.class
package com.zhaoyi.aweb.service;

import com.zhaoyi.aweb.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.zhaoyi.aweb.bean.User;

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    public User getUser(Integer id){
        System.out.println("查詢:" + id);
        return userMapper.getUserById(id);
    }
}

接下來,咱們從新寫一下controller的方法,以下所示:apache

controller/UserController.class
package com.zhaoyi.aweb.controller;

import com.zhaoyi.aweb.bean.User;
import com.zhaoyi.aweb.mapper.UserMapper;
import com.zhaoyi.aweb.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
    @Autowired
    private UserService userService;

    @RequestMapping("/user/select/{id}")
    public User getUser(@PathVariable("id") Integer id){
       return userService.getUser(id);
    }

}
  1. 開啓緩存相關配置

這樣,咱們就保證controller只和service進行交互了。咱們開始新知識了,通常使用緩存,咱們須要以下步驟:

  • 開啓基於註解的緩存;
  • 標註緩存註解。

很簡單吧?

如今,咱們每訪問一次select url,都會在控制檯打印一次

查詢:1

也就是說,當前都會調用service的getUser在數據庫進行查詢操做。接下來咱們爲該方法提供緩存效果,即在啓動類中添加註解@EnableCaching

AwebApplication.class
package com.zhaoyi.aweb;

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

@EnableCaching
@MapperScan(value = "com.zhaoyi.aweb.mapper")
@SpringBootApplication
public class AwebApplication {
    public static void main(String[] args) {
        SpringApplication.run(AwebApplication.class, args);
    }

}

而後咱們去標註緩存註解在對應的服務方法上

service/UserService.class
@Cacheable(value = {"user"})
    public User getUser(Integer id){
        System.out.println("查詢:" + id);
        return userMapper.getUserById(id);
    }

這一次咱們再次訪問,發現除了第一次會打印查詢記錄以外,其餘的查詢都不會打印了(一個id只會進行一次查詢,即第一次),顯然已經作了緩存了。

5 緩存工做原理

仍是得先從自動配置類源碼入手。

CacheAutoConfiguration.class
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.boot.autoconfigure.cache;

import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration;
import org.springframework.boot.autoconfigure.data.jpa.EntityManagerFactoryDependsOnPostProcessor;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.interceptor.CacheAspectSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.util.Assert;

@Configuration
@ConditionalOnClass({CacheManager.class})
@ConditionalOnBean({CacheAspectSupport.class})
@ConditionalOnMissingBean(
    value = {CacheManager.class},
    name = {"cacheResolver"}
)
@EnableConfigurationProperties({CacheProperties.class})
@AutoConfigureAfter({CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class, HibernateJpaAutoConfiguration.class, RedisAutoConfiguration.class})
@Import({CacheAutoConfiguration.CacheConfigurationImportSelector.class})
public class CacheAutoConfiguration {
    public CacheAutoConfiguration() {
    }

    @Bean
    @ConditionalOnMissingBean
    public CacheManagerCustomizers cacheManagerCustomizers(ObjectProvider<CacheManagerCustomizer<?>> customizers) {
        return new CacheManagerCustomizers((List)customizers.orderedStream().collect(Collectors.toList()));
    }

    @Bean
    public CacheAutoConfiguration.CacheManagerValidator cacheAutoConfigurationValidator(CacheProperties cacheProperties, ObjectProvider<CacheManager> cacheManager) {
        return new CacheAutoConfiguration.CacheManagerValidator(cacheProperties, cacheManager);
    }

    static class CacheConfigurationImportSelector implements ImportSelector {
        CacheConfigurationImportSelector() {
        }

        public String[] selectImports(AnnotationMetadata importingClassMetadata) {
            CacheType[] types = CacheType.values();
            String[] imports = new String[types.length];

            for(int i = 0; i < types.length; ++i) {
                imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
            }

            return imports;
        }
    }

    static class CacheManagerValidator implements InitializingBean {
        private final CacheProperties cacheProperties;
        private final ObjectProvider<CacheManager> cacheManager;

        CacheManagerValidator(CacheProperties cacheProperties, ObjectProvider<CacheManager> cacheManager) {
            this.cacheProperties = cacheProperties;
            this.cacheManager = cacheManager;
        }

        public void afterPropertiesSet() {
            Assert.notNull(this.cacheManager.getIfAvailable(), () -> {
                return "No cache manager could be auto-configured, check your configuration (caching type is '" + this.cacheProperties.getType() + "')";
            });
        }
    }

    @Configuration
    @ConditionalOnClass({LocalContainerEntityManagerFactoryBean.class})
    @ConditionalOnBean({AbstractEntityManagerFactoryBean.class})
    protected static class CacheManagerJpaDependencyConfiguration extends EntityManagerFactoryDependsOnPostProcessor {
        public CacheManagerJpaDependencyConfiguration() {
            super(new String[]{"cacheManager"});
        }
    }
}

咱們經過打斷點方式,能夠查看配置相關的信息,這裏就不一一列出了。其運行過程大體以下:

  1. 方法運行以前,先查詢Cache(緩存組件),按照@CacheName指定的名字獲取(``CacheManager`獲取相應的緩存);第一次獲取緩存時,若是沒有對應的組件,會先自動建立。 總之,第一步獲取了一個緩存組件。

  2. 在組件中經過咱們提供的key,默認是方法的參數,以後,在緩存內部,根據咱們提供的key又根據某種策略生成內部的key,默認使用SimpleKeyGenerator生成key(若是沒有參數:key = new SimpleKey(),若是有一個參數,key=參數的值,多個參數 key = SimpleKey(param))查詢對應的值。

  3. 若是沒有從對應的key中查到值,就會調用目標方法(緩存標註的方法)返回的結果放到緩存(key對應的值)之中;若是有值,則直接將結果返回。

@Cacheable標註的方法執行以前先來檢查緩存中有沒有這個數據,默認按照參數的值查找緩存,若是沒有就運行方法結果放入緩存。之後再次調用就能夠直接使用緩存中的數據。

核心:

  1. 使用CacheManager按照名字獲得Cache組件,若沒有配置,默認就是ConcurrentMapCacheManager組件;

  2. key是使用KeyGenerator生成的,默認使用SimpleKeyGenerator;

6 @Cacheable分析

CacheEnable主要用於標準方法,表示對該方法的返回結果進行緩存保存。

經過觀察CacheEnable源碼以下所示:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.cache.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {
    @AliasFor("cacheNames")
    String[] value() default {};

    @AliasFor("value")
    String[] cacheNames() default {};

    String key() default "";

    String keyGenerator() default "";

    String cacheManager() default "";

    String cacheResolver() default "";

    String condition() default "";

    String unless() default "";

    boolean sync() default false;
}

從中能夠查詢到咱們能夠定製的屬性8個,其中valuecacheNames是相同功效的兩個註解參數。咱們接下來分析幾個經常使用的,cacheManager放到後面。

  1. value&cacheNames 緩存組件的名稱,在 spring 配置文件中定義,必須指定至少一個。
@Cacheable(value="mycache")

他等同於

 @Cacheable(value={"cache1","cache2"}
  1. key 緩存的 key,能夠爲空,若是指定要按照 SpEL 表達式編寫,若是不指定,則缺省按照方法的全部參數進行組合。
@Cacheable(value = {"user"}, key="#root.methodName+'[' + #id + ']'")

此方法自定義了key的值爲:methodname[id]。其中id爲咱們傳入的參數的值。

其語法特色遵循以下取值方式: |名字| 位置 |描述 |示例| |-|-|-|-| |methodName |root object| 當前被調用的方法名 #root.methodName |method |root object| 當前被調用的方法 #root.method.name |target |root object| 當前被調用的目標對象 #root.target |targetClass| root object| 當前被調用的目標對象類 #root.targetClass |args |root object| 當前被調用的方法的參數列表 #root.args[0] |caches |root object| 當前方法調用使用的緩存列表(如@Cacheable(value={"cache1", "cache2"})),則有兩個cache #root.caches[0].name |argument name |evaluation context| 方法參數的名字. 能夠直接 #參數名 ,也可使用 #p0或#a0 的形式,0表明參數的索引; #iban 、 #a0 、  #p0  |result |evaluation context| 方法執行後的返回值(僅當方法執行以後的判斷有效,如‘unless’,’cache put’的表達式 ’cache evict’的表達式beforeInvocation=false) #result ||||

  1. keyGenerator 和key二選一,指定key的生成策略,即用於指定自定義key生成器(KeyGenerator)。

咱們來測試一下這個功能,首先編寫配置文件

config/CacheConfig.class
package com.zhaoyi.aweb.config;

import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.lang.reflect.Method;

@Configuration
public class CacheConfig {

    @Bean
    public KeyGenerator keyGenerator(){
        return new KeyGenerator() {
            @Override
            public Object generate(Object o, Method method, Object... objects) {
                return method.getName() + "[" + Arrays.asList(objects).toString()  + "]";
            }
        };
    }
}

咱們自定義了一個key的生成器,接下來只須要在緩存註解處標註咱們定義的生成器便可:

service/UserService.class
@Cacheable(value = {"user"}, keyGenerator="keyGenerator")
    public User getUser(Integer id){
        System.out.println("查詢:" + id);
        return userMapper.getUserById(id);
    }

keyGenerator在孤獨部分的是咱們加入容器的bean的名字,@Bean不指定name時,默認就是方法名做爲類名,所以咱們此時的bean的名字就是keyGenerator

經過斷點,咱們能夠發現,當查詢用戶1的信息時,該方式生成的key相似爲:getUser[[1]]

  1. cacheManager 指定緩存管理器
  2. cacheResolver 和cacheManager二選一,緩存解析器
  3. condition 緩存的條件,能夠爲空,使用SpEL編寫,返回 true 或者 false,只有爲 true才進行緩存/清除緩存,在調用方法以前以後都能判斷。
@Cacheable(value = {"user"}, condition = "#a0 > 1")
    public User getUser(Integer id){
        System.out.println("查詢:" + id);
        return userMapper.getUserById(id);
    }

condition代表只對id大於2的用戶信息進行緩存。

#a0表明第1個參數,你也能夠直接#參數名提取對應的參數值。

  1. unless(@CachePut)(@Cacheable) 用於否決緩存,不像condition,該表達式只在方法執行以後判斷,此時能夠拿到返回值result進行判斷。條件爲true不會緩存,fasle才緩存。例如:
@Cacheable(value="testcache",unless="#result == null")

#result表示返回結果。

sync

是否使用異步模式,默認false,是否等待方法執行完才返回結果,另外,該配置若是生效,則@unless註解失效。

@CachePut

緩存機制若是對一個常常變化的值進行操做的話,顯然咱們須要一個更新機制,例如當編號爲1的用戶被修改了以後,也但願經過返回結果緩存或者更新(若是對應的key已經有了的話)他的緩存信息。這個功能就是由@CachePut來完成。

他的特色是標註方法在進行操做以後,對結果進行緩存。咱們不難想象,他的使用方式和@Cacheable一模一樣,對其指定一樣的和@Cacheable同樣的key便可。

爲了測試一下,咱們爲controller添加一個update方法

controller/UserController.class
@RequestMapping("/user/update")
    public User updateUser(User user){
        return userService.updateUser(user);
    }

而後對service使用@CachePut註解,注意指定和讀取時使用的同樣的key(即用戶ID):

service/UserService.class
@Cacheable(value = "user", key = "#id")
    public User getUser(Integer id){
        System.out.println("查詢:" + id);
        return userMapper.getUserById(id);
    }

    @CachePut(value = "user", key="#user.id")
    public User updateUser(User user) {
        userMapper.updateUser(user);
        return userMapper.getUserById(user.getId());
    }

咱們和@Cacheable同樣用了相同的緩存組件user,以及一致的key生成策略——用戶ID,同時查詢了更新後的用戶信息做爲返回值,確保@CachePut將其放進緩存。

接下來咱們先查詢id爲1的用戶信息,重複查兩次,確保對當前結果進行了緩存,訪問localhost:8090/user/select/1

{"id":1,"loginName":"akuya","userName":"阿庫婭","departmentId":1}

如今,咱們將userName修改成「阿庫婭520」,訪問localhost:8090/user/update?id=1&userName=阿庫婭520,確保修改爲功以後,咱們再來訪問localhost:8090/user/select/1:

{"id":1,"loginName":"akuya","userName":"阿庫婭520","departmentId":1}

獲得了咱們更新以後的結果,說明@CachePut達到了咱們想要的需求。

爲了測試@CachePut的效果,能夠先去除更新@CachePut的註解,這樣咱們就能夠發現,即使咱們修改了用戶信息,緩存的信息仍是舊用戶信息,添加了@CachePut以後,結果就實時更新了。

@Cacheable不同的是,@CachePut是優先調用方法,再將結果存儲在緩存中;而@Cacheable則是先判斷緩存中是否存在對應的key,不存在才調用方法。所以咱們能夠推導出@Cacheable指定key的時候是不能使用#result這樣的語法的(聰明的你應該很容易理解)。

8 @CacheEvict

緩存清除,例如咱們刪除一個用戶的時候,可使用該註解刪除指定的緩存信息。

一樣須要指定緩存組件,以及key,有了以前的經驗,咱們應該寫這樣的service方法就能夠了:

service/UserService.class
@CacheEvict(value = "user", key="#id")
    public void deleteUser(Integer id) {
        System.out.println("刪除");
        userMapper.deleteUser(id);
    }

咱們也指定了user,也指定了其key的值。

相比較以前的註解,CacheEvict還有一些特殊的註解,例如:

  1. allEntries 默認值false,標誌爲true以後,會清除指定組件中的全部緩存信息,例如
@CacheEvict(value = "user", allEntries = true)

該註解就會在方法執行後,將user組件中的全部緩存都清除。

  1. beforeInvocation 默認爲false,若是設置爲true,那麼清除緩存操做會發生在方法執行以前。這種狀況比較特殊,默認狀況下,咱們是方法執行後才進行清緩存操做,顯然若是在方法運行過程當中出現異常,就不會清除緩存。因此,若是有特殊要求,咱們可使用該參數,讓緩存一開始就清除掉,而無論你方法是否運行成功與否。

9 @Caching

若是咱們的緩存規則比較複雜,須要用到以上多個註解的特性,那麼能夠考慮使用@Caching註解,查詢其源碼咱們就能夠知道,他其實就是其餘註解的組合體:

org.springframework.cache.annotation/Caching.interface
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Caching {
    Cacheable[] cacheable() default {};

    CachePut[] put() default {};

    CacheEvict[] evict() default {};
}

爲了使用@Caching,咱們能夠編寫一個這樣的service,經過loginName查詢用戶信息,而後呢,將返回結果放在幾個不一樣的key值之中,這樣,咱們直接經過id或者其餘爲查詢條件查詢時,就能夠直接從 以前的查詢中,拿到結果,而無需再從數據庫中查詢了:

mapper/UserMapper.class
@Select("select * from user where login_name = #{loginName}")
    User getUserByLoginName(String loginName);
service/UserService.class
@Caching(
        cacheable = {
            @Cacheable(value = "user", key = "#loginName")
        },
        put = {
            @CachePut(value="user", key = "#result.id"),
            @CachePut(value="user", key = "#result.userName")
        }
    )
    public User getUserByLoginName(String loginName){
        return userMapper.getUserByLoginName(loginName);
    }

咱們經過loginName查詢到結果以後,還想要經過userName以及id做爲緩存的key保存到內存中,這些key值都只能從返回結果中取到。咱們前面有提到,@cacheable是沒辦法拿到返回結果的。所以咱們使用@CachePut來完善了咱們的需求。

接下來經過測試就會發現,只要咱們經過loginName查詢了akuya的數據,咱們再經過id爲1(就是akuya)來查詢akuya信息,發現這時候就直接獲得結果,無需查詢數據庫了。由於咱們的緩存組件中,經過userService.getUserByLoginName方法的執行,已經就id、loginName以及userName對akuya的信息進行了緩存。

10 @CacheConfig

仔細觀察咱們以前緩存屬性配置你會發現,不少屬性很是的囉嗦,例如value="user",重複指定了N次,咱們能不能經過某個註解,直接一次性將這些配置指定了呢?

有,他就是CacheConfig,用於抽取緩存的公共配置。如下是其源碼:

org.springframework.cache.annotation.CacheConfig
/*
 * Copyright 2002-2015 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.cache.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * {@code @CacheConfig} provides a mechanism for sharing common cache-related
 * settings at the class level.
 *
 * <p>When this annotation is present on a given class, it provides a set
 * of default settings for any cache operation defined in that class.
 *
 * @author Stephane Nicoll
 * @author Sam Brannen
 * @since 4.1
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheConfig {

	/**
	 * Names of the default caches to consider for caching operations defined
	 * in the annotated class.
	 * <p>If none is set at the operation level, these are used instead of the default.
	 * <p>May be used to determine the target cache (or caches), matching the
	 * qualifier value or the bean names of a specific bean definition.
	 */
	String[] cacheNames() default {};

	/**
	 * The bean name of the default {@link org.springframework.cache.interceptor.KeyGenerator} to
	 * use for the class.
	 * <p>If none is set at the operation level, this one is used instead of the default.
	 * <p>The key generator is mutually exclusive with the use of a custom key. When such key is
	 * defined for the operation, the value of this key generator is ignored.
	 */
	String keyGenerator() default "";

	/**
	 * The bean name of the custom {@link org.springframework.cache.CacheManager} to use to
	 * create a default {@link org.springframework.cache.interceptor.CacheResolver} if none
	 * is set already.
	 * <p>If no resolver and no cache manager are set at the operation level, and no cache
	 * resolver is set via {@link #cacheResolver}, this one is used instead of the default.
	 * @see org.springframework.cache.interceptor.SimpleCacheResolver
	 */
	String cacheManager() default "";

	/**
	 * The bean name of the custom {@link org.springframework.cache.interceptor.CacheResolver} to use.
	 * <p>If no resolver and no cache manager are set at the operation level, this one is used
	 * instead of the default.
	 */
	String cacheResolver() default "";

}

源碼也加了很多註釋,但其內在屬性都是提取的公共配置,咱們直接在service類上指定以後,若是內部方法沒有特殊指定,都會套用咱們使用該註解指定的值。例如,咱們抽取出公共value屬性。

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

這樣,咱們整個service的緩存方法註解,若是都是用的user組件,就無需特殊指定,直接刪除便可。經過該註解能夠自動的幫咱們指定這個值。

cacheNames和value是同一個意思,不過@CacheConfig沒對value作兼容,因此咱們這裏必須寫cacheNames.

通過這些練習,或許您會發現,緩存內部存儲的數據咱們能看到嗎?若是能看到就行了。能夠嗎?固然能夠。因此,咱們接下來要學習的redis,爲咱們解決這個問題。

相關文章
相關標籤/搜索