SpringBoot2.x—SpringCache(1)集成

JAVA && Spring && SpringBoot2.x — 学习目录

SpringBoot2.x—SpringCache(1)集成
SpringBoot2.x—SpringCache(2)使用
SpringBoot2.x—SpringCache(3) CacheManager源码

声明式与编程式

说起SpringCache您可能不清楚。但您绝对清楚事务。

一般使用事务分为编程式和声明式。

编程式:事务操作与业务代码耦合,一般我们不会使用这种方式;
声明式:AOP的运用,通过注解使得事务代码与业务代码解耦,目前项目中一般都是使用事务注解。

而我们平时使用缓存,正是编程式,即对缓存的操作与业务代码耦合。那么是否存在一种类似于事务的技术,完成声明式的缓存操作呢?

而SpringCahe便可以提供透明化的缓存操作,即用户可以使用注解的方式。灵活的操纵缓存。

1. 引入依赖

本篇是SpringCache+Redis的整合。SpringCache只是缓存抽象,即具体缓存的操作需要子类实现。

spring-boot-starter-data-redis中实现了SpringCache的抽象接口,即我们整合SpringCache+Redis无需自己实现具体缓存。

        <!--SpringCache的依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
       <!--整合Redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>

SpringBoot2.X整合Redis缓存可以看这篇文章,因为有个项目在生产环境中,使用lettuce客户端每隔一段时间连接断开(初步估计是Redis机房和应用服务器机房网络问题)。切换成了jedis客户端。

2. SpringCache配置

两种配置,一种可以在yml中配置,一种是在代码中配置,此处推荐在@Configuration中进行配置。

原因一是更加灵活,在配置CacheManager的Bean时,可以初始化Cache对象,在项目启动的时候注册到CacheManager中。

import com.galax.Config.serialize.RedisObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

@Configuration
@EnableCaching  //开启缓存,可以放在启动类上。
public class RedisSpringCache {

    /**
     * 自定义KeyGenerator。
     * @return
     */
    @Bean
    public KeyGenerator keyGenerator() {
        return (target, method, params) -> {
            //获取代理对象的最终目标对象
            Class<?> targetClass = AopProxyUtils.ultimateTargetClass(target);
            StringBuilder sb = new StringBuilder();
            sb.append(targetClass.getSimpleName()).append(":");
            sb.append(method.getName()).append(":");
            //调用SimpleKey的逻辑
            Object key = SimpleKeyGenerator.generateKey(params);
            return sb.append(key);
        };
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        //设置特有的Redis配置
        Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
        //定制化的Cache为300s
        cacheConfigurations.put("as",customRedisCacheConfiguration(Duration.ofSeconds(300)));
        cacheConfigurations.put("books",customRedisCacheConfiguration(Duration.ofSeconds(300)));
        cacheConfigurations.put("cs",customRedisCacheConfiguration(Duration.ofSeconds(300)));
        //默认超时时间60s
        return RedisCacheManager.builder(connectionFactory).
                transactionAware().   //Cache的事务支持
                cacheDefaults(customRedisCacheConfiguration(Duration.ofSeconds(60))).
                withInitialCacheConfigurations(cacheConfigurations).   //设置个性化的Cache配置
                build();
    }


    /**
     * 设置RedisConfiguration配置
     *
     * @param ttl
     * @return
     */
    public RedisCacheConfiguration customRedisCacheConfiguration(Duration ttl) {
        //设置序列化格式
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer
                = new Jackson2JsonRedisSerializer<>(Object.class);
        jackson2JsonRedisSerializer.setObjectMapper(RedisObjectMapper.redisConfigurationObjectMapper());
        return RedisCacheConfiguration.
                defaultCacheConfig().serializeValuesWith(
                RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)).
                computePrefixWith(cacheName -> cacheName + ":").   //设置Cache的前缀,默认::
                disableCachingNullValues().   //若返回值为null,则不允许存储到Cache中
                entryTtl(ttl);  //设置缓存缺省超时时间
    }
}

注意不要将ObjectMapper加入到Spring容器中。因为Spring容器中存在一个ObjectMapper,以用于@RequestBodyResponseBodyRestTemplate等地的序列化和反序列化。

为什么不采用Spring容器的ObjectMapper对象,而要自己设置是因为Redis配置了objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);属性,在序列化时记录类/属性的类型,以便在反序列化时得到POJO对象。此属性详见——Jackson的ObjectMapper.DefaultTyping.NON_FINAL属性。

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;

import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class RedisObjectMapper {

    public static ObjectMapper redisConfigurationObjectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        //JDK1.8新版时间格式化Model
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        objectMapper.registerModule(javaTimeModule);
        //Date类型禁止转换为时间戳
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        //序列化时格式化时间戳
        objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        //字段名字开启驼峰命名法
        objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
        //序列化无public的属性或方法时,不会抛出异常
        objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
        //序列化时保存对象类型,以便反序列化时直接得到具体POJO
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        //非空数据才进行格式化
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        //针对BigDecimal,序列化时,不采取科学计数法
        objectMapper.enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN);
        //反序列化时,POJO中不含有JSON串的属性,不解析该字段,并且不会抛出异常
        objectMapper.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        //反序列化{}时,不抛出异常,而是得到null值
        objectMapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
        return objectMapper;
    }
}

此处可以使用protostuff替换Jackson进行序列化和反序列化,详细内容请点击...

3. key的设置

需要注意的是,SpringCache作为应用层的声明式缓存。其数据结构为Key-Value。那么设计一个安全优雅的Key,是一个重要的任务。

  1. SpringCache官网中,这样描述SpringCache默认的KeyGenerator的:
  • 若没有参数值被得到,返回SimpleKey.EMPTY(空数组)。
  • 若只有一个参数值被得到,返回该参数值的实例。
  • 若多个参数值被得到,返回一个包含所有参数值SimpleKey对象。
  1. 默认的KeyGenerator如何获取参数值?
  • 若注解上只是指定cacheName属性,SimpleKeyGenerator将获取所有的参数值。组成SimpleKey对象。
  • 指定cacheNamekey属性,并且key的属性支持SpEL表达式:
  1. 基本形式
@Cacheable(value="cacheName", key="#id")
public ResultDTO method(int id);
  1. 组合形式
@Cacheable(value="cacheName", key="T(String).valueOf(#name).concat('-').concat(#password))
public ResultDTO method(int name, String password);
  1. 对象形式
@Cacheable(value="cacheName", key="#user.id)
public ResultDTO method(User user);
  1. 默认的SimpleKeyGenerator的缺陷

SimpleGenerator只会将参数值封装为SimpleKey对象。然后作为Key,可能会导致不同方法Key冲突。
我们虽然可以使用SpEL表达式获取类名、方法名,在进行拼接。但是需要为每一个注解指定,太过于繁杂。

  1. 自定义KeyGenerator

注解上keyGenerator属性与key属性是不共存的,即我们若通过keyGenerator来自定义我们的Key生成器,那么就需要将所有的参数值均进行处理,而不能指定特定的参数值
处理。

    @Bean
    public KeyGenerator keyGenerator() {
        return (target, method, params) -> {
            //获取代理对象的最终目标对象
            Class<?> targetClass = AopProxyUtils.ultimateTargetClass(target);
            StringBuilder sb = new StringBuilder();
            sb.append(targetClass.getSimpleName()).append(":");
            sb.append(method.getName()).append(":");
            //调用SimpleKey的逻辑
            Object key = SimpleKeyGenerator.generateKey(params);
            return sb.append(key);
        };
    }

使用:

    @Cacheable(value = "book2",keyGenerator = "keyGenerator")
    public Account getAccInfo(String customerId, String accType) {
      //业务逻辑
    }

4. 使用

springCache和事务类型,均采用AOP原理。故它们的注意事项也是相同。

  1. 若一个service中,注解方法被调用,则注解不会生效;
  2. 只有访问修饰符为public的方法,注解才会生效;

SpringCache的使用请参考——SpringBoot2.x—SpringCache(2)使用

文章参考

SpringBoot2.X整合Redis缓存

https://www.cnblogs.com/zhangjianbin/p/6439206.html

https://www.cnblogs.com/wzdnwyyu/p/11180461.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,794评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,050评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,587评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,861评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,901评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,898评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,832评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,617评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,077评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,349评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,483评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,199评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,824评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,442评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,632评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,474评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,393评论 2 352