spring boot 源码解析33-spring boot集成spring cache(基于ConcurrentMapCache)

前言

接下来的几篇我们来讲解一下spring boot 中如何集成spring cache. spring cache 中支持如下cache:

  1. ConcurrentMap Cache
  2. Caffeine Cache
  3. EhCache
  4. GuavaCache(1.5版本废弃)
  5. Hazelcast Cache
  6. Infinispan Cache
  7. JCache Cache

我们只讲解ConcurrentMapCache和EhCache,其他的cache感兴趣的可以查阅相关资料.本文首先来看下ConcurrentMapCache.

ConcurrentMapCache集成

  1. 在pom文件中加入如下依赖:

    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-cacheartifactId>
    dependency>
  2. 在启动类加上@EnableCaching注解开始spring cache的支持.

  3. 由于加入cache,为了模拟的方便,我们加入mybatis,pom文件加入如下配置:

    <dependency>
        <groupId>org.mybatis.spring.bootgroupId>
        <artifactId>mybatis-spring-boot-starterartifactId>
        <version>1.3.0version>
    dependency>

    在application.properties中加入如下配置:

    mybatis.mapper-locations=classpath:com/example/demo/mapper/*.xml

    在启动类中加入如下注解即可:

    @MapperScan("com.example.demo.mapper")
  4. UserMapper如下:

    package com.example.demo.mapper;
    
    import com.example.demo.model.User;
    
    public interface UserMapper {
        int deleteByPrimaryKey(Long id);
    
        int insert(User record);
    
        int insertSelective(User record);
    
        User selectByPrimaryKey(Long id);
    
        int updateByPrimaryKeySelective(User record);
    
        int updateByPrimaryKey(User record);
    }
  5. User,代码如下:

    package com.example.demo.model;
    import java.util.Date;
    public class User {
        private Long id;
    
        private String nickName;
    
        private String emailNew;
    
        private Date registerTime;
    
        public Long getId() {
            return id;
        }
    
        public void setId(Long id) {
            this.id = id;
        }
    
        public String getNickName() {
            return nickName;
        }
    
        public void setNickName(String nickName) {
            this.nickName = nickName == null ? null : nickName.trim();
        }
    
        public String getEmailNew() {
            return emailNew;
        }
    
        public void setEmailNew(String emailNew) {
            this.emailNew = emailNew == null ? null : emailNew.trim();
        }
    
        public Date getRegisterTime() {
            return registerTime;
        }
    
        public void setRegisterTime(Date registerTime) {
            this.registerTime = registerTime;
        }
    }
  6. UserMapper.xml 如下:

    version="1.0" encoding="UTF-8" ?>
    "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
    "com.example.demo.mapper.UserMapper" >
    id="BaseResultMap" type="com.example.demo.model.User" >
    <id column="id" property="id" jdbcType="BIGINT" />
    <result column="nick_name" property="nickName" jdbcType="VARCHAR" />
    <result column="email_new" property="emailNew" jdbcType="VARCHAR" />
    <result column="register_time" property="registerTime" jdbcType="TIMESTAMP" />
    
    id="Base_Column_List" >
    id, nick_name, email_new, register_time
    
    
    id="deleteByPrimaryKey" parameterType="java.lang.Long" >
    delete from user
    where id = #{id,jdbcType=BIGINT}
    
    id="insert" parameterType="com.example.demo.model.User" >
    insert into user (id, nick_name, email_new, 
      register_time)
    values (#{id,jdbcType=BIGINT}, #{nickName,jdbcType=VARCHAR}, #{emailNew,jdbcType=VARCHAR}, 
      #{registerTime,jdbcType=TIMESTAMP})
    
    id="insertSelective" parameterType="com.example.demo.model.User" >
    insert into user
    "(" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        id,
      if>
      <if test="nickName != null" >
        nick_name,
      if>
      <if test="emailNew != null" >
        email_new,
      if>
      <if test="registerTime != null" >
        register_time,
      if>
    
    "values (" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        #{id,jdbcType=BIGINT},
      if>
      <if test="nickName != null" >
        #{nickName,jdbcType=VARCHAR},
      if>
      <if test="emailNew != null" >
        #{emailNew,jdbcType=VARCHAR},
      if>
      <if test="registerTime != null" >
        #{registerTime,jdbcType=TIMESTAMP},
      if>
    
    
    id="updateByPrimaryKeySelective" parameterType="com.example.demo.model.User" >
    update user
    <set >
      <if test="nickName != null" >
        nick_name = #{nickName,jdbcType=VARCHAR},
      if>
      <if test="emailNew != null" >
        email_new = #{emailNew,jdbcType=VARCHAR},
      if>
      <if test="registerTime != null" >
        register_time = #{registerTime,jdbcType=TIMESTAMP},
      if>
    set>
    where id = #{id,jdbcType=BIGINT}
    
    id="updateByPrimaryKey" parameterType="com.example.demo.model.User" >
    update user
    set nick_name = #{nickName,jdbcType=VARCHAR},
      email_new = #{emailNew,jdbcType=VARCHAR},
      register_time = #{registerTime,jdbcType=TIMESTAMP}
    where id = #{id,jdbcType=BIGINT}
    
    
  7. UserService如下:

    package com.example.demo.service;
    
    import com.example.demo.model.User;
    
    public interface UserService {
    
        int insertSelective(User record);
    
        User selectByPrimaryKey(Long id);
    
        User updateByPrimaryKeySelective(User record);
    
        int deleteByPrimaryKey(Long id);
    }
  8. UserServiceImpl 如下:

    package com.example.demo.service;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cache.annotation.CacheEvict;
    import org.springframework.cache.annotation.CachePut;
    import org.springframework.cache.annotation.Cacheable;
    import org.springframework.stereotype.Service;
    import com.example.demo.mapper.UserMapper;
    import com.example.demo.model.User;
    @Service
    public class UserServiceImpl implements UserService{
    
        private static Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
    
        @Autowired
        private UserMapper userMapper;
    
        @Override
        public int insertSelective(User record) {
            return userMapper.insertSelective(record);
        }
    
        @Override
        @Cacheable(cacheNames="users",key="#id")
        public User selectByPrimaryKey(Long id) {
            logger.info("查询数据库");
            return userMapper.selectByPrimaryKey(id);
        }
    
        @Override
        @CachePut(cacheNames="users",key="#record.id")
        public User updateByPrimaryKeySelective(User record) {
            userMapper.updateByPrimaryKeySelective(record);
            return userMapper.selectByPrimaryKey(record.getId());
        }
    
        @Override
        @CacheEvict(cacheNames="users",key="#id")
        public int deleteByPrimaryKey(Long id) {
            return userMapper.deleteByPrimaryKey(id);
        }
    }

    这里有必要说明一下spring cache相关的注解:

    • @CacheConfig:主要用于配置该类中会用到的一些共用的缓存配置
    • @Cacheable:配置了findByName函数的返回值将被加入缓存。同时在查询时,会先从缓存中获取,若不存在才再发起对数据库的访问。该注解主要有下面几个参数:

      • value、cacheNames:两个等同的参数(cacheNames为Spring 4新增,作为value的别名),用于指定缓存存储的集合名。由于Spring 4中新增了@CacheConfig,因此在Spring 3中原本必须有的value属性,也成为非必需项了

      • key:缓存对象存储在Map集合中的key值,非必需,缺省按照函数的所有参数组合作为key值,若自己配置需使用SpEL表达式,比如:@Cacheable(key = “#p0”):使用函数第一个参数作为缓存的key值

      • condition:缓存对象的条件,非必需,也需使用SpEL表达式,只有满足表达式条件的内容才会被缓存,比如:@Cacheable(key = “#p0”, condition = “#p0.length() < 3”),表示只有当第一个参数的长度小于3的时候才会被缓存
      • unless:另外一个缓存条件参数,非必需,需使用SpEL表达式。它不同于condition参数的地方在于它的判断时机,该条件是在函数被调用之后才做判断的,所以它可以通过对result进行判断。
      • keyGenerator:用于指定key生成器,非必需。若需要指定一个自定义的key生成器,我们需要去实现org.springframework.cache.interceptor.KeyGenerator接口,并使用该参数来指定。需要注意的是:该参数与key是互斥的
      • cacheManager:用于指定使用哪个缓存管理器,非必需。只有当有多个时才需要使用
      • cacheResolver:用于指定使用那个缓存解析器,非必需。需通过org.springframework.cache.interceptor.CacheResolver接口来实现自己的缓存解析器,并用该参数指定。
    • @CachePut:配置于函数上,能够根据参数定义条件来进行缓存,它与@Cacheable不同的是,它每次都会真是调用函数,所以主要用于数据新增和修改操作上。它的参数与@Cacheable类似,具体功能可参考上面对@Cacheable参数的解析

    • @CacheEvict:配置于函数上,通常用在删除方法上,用来从缓存中移除相应数据。除了同@Cacheable一样的参数之外,它还有下面两个参数:
      • allEntries:非必需,默认为false。当为true时,会移除所有数据
      • beforeInvocation:非必需,默认为false,会在调用方法之后移除数据。当为true时,会在调用方法之前移除数据。

    参考链接如下:

    Spring Boot中的缓存支持(一)注解配置与EhCache使用

    Spring4.1新特性——Spring缓存框架增强

  9. UserController,代码如下:

    package com.example.demo;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.ResponseBody;
    import com.example.demo.model.User;
    import com.example.demo.service.UserService;
    @Controller
    public class UserController {
    
        @Autowired
        private UserService userService;
    
        @RequestMapping("/add-user")
        @ResponseBody
        public String insertSelective(User record) {
            userService.insertSelective(record);
            return "OK";
        }
    
        @RequestMapping("/get-by-id")
        @ResponseBody
        public User selectByPrimaryKey(Long id) {
            return userService.selectByPrimaryKey(id);
        }
    
        @RequestMapping("/update-by-id")
        @ResponseBody
        public String update(User record) {
            userService.updateByPrimaryKeySelective(record);
            return "ok";
        }
    
        @RequestMapping("/delete-by-id")
        @ResponseBody
        public Integer delete(Long id) {
            return userService.deleteByPrimaryKey(id);
        }  
    }
  10. 测试:

    1. 首先我们访问如下链接:

      http://127.0.0.1:8080/[email protected]

      http://127.0.0.1:8080/[email protected]

      加入2条数据

    2. 然后我们访问如下链接:

      http://127.0.0.1:8080/get-by-id?id=1

      发现有打印日志,如下:

      2018-01-23 14:52:41.658  INFO 60766 --- [nio-8881-exec-6] c.example.demo.service.UserServiceImpl   : 查询数据库

      返回值如下:

      {
          id: 1,
          nickName: "harry",
          emailNew: "[email protected]",
          registerTime: 1484488802000
      }

      此时我们再次访问,发现没有再次打印日志,而是直接从缓存中返回的.

    3. 访问如下链接就行修改:

      http://127.0.0.1:8080/update-by-id?id=1&nickName=22

      将昵称改为22.

      此时我们继续访问 http://127.0.0.1:8080/get-by-id?id=1 ,发现没有打印日志,且返回的结果中昵称也改为了22,如下:

      {
      id: 1,
      nickName: "22",
      emailNew: "[email protected]",
      registerTime: 1484488802000
      }

ConcurrentMapCache解析

ConcurrentMapCache的自动装配声明在SimpleCacheConfiguration中.

  1. SimpleCacheConfiguration 声明了如下注解:

    @Configuration
    @ConditionalOnMissingBean(CacheManager.class)
    @Conditional(CacheCondition.class)
    • @Configuration –> 配置类
    • @ConditionalOnMissingBean(CacheManager.class)–> 当BeanFactory中缺少CacheManager类型的bean时生效
    • @Conditional(CacheCondition.class) –> 通过CacheCondition来进行判断,代码如下:

      public ConditionOutcome getMatchOutcome(ConditionContext context,
          AnnotatedTypeMetadata metadata) {
          // 1. 获得被注解的类名
          String sourceClass = "";
          if (metadata instanceof ClassMetadata) {
              sourceClass = ((ClassMetadata) metadata).getClassName();
          }
          ConditionMessage.Builder message = ConditionMessage.forCondition("Cache",
                  sourceClass);
          // 2. 实例化RelaxedPropertyResolver,读取spring.cache.开头的属性
          RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(
                  context.getEnvironment(), "spring.cache.");
          // 3. 如果没有配置spring.cache.type,则返回匹配--> 进行自动选择
          if (!resolver.containsProperty("type")) {
              return ConditionOutcome.match(message.because("automatic cache type"));
          }
          // 4. 根据被注解的类名获得对应的CacheType
          CacheType cacheType = CacheConfigurations
                  .getType(((AnnotationMetadata) metadata).getClassName());
          // 5. 将spring.cache.type 配置的值 中的-变为_,然后变成大写,如果和CacheType的name相等的化,则返回匹配,否则,返回不匹配
          String value = resolver.getProperty("type").replace('-', '_').toUpperCase();
          if (value.equals(cacheType.name())) {
              return ConditionOutcome.match(message.because(value + " cache type"));
          }
          return ConditionOutcome.noMatch(message.because(value + " cache type"));
      }
      1. 获得被注解的类名,对于当前是org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration
      2. 实例化RelaxedPropertyResolver,读取spring.cache.开头的属性
      3. 如果没有配置spring.cache.type,则返回匹配–> 进行自动选择. 由于默认情况下是没有配置spring.cache.type的,因此,在这里返回.
      4. 根据被注解的类名获得对应的CacheType
      5. 将spring.cache.type 配置的值 中的-变为_,然后变成大写,如果和CacheType的name相等的化,则返回匹配,否则,返回不匹配
  2. bean方法,代码如下:

    @Bean
    public ConcurrentMapCacheManager cacheManager() {
        ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
        List cacheNames = this.cacheProperties.getCacheNames();
        if (!cacheNames.isEmpty()) {
            cacheManager.setCacheNames(cacheNames);
        }
        return this.customizerInvoker.customize(cacheManager);
    }
    • @Bean –> 注册1个id为cacheManager,类型为ConcurrentMapCacheManager的bean

    该方法的处理逻辑如下:

    1. 实例化ConcurrentMapCacheManager
    2. 如果配置有spring.cache.cache-names=xx,xx,则进行配置cacheNames,默认是没有配置的
    3. 调用CacheManagerCustomizers#customize 进行个性化设置,在该方法中是遍历其持有的List

你可能感兴趣的:(spring,boot,spring,boot,源码解析)