SpringBoot 2.0 之使用聲明式註解簡化緩存

SpringBoot 2.X中使用緩存註解時,發現某些配置方面與SpringBoot 1.X有些異同,這裏做爲第一篇博客記錄下來。java

1.建立SpringBoot 2.0.1 工程,引入spring-boot-starter-data-redis依賴redis

 

2.配置redis的鏈接屬性spring

spring:
  redis:
    password: 
    host: localhost
    port: 6379
  cache:
    type: redis

spring.cache.type 指定註解緩存使用redis緩存

 

3.添加@EnableCaching開啓緩存註解springboot

@EnableCaching
@SpringBootApplication
public class SpringBoot2Application {
    public static void main(String[] args) {
        SpringApplication.run(SpringBoot2Application.class, args);
    }
}

 

4.使用緩存註解app

通用屬性解釋:dom

value屬性:要使用緩存的名稱spring-boot

key屬性:使用SpEL表達式自定義緩存Key,例如:#name—以參數name做爲自定義緩存Key,#result.name—以返回值結果的name屬性做爲自定義緩存Key源碼分析

 

(1)@Cacheable註解ui

若是沒有緩存則會執行方法並將返回值緩存,若是有緩存時,不會執行方法而是直接返回緩存中的值

@GetMapping
@Cacheable(value = "users", key = "#name")
public User getByName(String name) {
    log.info("get user info...");
    User user = new User();
    user.setName(name);
    return user;
}

第一次發起GET請求/users?name=123456時,會輸出get user info...日誌而且查看redis會發現如下結果

再次發起一個GET請求/users?name=123456,並無日誌輸出,可見有緩存時並不會調用方法。

 

(2)@CachePut註解

無論有沒有緩存都會執行方法並將結果緩存起來

@PostMapping
@CachePut(value = "users", key = "#result.name")
public User add(String name) {
    User user = new User();
    user.setId(1);
    user.setName(name);
    if (userRepository.save(user)) {
        log.info("用戶:%s 保存成功!\n", user);
    }
    return user;
}

發起POST請求/users?name=123456,可見每次都會打印保存成功的日誌而且將最新的結果緩存起來

 

(3)@CacheEvict註解

移除指定緩存

@DeleteMapping
@CacheEvict(value = "users", key = "#name")
public void deleteByName(String name) {
    log.info("delete user by name :{}", name);
}

發起DELETE請求/users?name=123456,可見每次都會打印刪除日誌而且刪除緩存

注意:

a.User對象須要實現序列化接口

b.只有@CacheEvict註解的方法返回值能夠爲void

 

5.使用@CacheConfig註解進一步簡化

@Slf4j
@CacheConfig(cacheNames = "users")
@RestController
@RequestMapping("/users")
public class UserController {
    private final UserRepository userRepository;

    @Autowired
    public UserController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @PostMapping
    @CachePut
    public User add(String name) {
        User user = new User();
        user.setName(name);
        if (userRepository.save(user)) {
            log.info("用戶:%s 保存成功!\n", user);
        }
        return user;
    }

    @GetMapping
    @Cacheable
    public User getByName(String name) {
        log.info("get user info...");
        User user = new User();
        user.setName(name);
        return user;
    }

    @DeleteMapping
    @CacheEvict
    public void deleteByName(String name) {
        log.info("delete user by name :{}", name);
    }
}

使用@CacheConfig聲明類下的緩存註解的value默認是"users",讓代碼更簡潔、優雅,效果與上面同樣。

 

6.指定序列化器

上面就已經簡單的使用了緩存註解,實際上還有不少註解屬性沒有介紹到,由於並非本篇的重點,有興趣的小夥伴能夠本身進行拓展,好比,條件化緩存,XML方式對源碼中的方法進行緩存,分組緩存等特性。上面當咱們去redis查看value值時,咱們一臉懵逼根本看不懂存的是啥東西。下面讓咱們自定義序列化器來讓緩存的value值透明化,這裏的自定義配置也是SpringBoot 2.X 與SpringBoot 1.X不同的地方。

@Bean
public RedisCacheConfiguration redisCacheConfiguration() {
    return RedisCacheConfiguration
            .defaultCacheConfig()
            .serializeKeysWith(
                    RedisSerializationContext
                            .SerializationPair
                            .fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(
                    RedisSerializationContext
                            .SerializationPair
                            .fromSerializer(new GenericJackson2JsonRedisSerializer()));
}

當咱們添加以上配置後,使用緩存註解時會使用StringRedisSerializer對Key進行序列化,使用GenericJackson2JsonRedisSerializer對Value進行反序列化。

此時從新啓動咱們刪除原有的緩存後再次發起一個GET請求/users?name=123456

與以前效果同樣,不同的在於咱們終於能看懂緩存中Value裝的是啥玩意兒了,以下圖:

class用於在反序列化時將其由JSON數據反序列化爲User實體

id與name都是User實體中的屬性

 

7.源碼分析

SpringBoot 1.X 中自定義序列化器一般是聲明一個RedisCacheManager並在其構造中傳一個RedisTemplate,接着對RedisTemplate配置自定義序列化器就可達到自定義序列化器的目的。可是SpringBoot 2.X 中你會發現RedisCacheManager的構造方法徹底變樣了,再也不是依賴RedisTemplate。下面來走源碼分析一波。

首先,咱們進到RedisCacheConfiguration看一波

//默認聲明的RedisCacheManager
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory, ResourceLoader resourceLoader) {
    //與Spring Boot 1.X 中RedisCacheManager的構造方式不一樣
    RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(this.determineConfiguration(resourceLoader.getClassLoader()));
    List<String> cacheNames = this.cacheProperties.getCacheNames();
    if (!cacheNames.isEmpty()) {
        builder.initialCacheNames(new LinkedHashSet(cacheNames));
    }

    return (RedisCacheManager)this.customizerInvoker.customize(builder.build());
}

SpringBoot 的契約優於配置的特性已經幫咱們配了一個默認的RedisCacheManager

//決定使用的RedisCacheConfiguration配置
private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration(ClassLoader classLoader) {
    //若是有自定義的RedisCacheConfiguration則使用自定義的
    if (this.redisCacheConfiguration != null) {
        return this.redisCacheConfiguration;
    //沒有則使用默認配置
    } else {
        Redis redisProperties = this.cacheProperties.getRedis();
        org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration.defaultCacheConfig();
        //默認使用JDK自帶的序列化器
        config = config.serializeValuesWith(SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader)));
        if (redisProperties.getTimeToLive() != null) {
            config = config.entryTtl(redisProperties.getTimeToLive());
        }

        if (redisProperties.getKeyPrefix() != null) {
            config = config.prefixKeysWith(redisProperties.getKeyPrefix());
        }

        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }

        if (!redisProperties.isUseKeyPrefix()) {
            config = config.disableKeyPrefix();
        }

        return config;
    }
}

能夠發現若是沒有找到RedisCacheConfiguration Bean對象 則使用默認配置中的序列化器—JDK自帶的序列化器,若是有就使用自定義的RedisCacheConfiguration,這就能夠解釋爲何咱們想使用自定義的序列化器須要聲明一個RedisCacheConfiguration Bean對象了。

 

8.Jackson序列化器

Jackson序列化器其實有兩個,Jackson2JsonRedisSerializer和咱們上面使用的GenericJackson2JsonRedisSerializer。

若是使用Jackson2JsonRedisSerializer在反序列化時會遇到問題,由於沒有具體泛型或泛型爲Object時,會將緩存中的數據反序列化爲LinkedHashMap,而咱們須要的是User對象,所以就會拋出一個異常。

@Bean
public RedisCacheConfiguration redisCacheConfiguration() {
    return RedisCacheConfiguration
            .defaultCacheConfig()
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
            //若是將Object.class替換爲User.class也能夠暫時解決問題可是這就不是一個通用的序列化器了
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new Jackson2JsonRedisSerializer<>(Object.class)));
}

Jackson2JsonRedisSerializer<T>源碼

public T deserialize(@Nullable byte[] bytes) throws SerializationException {
    if (SerializationUtils.isEmpty(bytes)) {
        return null;
    } else {
        try {
            return this.objectMapper.readValue(bytes, 0, bytes.length, this.javaType);
        } catch (Exception var3) {
            throw new SerializationException("Could not read JSON: " + var3.getMessage(), var3);
        }
    }
}

使用Jackson2JsonRedisSerializer反序列化時的轉換異常

java.lang.ClassCastException: java.base/java.util.LinkedHashMap cannot be cast to com.springboot2.domain.User

而使用GenericJackson2JsonRedisSerializer就能夠避免這種狀況。

相關文章
相關標籤/搜索