当前位置:主页 > 查看内容

jetcache教程

发布时间:2021-08-18 00:00| 位朋友查看

简介:jetcache简介 JetCache是一个基于Java的缓存系统封装 提供统一的API和注解来简化缓存的使用。 JetCache提供了比SpringCache更加强大的注解 可以原生的支持TTL、两级缓存、分布式自动刷新 还提供了Cache接口用于手工缓存操作。当前有四个实现 RedisCache、Tai……
jetcache简介

JetCache是一个基于Java的缓存系统封装 提供统一的API和注解来简化缓存的使用。 JetCache提供了比SpringCache更加强大的注解 可以原生的支持TTL、两级缓存、分布式自动刷新 还提供了Cache接口用于手工缓存操作。当前有四个实现 RedisCache、TairCache 此部分未在github开源 、CaffeineCache(in memory)和一个简易的LinkedHashMapCache(in memory) 要添加新的实现也是非常简单的。

全部特性:

通过统一的API访问Cache系统通过注解实现声明式的方法缓存 支持TTL和两级缓存通过注解创建并配置Cache实例针对所有Cache实例和方法缓存的自动统计Key的生成策略和Value的序列化策略是可以配置的分布式缓存自动刷新 分布式锁 (2.2 )异步Cache API (2.2 使用Redis的lettuce客户端时)Spring Boot支持要求

JetCache需要JDK1.8、Spring Framework4.0.8以上版本。Spring Boot为可选 需要1.1.9以上版本。如果不使用注解 仅使用jetcache-core Spring Framework也是可选的 此时使用方式与Guava/Caffeinecache类似。

文档目录快速入门基本Cache API通过 CreateCache注解创建Cache实例通过注解实现方法缓存配置详解高级Cache APIRedis支持(两种redis客户端二选一即可) 使用jedis客户端连接redis使用lettuce客户端连接redis内存缓存LinkedHashMapCache和CaffeineCache统计Builder 未使用Spring4 或未使用Spring 的时候 或通过Builder手工构造Cache开发者文档升级和兼容性指南依赖哪个jar jetcache-anno-api 定义jetcache的注解和常量 不传递依赖。如果你想把Cached注解加到接口上 又不希望你的接口jar传递太多依赖 可以让接口jar依赖jetcache-anno-api。jetcache-core 核心api 完全通过编程来配置操作Cache 不依赖Spring。两个内存中的缓存实现LinkedHashMapCache和CaffeineCache也由它提供。jetcache-anno 基于Spring提供 Cached和 CreateCache注解支持。jetcache-redis 使用jedis提供Redis支持。jetcache-redis-lettuce 需要JetCache2.3以上版本 使用lettuce提供Redis支持 实现了JetCache异步访问缓存的的接口。jetcache-starter-redis Spring Boot方式的Starter 基于Jedis。jetcache-starter-redis-lettuce 需要JetCache2.3以上版本 Spring Boot方式的Starter 基于Lettuce。快速入门创建缓存实例

通过 CreateCache注解创建一个缓存实例 默认超时时间是100秒

 CreateCache(expire 100)
private Cache Long, UserDO userCache;

image.gif

用起来就像map一样

UserDO user userCache.get(123L);
userCache.put(123L, user);
userCache.remove(123L);

image.gif

创建一个两级 内存 远程 的缓存 内存中的元素个数限制在50个。

 CreateCache(name UserService.userCache , expire 100, cacheType CacheType.BOTH, localLimit 50)
private Cache Long, UserDO userCache;

image.gif

name属性不是必须的 但是起个名字是个好习惯 展示统计数据的使用 会使用这个名字。如果同一个area两个 CreateCache的name配置一样 它们生成的Cache将指向同一个实例。

创建方法缓存

使用 Cached方法可以为一个方法添加上缓存。JetCache通过Spring AOP生成代理 来支持缓存功能。注解可以加在接口方法上也可以加在类方法上 但需要保证是个Springbean。

public interface UserService {
 Cached(name UserService.getUserById , expire 3600)
 User getUserById(long userId);
}

image.gif

基本配置 使用Spring Boot

如果使用SpringBoot 可以按如下的方式配置。

POM
 dependency 
 groupId com.alicp.jetcache /groupId 
 artifactId jetcache-starter-redis /artifactId 
 version 2.4.4 /version 
 /dependency 

image.gif

配置一个springboot风格的application.yml文件 把他放到资源目录中

jetcache:
 statIntervalMinutes: 15
 areaInCacheName: false
 local:
 default:
 type: linkedhashmap
 keyConvertor: fastjson
 remote:
 default:
 type: redis
 keyConvertor: fastjson
 valueEncoder: java
 valueDecoder: java
 poolConfig:
 minIdle: 5
 maxIdle: 20
 maxTotal: 50
 host: 127.0.0.1
 port: 6379

image.gif

然后创建一个App类放在业务包的根下 EnableMethodCache EnableCreateCacheAnnotation这两个注解分别激活Cached和CreateCache注解 其他和标准的SpringBoot程序是一样的。这个类可以直接main方法运行。

package com.company.mypackage;
import com.alicp.jetcache.anno.config.EnableCreateCacheAnnotation;
import com.alicp.jetcache.anno.config.EnableMethodCache;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 SpringBootApplication
 EnableMethodCache(basePackages com.company.mypackage )
 EnableCreateCacheAnnotation
public class MySpringBootApp {
 public static void main(String[] args) {
 SpringApplication.run(MySpringBootApp.class);
}

image.gif

未使用SpringBoot的配置方式

如果没有使用springboot 可以按下面的方式配置 这里使用jedis客户端连接redis为例 。

 dependency 
 groupId com.alicp.jetcache /groupId 
 artifactId jetcache-anno /artifactId 
 version 2.4.4 /version 
 /dependency 
 dependency 
 groupId com.alicp.jetcache /groupId 
 artifactId jetcache-redis /artifactId 
 version 2.4.4 /version 
 /dependency 

image.gif

配置了这个JetCacheConfig类以后 可以使用 CreateCache和 Cached注解。

package com.company.mypackage;
import java.util.HashMap;
import java.util.Map;
import com.alicp.jetcache.anno.CacheConsts;
import com.alicp.jetcache.anno.config.EnableCreateCacheAnnotation;
import com.alicp.jetcache.anno.config.EnableMethodCache;
import com.alicp.jetcache.anno.support.GlobalCacheConfig;
import com.alicp.jetcache.anno.support.SpringConfigProvider;
import com.alicp.jetcache.embedded.EmbeddedCacheBuilder;
import com.alicp.jetcache.embedded.LinkedHashMapCacheBuilder;
import com.alicp.jetcache.redis.RedisCacheBuilder;
import com.alicp.jetcache.support.FastjsonKeyConvertor;
import com.alicp.jetcache.support.JavaValueDecoder;
import com.alicp.jetcache.support.JavaValueEncoder;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.util.Pool;
 Configuration
 EnableMethodCache(basePackages com.company.mypackage )
 EnableCreateCacheAnnotation
public class JetCacheConfig {
 Bean
 public Pool Jedis pool(){
 GenericObjectPoolConfig pc new GenericObjectPoolConfig();
 pc.setMinIdle(2);
 pc.setMaxIdle(10);
 pc.setMaxTotal(10);
 return new JedisPool(pc, localhost , 6379);
 Bean
 public SpringConfigProvider springConfigProvider() {
 return new SpringConfigProvider();
 Bean
 public GlobalCacheConfig config(SpringConfigProvider configProvider, Pool Jedis pool){
 Map localBuilders new HashMap();
 EmbeddedCacheBuilder localBuilder LinkedHashMapCacheBuilder
 .createLinkedHashMapCacheBuilder()
 .keyConvertor(FastjsonKeyConvertor.INSTANCE);
 localBuilders.put(CacheConsts.DEFAULT_AREA, localBuilder);
 Map remoteBuilders new HashMap();
 RedisCacheBuilder remoteCacheBuilder RedisCacheBuilder.createRedisCacheBuilder()
 .keyConvertor(FastjsonKeyConvertor.INSTANCE)
 .valueEncoder(JavaValueEncoder.INSTANCE)
 .valueDecoder(JavaValueDecoder.INSTANCE)
 .jedisPool(pool);
 remoteBuilders.put(CacheConsts.DEFAULT_AREA, remoteCacheBuilder);
 GlobalCacheConfig globalCacheConfig new GlobalCacheConfig();
 globalCacheConfig.setConfigProvider(configProvider);
 globalCacheConfig.setLocalCacheBuilders(localBuilders);
 globalCacheConfig.setRemoteCacheBuilders(remoteBuilders);
 globalCacheConfig.setStatIntervalMinutes(15);
 globalCacheConfig.setAreaInCacheName(false);
 return globalCacheConfig;
}

image.gif

进一步阅读CreateCache的详细使用说明可以看这里使用 CacheCache创建的Cache接口实例 它的API使用可以看这里关于方法缓存( Cached, CacheUpdate, CacheInvalidate)的详细使用看这里详细的配置说明看这里。基本Cache API简介

JetCache2.0的核心是com.alicp.jetcache.Cache接口 以下简写为Cache 它提供了部分类似于javax.cache.Cache JSR107 的API操作。没有完整实现JSR107的原因包括

希望维持API的简单易用。对于特定的远程缓存系统来说 javax.cache.Cache中定义的有些操作无法高效率的实现 比如一些原子操作方法和类似removeAll()这样的方法。JSR107比较复杂 完整实现要做的工作很多。JSR107 style API

以下是Cache接口中和JSR107的javax.cache.Cache接口一致的方法 除了不会抛出异常 这些方法的签名和行为和JSR107都是一样的。

V get(K key)
void put(K key, V value);
boolean putIfAbsent(K key, V value); //多级缓存MultiLevelCache不支持此方法
boolean remove(K key);
 T T unwrap(Class T clazz);//2.2版本前 多级缓存MultiLevelCache不支持此方法
Map K,V getAll(Set ? extends K keys);
void putAll(Map ? extends K,? extends V map);
void removeAll(Set ? extends K keys);

image.gif

JetCache特有API
V computeIfAbsent(K key, Function K, V loader)

image.gif

当key对应的缓存不存在时 使用loader加载。通过这种方式 loader的加载时间可以被统计到。

V computeIfAbsent(K key, Function K, V loader, boolean cacheNullWhenLoaderReturnNull)

image.gif

当key对应的缓存不存在时 使用loader加载。cacheNullWhenLoaderReturnNull参数指定了当loader加载出来时null值的时候 是否要进行缓存 有时候即使是null值也是通过很繁重的查询才得到的 需要缓存 。

V computeIfAbsent(K key, Function K, V loader, boolean cacheNullWhenLoaderReturnNull, long expire, TimeUnit timeUnit)

image.gif

当key对应的缓存不存在时 使用loader加载。cacheNullWhenLoaderReturnNull参数指定了当loader加载出来时null值的时候 是否要进行缓存 有时候即使是null值也是通过很繁重的查询才得到的 需要缓存 。expire和timeUnit指定了缓存的超时时间 会覆盖缓存的默认超时时间。

void put(K key, V value, long expire, TimeUnit timeUnit)

image.gif

put操作 expire和timeUnit指定了缓存的超时时间 会覆盖缓存的默认超时时间。

AutoReleaseLock tryLock(K key, long expire, TimeUnit timeUnit)

image.gif

boolean tryLockAndRun(K key, long expire, TimeUnit timeUnit, Runnable action)

image.gif

非堵塞的尝试获取一个锁 如果对应的key还没有锁 返回一个AutoReleaseLock 否则立即返回空。如果Cache实例是本地的 它是一个本地锁 在本JVM中有效 如果是redis等远程缓存 它是一个不十分严格的分布式锁。锁的超时时间由expire和timeUnit指定。多级缓存的情况会使用最后一级做tryLock操作。用法如下

// 使用try-with-resource方式 可以自动释放锁
 try(AutoReleaseLock lock cache.tryLock( MyKey ,100, TimeUnit.SECONDS)){
 if(lock ! null){
 // do something
 }

image.gif

上面的代码有个潜在的坑是忘记判断if(lock! null) 所以一般可以直接用tryLockAndRun更加简单

boolean hasRun tryLockAndRun( MyKey ,100, TimeUnit.SECONDS), () - {
 // do something
 };

image.gif

tryLock内部会在访问远程缓存失败时重试 会自动释放 而且不会释放不属于自己的锁 比你自己做这些要简单。当然 基于远程缓存实现的任何分布式锁都不会是严格的分布式锁 不能和基于ZooKeeper或Consul做的锁相比。

大写API

Vget(K key)这样的方法虽然用起来方便 但有功能上的缺陷 当get返回null的时候 无法断定是对应的key不存在 还是访问缓存发生了异常 所以JetCache针对部分操作提供了另外一套API 提供了完整的返回值 包括

CacheGetResult V GET(K key);
MultiGetResult K, V GET_ALL(Set ? extends K keys);
CacheResult PUT(K key, V value);
CacheResult PUT(K key, V value, long expireAfterWrite, TimeUnit timeUnit);
CacheResult PUT_ALL(Map ? extends K, ? extends V map);
CacheResult PUT_ALL(Map ? extends K, ? extends V map, long expireAfterWrite, TimeUnit timeUnit);
CacheResult REMOVE(K key);
CacheResult REMOVE_ALL(Set ? extends K keys);
CacheResult PUT_IF_ABSENT(K key, V value, long expireAfterWrite, TimeUnit timeUnit);

image.gif

这些方法的特征是方法名为大写 与小写的普通方法对应 提供了完整的返回值 用起来也稍微繁琐一些。例如

CacheGetResult OrderDO r cache.GET(orderId);
if( r.isSuccess() ){
 OrderDO order r.getValue();
} else if (r.getResultCode() CacheResultCode.NOT_EXISTS) {
 System.out.println( cache miss: orderId);
} else if(r.getResultCode() CacheResultCode.EXPIRED) {
 System.out.println( cache expired: orderId));
} else {
 System.out.println( cache get error: orderId);
}

image.gif

通过 CreateCache注解创建Cache实例简介

在Springbean中使用 CreateCache注解创建一个Cache实例。例如

 CreateCache(expire 100)
private Cache Long, UserDO userCache;

image.gif

CreateCache属性表

属性

默认值

说明

area

“default”

如果需要连接多个缓存系统 可在配置多个cache area 这个属性指定要使用的那个area的name

name

未定义

指定缓存的名称 不是必须的 如果没有指定 会使用类名 方法名。name会被用于远程缓存的key前缀。另外在统计中 一个简短有意义的名字会提高可读性。如果两个 CreateCache的name和area相同 它们会指向同一个Cache实例

expire

未定义

该Cache实例的默认超时时间定义 注解上没有定义的时候会使用全局配置 如果此时全局配置也没有定义 则取无穷大

timeUnit

TimeUnit.SECONDS

指定expire的单位

cacheType

CacheType.REMOTE

缓存的类型 包括CacheType.REMOTE、CacheType.LOCAL、CacheType.BOTH。如果定义为BOTH 会使用LOCAL和REMOTE组合成两级缓存

localLimit

未定义

如果cacheType为CacheType.LOCAL或CacheType.BOTH 这个参数指定本地缓存的最大元素数量 以控制内存占用。注解上没有定义的时候会使用全局配置 如果此时全局配置也没有定义 则取100

serialPolicy

未定义

如果cacheType为CacheType.REMOTE或CacheType.BOTH 指定远程缓存的序列化方式。JetCache内置的可选值为SerialPolicy.JAVA和SerialPolicy.KRYO。注解上没有定义的时候会使用全局配置 如果此时全局配置也没有定义 则取SerialPolicy.JAVA

keyConvertor

未定义

指定KEY的转换方式 用于将复杂的KEY类型转换为缓存实现可以接受的类型 JetCache内置的可选值为KeyConvertor.FASTJSON和KeyConvertor.NONE。NONE表示不转换 FASTJSON通过fastjson将复杂对象KEY转换成String。如果注解上没有定义 则使用全局配置。

默认值

对于以上未定义默认值的参数 如果没有指定 将使用yml中指定的全局配置 请参考配置详解。

通过注解实现方法缓存

JetCache方法缓存和SpringCache比较类似 它原生提供了TTL支持 以保证最终一致 并且支持二级缓存。JetCache2.4以后支持基于注解的缓存更新和删除。

在spring环境下 使用 Cached注解可以为一个方法添加缓存 CacheUpdate用于更新缓存 CacheInvalidate用于移除缓存元素。注解可以加在接口上也可以加在类上 加注解的类必须是一个spring bean 例如

public interface UserService {
 Cached(name userCache. , key #userId , expire 3600)
 User getUserById(long userId);
 CacheUpdate(name userCache. , key #user.userId , value #user )
 void updateUser(User user);
 CacheInvalidate(name userCache. , key #userId )
 void deleteUser(long userId);
}

image.gif

key使用Spring的SpEL脚本来指定。如果要使用参数名 比如这里的key #userId 项目编译设置target必须为1.8格式 并且指定javac的-parameters参数 否则就要使用key args[0] 这样按下标访问的形式。

CacheUpdate和 CacheInvalidate的name和area属性必须和 Cached相同 name属性还会用做cache的key前缀。

Cached注解和 CreateCache的属性非常类似 但是多几个

属性

默认值

说明

area

“default”

如果在配置中配置了多个缓存area 在这里指定使用哪个area

name

未定义

指定缓存的唯一名称 不是必须的 如果没有指定 会使用类名 方法名。name会被用于远程缓存的key前缀。另外在统计中 一个简短有意义的名字会提高可读性。

key

未定义

使用SpEL指定key 如果没有指定会根据所有参数自动生成。

expire

未定义

超时时间。如果注解上没有定义 会使用全局配置 如果此时全局配置也没有定义 则为无穷大

timeUnit

TimeUnit.SECONDS

指定expire的单位

cacheType

CacheType.REMOTE

缓存的类型 包括CacheType.REMOTE、CacheType.LOCAL、CacheType.BOTH。如果定义为BOTH 会使用LOCAL和REMOTE组合成两级缓存

localLimit

未定义

如果cacheType为LOCAL或BOTH 这个参数指定本地缓存的最大元素数量 以控制内存占用。如果注解上没有定义 会使用全局配置 如果此时全局配置也没有定义 则为100

serialPolicy

未定义

指定远程缓存的序列化方式。可选值为SerialPolicy.JAVA和SerialPolicy.KRYO。如果注解上没有定义 会使用全局配置 如果此时全局配置也没有定义 则为SerialPolicy.JAVA

keyConvertor

未定义

指定KEY的转换方式 用于将复杂的KEY类型转换为缓存实现可以接受的类型 当前支持KeyConvertor.FASTJSON和KeyConvertor.NONE。NONE表示不转换 FASTJSON可以将复杂对象KEY转换成String。如果注解上没有定义 会使用全局配置。

enabled

true

是否激活缓存。例如某个dao方法上加缓存注解 由于某些调用场景下不能有缓存 所以可以设置enabled为false 正常调用不会使用缓存 在需要的地方可使用CacheContext.enableCache在回调中激活缓存 缓存激活的标记在ThreadLocal上 该标记被设置后 所有enable false的缓存都被激活

cacheNullValue

false

当方法返回值为null的时候是否要缓存

condition

未定义

使用SpEL指定条件 如果表达式返回true的时候才进行缓存

CacheInvalidate注解说明

属性

默认值

说明

area

“default”

如果在配置中配置了多个缓存area 在这里指定使用哪个area 指向对应的 Cached定义。

name

未定义

指定缓存的唯一名称 指向对应的 Cached定义。

key

未定义

使用SpEL指定key

condition

未定义

使用SpEL指定条件 如果表达式返回true才执行删除

CacheUpdate注解说明

属性

默认值

说明

area

“default”

如果在配置中配置了多个缓存area 在这里指定使用哪个area 指向对应的 Cached定义。

name

未定义

指定缓存的唯一名称 指向对应的 Cached定义。

key

未定义

使用SpEL指定key

value

未定义

使用SpEL指定value

condition

未定义

使用SpEL指定条件 如果表达式返回true才执行更新

使用 CacheUpdate和 CacheInvalidate的时候 相关的缓存操作可能会失败 比如网络IO错误 所以指定缓存的超时时间是非常重要的。

CacheRefresh注解说明

属性

默认值

说明

refresh

未定义

刷新间隔

timeUnit

TimeUnit.SECONDS

时间单位

stopRefreshAfterLastAccess

未定义

指定该key多长时间没有访问就停止刷新 如果不指定会一直刷新

refreshLockTimeout

60秒

类型为BOTH/REMOTE的缓存刷新时 同时只会有一台服务器在刷新 这台服务器会在远程缓存放置一个分布式锁 此配置指定该锁的超时时间

对于以上未定义默认值的参数 如果没有指定 将使用yml中指定的全局配置 全局配置请参考配置说明

配置详解

yml配置文件案例 如果没使用springboot 直接配置GlobalCacheConfig是类似的 参考快速入门教程

jetcache:
 statIntervalMinutes: 15
 areaInCacheName: false
 hiddenPackages: com.alibaba
 local:
 default:
 type: caffeine
 limit: 100
 keyConvertor: fastjson
 expireAfterWriteInMillis: 100000
 otherArea:
 type: linkedhashmap
 limit: 100
 keyConvertor: none
 expireAfterWriteInMillis: 100000
 remote:
 default:
 type: redis
 keyConvertor: fastjson
 valueEncoder: java
 valueDecoder: java
 poolConfig:
 minIdle: 5
 maxIdle: 20
 maxTotal: 50
 host: ${redis.host}
 port: ${redis.port}
 otherArea:
 type: redis
 keyConvertor: fastjson
 valueEncoder: kryo
 valueDecoder: kryo
 poolConfig:
 minIdle: 5
 maxIdle: 20
 maxTotal: 50
 host: ${redis.host}
 port: ${redis.port}

image.gif

配置通用说明如下

属性

默认值

说明

jetcache.statIntervalMinutes

0

统计间隔 0表示不统计

jetcache.areaInCacheName

true

jetcache-anno把cacheName作为远程缓存key前缀 2.4.3以前的版本总是把areaName加在cacheName中 因此areaName也出现在key前缀中。2.4.4以后可以配置 为了保持远程key兼容默认值为true 但是新项目的话false更合理些。

jetcache.hiddenPackages

Cached和 CreateCache自动生成name的时候 为了不让name太长 hiddenPackages指定的包名前缀被截掉

jetcache.[local|remote].${area}.type

缓存类型。tair、redis为当前支持的远程缓存 linkedhashmap、caffeine为当前支持的本地缓存类型

jetcache.[local|remote].${area}.keyConvertor

key转换器的全局配置 当前只有一个已经实现的keyConvertor fastjson。仅当使用 CreateCache且缓存类型为LOCAL时可以指定为none 此时通过equals方法来识别key。方法缓存必须指定keyConvertor

jetcache.[local|remote].${area}.valueEncoder

java

序列化器的全局配置。仅remote类型的缓存需要指定 可选java和kryo

jetcache.[local|remote].${area}.valueDecoder

java

序列化器的全局配置。仅remote类型的缓存需要指定 可选java和kryo

jetcache.[local|remote].${area}.limit

100

每个缓存实例的最大元素的全局配置 仅local类型的缓存需要指定。注意是每个缓存实例的限制 而不是全部 比如这里指定100 然后用 CreateCache创建了两个缓存实例 并且注解上没有设置localLimit属性 那么每个缓存实例的限制都是100

jetcache.[local|remote].${area}.expireAfterWriteInMillis

无穷大

以毫秒为单位指定超时时间的全局配置(以前为defaultExpireInMillis)

jetcache.local.${area}.expireAfterAccessInMillis

0

需要jetcache2.2以上 以毫秒为单位 指定多长时间没有访问 就让缓存失效 当前只有本地缓存支持。0表示不使用这个功能。

上表中${area}对应 Cached和 CreateCache的area属性。注意如果注解上没有指定area 默认值是 default 。

关于缓存的超时时间 有多个地方指定 澄清说明一下

put等方法上指定了超时时间 则以此时间为准put等方法上未指定超时时间 使用Cache实例的默认超时时间Cache实例的默认超时时间 通过在 CreateCache和 Cached上的expire属性指定 如果没有指定 使用yml中定义的全局配置 例如 Cached(cacheType local)使用jetcache.local.default.expireAfterWriteInMillis 如果仍未指定则是无穷大高级Cache APICacheBuilder

CacheBuilder提供使用代码直接构造Cache实例的方式 使用说明看这里。如果没有使用Spring 可以使用CacheBuilder 否则没有必要直接使用CacheBuilder。

异步API

从JetCache2.2版本开始 所有的大写API返回的CacheResult都支持异步。当底层的缓存实现支持异步的时候 大写API返回的结果都是异步的。当前支持异步的实现只有jetcache的redis-luttece实现 其他的缓存实现 内存中的、Tair、Jedis等 所有的异步接口都会同步堵塞 这样API仍然是兼容的。

以下的例子假设使用redis-luttece访问cache 例如

CacheGetResult UserDO r cache.GET(userId);

image.gif

这一行代码执行完以后 缓存操作可能还没有完成 如果此时调用r.isSuccess()或者r.getValue()或者r.getMessage()将会堵塞直到缓存操作完成。如果不想被堵塞 并且需要在缓存操作完成以后执行后续操作 可以这样做

CompletionStage ResultData future r.future();
future.thenRun(() - {
 if(r.isSuccess()){
 System.out.println(r.getValue());
});

image.gif

以上代码将会在缓存操作异步完成后 在完成异步操作的线程中调用thenRun中指定的回调。CompletionStage是Java8新增的功能 如果对此不太熟悉可以先查阅相关的文档。需要注意的是 既然已经选择了异步的开发方式 在回调中不能调用堵塞方法 以免堵塞其他的线程 回调方法很可能是在event loop线程中执行的 。

部分小写的api不需要任何修改 就可以直接享受到异步开发的好处。比如put和removeAll方法 由于它们没有返回值 所以此时就直接优化成异步调用 能够减少RT 而get方法由于需要取返回值 所以仍然会堵塞。

自动load read through

LoadingCache类提供了自动load的功能 它是一个包装 基于decorator模式 也实现了Cache接口。如果CacheBuilder指定了loader 那么buildCache返回的Cache实例就是经过LoadingCache包装过的。例如

Cache Long,UserDO userCache LinkedHashMapCacheBuilder.createLinkedHashMapCacheBuilder()
 .loader(key - loadUserFromDatabase(key))
 .buildCache();

image.gif

LoadingCache的get和getAll方法 在缓存未命中的情况下 会调用loader 如果loader抛出一场 get和getAll会抛出CacheInvokeException。

需要注意

GET、GET_ALL这类大写API只纯粹访问缓存 不会调用loader。如果使用多级缓存 loader应该安装在MultiLevelCache上 不要安装在底下的缓存上。

注解的属性只能是常量 所以没有办法在CreateCache注解中指定loader 不过我们可以这样

 CreateCache
private Cache Long,UserDO userCache;
 PostConstruct
public void init(){
 userCache.config().setLoader(this::loadUserFromDatabase);
}

image.gif

CreateCache总是初始化一个经过LoadingCache包装的Cache 直接在config中设置loader 可以实时生效。

自动刷新缓存

从JetCache2.2版本开始 RefreshCache基于decorator模式提供了自动刷新的缓存的能力 目的是为了防止缓存失效时造成的雪崩效应打爆数据库。同时设置了loader和refreshPolicy的时候 CacheBuilder的buildCache方法返回的Cache实例经过了RefreshCache的包装。

RefreshPolicy policy RefreshPolicy.newPolicy(1, TimeUnit.MINUTES)
 .stopRefreshAfterLastAccess(30, TimeUnit.MINUTES);
Cache String, Long orderSumCache LinkedHashMapCacheBuilder
 .createLinkedHashMapCacheBuilder()
 .loader(key - loadOrderSumFromDatabase(key))
 .refreshPolicy(policy)
 .buildCache();

image.gif

对一些key比较少 实时性要求不高 加载开销非常大的缓存场景 适合使用自动刷新。上面的代码指定每分钟刷新一次 30分钟如果没有访问就停止刷新。如果缓存是redis或者多级缓存最后一级是redis 缓存加载行为是全局唯一的 也就是说不管有多少台服务器 同时只有一个服务器在刷新 这是通过tryLock实现的 目的是为了降低后端的加载负担。

与LoadingCache一样 使用 CreateCache时 我们需要这样来添加自动刷新功能

 CreateCache
private Cache String, Long orderSumCache;
 PostConstruct
public void init(){
 RefreshPolicy policy RefreshPolicy.newPolicy(1, TimeUnit.MINUTES)
 .stopRefreshAfterLastAccess(30, TimeUnit.MINUTES);
 orderSumCache.config().setLoader(this::loadOrderSumFromDatabase);
 orderSumCache.config().setRefreshPolicy(policy);
}

image.gif

Redis支持(两种redis客户端二选一即可)使用jedis客户端连接redis

redis有多种java版本的客户端 JetCache2.2以前使用jedis客户端访问redis。从JetCache2.2版本开始 增加了对luttece客户端的支持 jetcache的luttece支持提供了异步操作和redis集群支持。

如果选用jedis访问redis 对应的maven artifact是jetcache-redis和jetcache-starter-redis(spring boot)。

spring boot环境下的jedis支持

application.yml文件如下 这里省去了local相关的配置

jetcache: 
 areaInCacheName: false
 remote:
 default:
 type: redis
 keyConvertor: fastjson
 poolConfig:
 minIdle: 5
 maxIdle: 20
 maxTotal: 50
 host: ${redis.host}
 port: ${redis.port}
 #sentinels: 127.0.0.1:26379 , 127.0.0.1:26380, 127.0.0.1:26381
 #masterName: mymaster

image.gif

image.gif

如果需要直接操作JedisPool 可以通过以下方式获取

 Bean(name defaultPool )
 DependsOn(RedisAutoConfiguration.AUTO_INIT_BEAN_NAME)//jetcache2.2 
// DependsOn( redisAutoInit )//jetcache2.1
public JedisPoolFactory defaultPool() {
 return new JedisPoolFactory( remote.default , JedisPool.class);
}

image.gif

然后可以直接使用

 Autowired
private Pool Jedis defaultPool;

image.gif

也可以用Cache接口上的 T Tunwrap(Class T clazz)方法来获取JedisPool 参见RedisCache.unwrap源代码。

不使用spring boot
 Configuration
 EnableMethodCache(basePackages com.company.mypackage )
 EnableCreateCacheAnnotation
public class JetCacheConfig {
 Bean
 public Pool Jedis pool(){
 // build jedis pool ...
 Bean
 public SpringConfigProvider springConfigProvider() {
 return new SpringConfigProvider();
 Bean
 public GlobalCacheConfig config(SpringConfigProvider configProvider, Pool Jedis pool){
 Map localBuilders new HashMap();
 EmbeddedCacheBuilder localBuilder LinkedHashMapCacheBuilder
 .createLinkedHashMapCacheBuilder()
 .keyConvertor(FastjsonKeyConvertor.INSTANCE);
 localBuilders.put(CacheConsts.DEFAULT_AREA, localBuilder);
 Map remoteBuilders new HashMap();
 RedisCacheBuilder remoteCacheBuilder RedisCacheBuilder.createRedisCacheBuilder()
 .keyConvertor(FastjsonKeyConvertor.INSTANCE)
 .valueEncoder(JavaValueEncoder.INSTANCE)
 .valueDecoder(JavaValueDecoder.INSTANCE)
 .jedisPool(pool);
 remoteBuilders.put(CacheConsts.DEFAULT_AREA, remoteCacheBuilder);
 GlobalCacheConfig globalCacheConfig new GlobalCacheConfig();
 globalCacheConfig.setConfigProvider(configProvider);
 globalCacheConfig.setLocalCacheBuilders(localBuilders);
 globalCacheConfig.setRemoteCacheBuilders(remoteBuilders);
 globalCacheConfig.setStatIntervalMinutes(15);
 globalCacheConfig.setAreaInCacheName(false);
 return globalCacheConfig;
}

image.gif

Builder API

如果不通过 CreateCache和 Cached注解 可以通过下面的方式创建RedisCache。通过注解创建的缓存会自动设置keyPrefix 这里是手工创建缓存 对于远程缓存需要设置keyPrefix属性 以免不同Cache实例的key发生冲突。

GenericObjectPoolConfig pc new GenericObjectPoolConfig();
pc.setMinIdle(2);
pc.setMaxIdle(10);
pc.setMaxTotal(10);
JedisPool pool new JedisPool(pc, localhost , 6379);
Cache Long,OrderDO orderCache RedisCacheBuilder.createRedisCacheBuilder()
 .keyConvertor(FastjsonKeyConvertor.INSTANCE)
 .valueEncoder(JavaValueEncoder.INSTANCE)
 .valueDecoder(JavaValueDecoder.INSTANCE)
 .jedisPool(pool)
 .keyPrefix( orderCache )
 .expireAfterWrite(200, TimeUnit.SECONDS)
 .buildCache();

image.gif

常见问题

如果遇到这个错误

java.lang.NoSuchMethodError: redis.clients.jedis.JedisPool. init (Lorg/apache/commons/pool2/impl/GenericObjectPoolConfig;Ljava/lang/String;IILjava/lang/String;ILjava/lang/String;Z)V

请确保jedis的版本在2.9.0以上 spring boot 1.5以下版本的spring-boot-dependencies会引入较低版本的jedis 可以在自己的pom中强制直接依赖jedis版本2.9.0

 dependency 
 groupId redis.clients /groupId 
 artifactId jedis /artifactId 
 version 2.9.0 /version 
 /dependency 

image.gif

使用lettuce客户端连接redis

redis有多种java版本的客户端 JetCache2.2以前使用jedis客户端访问redis。从JetCache2.2版本开始 增加了对lettuce客户端的支持 JetCache的lettuce支持提供了异步操作和redis集群支持。

使用lettuce访问redis 对应的maven artifact是jetcache-redis-lettuce和jetcache-starter-redis-lettuce。lettuce使用Netty建立单个连接连redis 所以不需要配置连接池。

注意 新发布的lettuce5更换了groupId和包名 2.3版本的JetCache同时支持lettuce4和5 jetcache-redis-lettuce jetcache-starter-redis-lettuce提供lettuce5支持 jetcache-redis-lettuce4和jetcache-starter-redis-lettuce4提供lettuce4支持。

注意 JetCache2.2版本中 lettuce单词存在错误的拼写 错写为“luttece” 该错误存在于包名、类名和配置中 2.3已经改正。

spring boot环境下的lettuce支持

application.yml文件如下 这里省去了local相关的配置

jetcache: 
 areaInCacheName: false
 remote:
 default:
 type: redis.lettuce
 keyConvertor: fastjson
 uri: redis://127.0.0.1:6379/

image.gif

image.gif

如果使用sentinel做自动主备切换 uri可以配置为redis-sentinel://127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381/?sentinelMasterId mymaster

如果是集群

jetcache: 
 areaInCacheName: false
 remote:
 default:
 type: redis.lettuce
 keyConvertor: fastjson
 uri:
 - redis://127.0.0.1:7000
 - redis://127.0.0.1:7001
 - redis://127.0.0.1:7002

image.gif

image.gif

如果需要直接使用lettuce的RedisClient

 Bean(name defaultClient )
 DependsOn(RedisLettuceAutoConfiguration.AUTO_INIT_BEAN_NAME)
public LettuceFactory defaultClient() {
 return new LettuceFactory( remote.default , RedisClient.class);
}

image.gif

然后可以直接使用

 Autowired
private RedisClient defaultClient;

image.gif

也可以用Cache接口上的 T Tunwrap(Class T clazz)方法来获取RedisClient和RedisCommands等。参考RedisLettuceCache.unwrap源代码。

不使用spring boot
 Configuration
 EnableMethodCache(basePackages com.company.mypackage )
 EnableCreateCacheAnnotation
public class JetCacheConfig {
 Bean
 public RedisClient redisClient(){
 RedisClient client RedisClient.create( redis://127.0.0.1 
 return client;
 Bean
 public SpringConfigProvider springConfigProvider() {
 return new SpringConfigProvider();
 Bean
 public GlobalCacheConfig config(SpringConfigProvider configProvider,RedisClient redisClient){
 Map localBuilders new HashMap();
 EmbeddedCacheBuilder localBuilder LinkedHashMapCacheBuilder
 .createLinkedHashMapCacheBuilder()
 .keyConvertor(FastjsonKeyConvertor.INSTANCE);
 localBuilders.put(CacheConsts.DEFAULT_AREA, localBuilder);
 Map remoteBuilders new HashMap();
 RedisLettuceCacheBuilder remoteCacheBuilder RedisLettuceCacheBuilder.createRedisLettuceCacheBuilder()
 .keyConvertor(FastjsonKeyConvertor.INSTANCE)
 .valueEncoder(JavaValueEncoder.INSTANCE)
 .valueDecoder(JavaValueDecoder.INSTANCE)
 .redisClient(redisClient);
 remoteBuilders.put(CacheConsts.DEFAULT_AREA, remoteCacheBuilder);
 GlobalCacheConfig globalCacheConfig new GlobalCacheConfig();
 globalCacheConfig.setConfigProvider(configProvider);
 globalCacheConfig.setLocalCacheBuilders(localBuilders);
 globalCacheConfig.setRemoteCacheBuilders(remoteBuilders);
 globalCacheConfig.setStatIntervalMinutes(15);
 globalCacheConfig.setAreaInCacheName(false);
 return globalCacheConfig;
}

image.gif

builder API

如果不通过 CreateCache和 Cached注解 可以通过下面的方式创建Cache。通过注解创建的缓存会自动设置keyPrefix 这里是手工创建缓存 对于远程缓存需要设置keyPrefix属性 以免不同Cache实例的key发生冲突。

RedisClient client RedisClient.create( redis://127.0.0.1 
Cache Long,OrderDO orderCache RedisLettuceCacheBuilder.createRedisLettuceCacheBuilder()
 .keyConvertor(FastjsonKeyConvertor.INSTANCE)
 .valueEncoder(JavaValueEncoder.INSTANCE)
 .valueDecoder(JavaValueDecoder.INSTANCE)
 .redisClient(client)
 .keyPrefix( orderCache )
 .expireAfterWrite(200, TimeUnit.SECONDS)
 .buildCache();

image.gif

内存缓存LinkedHashMapCache和CaffeineCache

本地缓存当前有两个实现。如果自己用jetcache-core的Cache API 可以不指定keyConvertor 此时本地缓存使用equals方法来比较key。如果使用jetcache-anno中的 Cached、 CreateCache等注解 必须指定keyConvertor。

LinkedHashMapCache

LinkedHashMapCache是JetCache中实现的一个最简单的Cache 使用LinkedHashMap做LRU方式淘汰。

Cache Long, OrderDO cache LinkedHashMapCacheBuilder.createLinkedHashMapCacheBuilder()
 .limit(100)
 .expireAfterWrite(200, TimeUnit.SECONDS)
 .buildCache();

image.gif

CaffeineCache

caffeine cache的介绍看这里 它是guava cache的后续作品。

Cache Long, OrderDO cache CaffeineCacheBuilder.createCaffeineCacheBuilder()
 .limit(100)
 .expireAfterWrite(200, TimeUnit.SECONDS)
 .buildCache();

image.gif

统计

当yml中的jetcache.statIntervalMinutes大于0时 通过 CreateCache和 Cached配置出来的Cache自带监控。JetCache会按指定的时间定期通过logger输出统计信息。默认输出信息类似如下

2017-01-12 19:00:00,001 INFO support.StatInfoLogger - jetcache stat from 2017-01-12 18:59:00,000 to 2017-01-12 19:00:00,000
cache | qps| rate| get| hit| fail| expire|avgLoadTime|maxLoadTime
----------------------------------------------------- ---------- ------- -------------- -------------- -------------- -------------- ----------- -----------
default_AlicpAppChannelManager.getAlicpAppChannelById| 0.00| 0.00%| 0| 0| 0| 0| 0.0| 0
default_ChannelManager.getChannelByAccessToten | 30.02| 99.78%| 1,801| 1,797| 0| 4| 0.0| 0
default_ChannelManager.getChannelByAppChannelId | 8.30| 99.60%| 498| 496| 0| 1| 0.0| 0
default_ChannelManager.getChannelById | 6.65| 98.75%| 399| 394| 0| 4| 0.0| 0
default_ConfigManager.getChannelConfig | 1.97| 96.61%| 118| 114| 0| 4| 0.0| 0
default_ConfigManager.getGameConfig | 0.00| 0.00%| 0| 0| 0| 0| 0.0| 0
default_ConfigManager.getInstanceConfig | 43.98| 99.96%| 2,639| 2,638| 0| 0| 0.0| 0
default_ConfigManager.getInstanceConfigSettingMap | 2.45| 70.75%| 147| 104| 0| 43| 0.0| 0
default_GameManager.getGameById | 1.33|100.00%| 80| 80| 0| 0| 0.0| 0
default_GameManager.getGameUrlByUrlKey | 7.33|100.00%| 440| 440| 0| 0| 0.0| 0
default_InstanceManager.getInstanceById | 30.98| 99.52%| 1,859| 1,850| 0| 0| 0.0| 0
default_InstanceManager.getInstanceById_local | 30.98| 96.40%| 1,859| 1,792| 0| 67| 0.0| 0
default_InstanceManager.getInstanceById_remote | 1.12| 86.57%| 67| 58| 0| 6| 0.0| 0
default_IssueDao.getIssueById | 7.62| 81.40%| 457| 372| 0| 63| 0.0| 0
default_IssueDao.getRecentOnSaleIssues | 8.00| 85.21%| 480| 409| 0| 71| 0.0| 0
default_IssueDao.getRecentOpenAwardIssues | 2.52| 82.78%| 151| 125| 0| 26| 0.0| 0
default_PrizeManager.getPrizeMap | 0.82|100.00%| 49| 49| 0| 0| 0.0| 0
default_TopicManager.getOnSaleTopics | 0.97|100.00%| 58| 58| 0| 0| 0.0| 0
default_TopicManager.getOnSaleTopics_local | 0.97| 91.38%| 58| 53| 0| 5| 0.0| 0
default_TopicManager.getOnSaleTopics_remote | 0.08|100.00%| 5| 5| 0| 0| 0.0| 0
default_TopicManager.getTopicByTopicId | 2.90| 98.85%| 174| 172| 0| 0| 0.0| 0
default_TopicManager.getTopicByTopicId_local | 2.90| 96.55%| 174| 168| 0| 6| 0.0| 0
default_TopicManager.getTopicByTopicId_remote | 0.10| 66.67%| 6| 4| 0| 2| 0.0| 0
default_TopicManager.getTopicList | 0.02|100.00%| 1| 1| 0| 0| 0.0| 0
default_TopicManager.getTopicList_local | 0.02| 0.00%| 1| 0| 0| 1| 0.0| 0
default_TopicManager.getTopicList_remote | 0.02|100.00%| 1| 1| 0| 0| 0.0| 0
----------------------------------------------------- ---------- ------- -------------- -------------- -------------- -------------- ----------- -----------

image.gif

只有使用computeIfAbsent方法或者 Cached注解才会统计loadTime。用get方法取缓存 没有命中的话自己去数据库load 显然是无法统计到的。

如果需要定制输出 可以这样做:

 Bean
 public SpringConfigProvider springConfigProvider() {
 return new SpringConfigProvider(){
 public Consumer StatInfo statCallback() {
 // return new StatInfoLogger(false);
 ... // 实现自己的logger
 }

image.gif

JetCache按statIntervalMinutes指定的周期 定期调用statCallback返回着这个Consumer 传入的StatInfo是已经统计好的数据。这个方法默认的实现是

returnnew StatInfoLogger(false);

image.gif

StatInfoLogger的构造参数设置为true会有更详细的统计信息 包括put等操作的统计。StatInfoLogger输出的是给人读的信息 你也可以自定义logger将日志输出成特定格式 然后通过日志系统统一收集和统计。

如果想要让jetcache的日志输出到独立的文件中 在使用logback的情况下可以这样配置

 appender name JETCACHE_LOGFILE class ch.qos.logback.core.rolling.RollingFileAppender 
 file jetcache.log /file 
 rollingPolicy class ch.qos.logback.core.rolling.TimeBasedRollingPolicy 
 fileNamePattern jetcache.log.%d{yyyy-MM-dd} /fileNamePattern 
 maxHistory 30 /maxHistory 
 /rollingPolicy 
 encoder 
 pattern %-4relative [%thread] %-5level %logger{35} - %msg%n /pattern 
 /encoder 
 /appender 
 logger name com.alicp.jetcache level INFO additivity false 
 appender-ref ref JETCACHE_LOGFILE / 
 /logger 

image.gif

Builder:未使用Spring4(或者spring)的时候 通过Builder手工构造Cache

JetCache2版本的 Cached和 CreateCache等注解都是基于Spring4.X版本实现的 在没有Spring支持的情况下 注解将不能使用。但是可以直接使用JetCache的API来创建、管理、监控Cache 多级缓存也可以使用。

创建缓存

创建缓存的操作类似guava/caffeinecache 例如下面的代码创建基于内存的LinkedHashMapCache

Cache String, Integer cache LinkedHashMapCacheBuilder.createLinkedHashMapCacheBuilder()
 .limit(100)
 .expireAfterWrite(200, TimeUnit.SECONDS)
 .buildCache();

image.gif

创建RedisCache

GenericObjectPoolConfig pc new GenericObjectPoolConfig();
 pc.setMinIdle(2);
 pc.setMaxIdle(10);
 pc.setMaxTotal(10);
 JedisPool pool new JedisPool(pc, localhost , 6379);
Cache Long, OrderDO orderCache RedisCacheBuilder.createRedisCacheBuilder()
 .keyConvertor(FastjsonKeyConvertor.INSTANCE)
 .valueEncoder(JavaValueEncoder.INSTANCE)
 .valueDecoder(JavaValueDecoder.INSTANCE)
 .jedisPool(pool)
 .keyPrefix( orderCache )
 .expireAfterWrite(200, TimeUnit.SECONDS)
 .buildCache();

image.gif

多级缓存

在2.2以后通过下面的方式创建多级缓存

Cache multiLevelCache MultiLevelCacheBuilder.createMultiLevelCacheBuilder()
 .addCache(memoryCache, redisCache)
 .expireAfterWrite(100, TimeUnit.SECONDS)
 .buildCache();

image.gif

实际上 使用MultiLevelCache可以创建多级缓存 它的构造函数接收的是一个Cache数组 可变参数 。

如果是2.2之前的版本

Cache memoryCache ...
Cache redisCache ...
Cache multiLevelCache new MultiLevelCache(memoryCache, redisCache);

image.gif

监控统计

如果要对Cache进行监控统计

Cache orderCache ...
CacheMonitor orderCacheMonitor new DefaultCacheMonitor( OrderCache 
orderCache.config().getMonitors().add(orderCacheMonitor); // jetcache 2.2 , or call builder.addMonitor() before buildCache()
// Cache Long, Order monitedOrderCache new MonitoredCache(orderCache, orderCacheMonitor); //before jetcache 2.2
int resetTime 
boolean verboseLog false;
DefaultCacheMonitorManager cacheMonitorManager new DefaultCacheMonitorManager(resetTime, TimeUnit.SECONDS, verboseLog);
cacheMonitorManager.add(orderCacheMonitor);
cacheMonitorManager.start();

image.gif

首先创建一个CacheMonitor 每个DefaultCacheMonitor只能用于一个Cache。当DefaultCacheMonitorManager启动以后 会使用slf4j按指定的时间定期输出统计信息到日志中 简版输出格式参见统计 DefaultCacheMonitor构造时指定的名字会作为输出时cache的名字。

在组装多级缓存的过程中 可以给每个缓存安装一个Monitor 这样可以监控每一级的命中情况。

也可以自己对统计信息进行处理 调用下面的构造方法创建DefaultCacheMonitorManager

public DefaultCacheMonitorManager(int resetTime, TimeUnit resetTimeUnit, Consumer StatInfo stat

image.gif

开发者文档

clone下来以后 可以按maven项目导入idea或eclipse。

跑通单元测试 需要在本地运行redis 先安装docker 然后用下面的命令运行redis-sentinel

docker run --rm -it -p 6379-6381:6379-6381 -p 26379-26381:26379-26381 areyouok/redis-sentinel

接下来mvn cleantest可以跑通所有测试 如果在IDE里面 可能还需要给javac设置-parameters参数。需要注意的是机器繁忙时单元测试有可能会失败 因为很多单元测试使用了sleep 为了不让单元测试运行的时间过长 sleep的时间都设置的比较短 这样机器卡顿时可能导致检查失败 不过对于正常机器这并不经常发生。

使用snapshot版本 在自己的pom里面加上

 repositories 
 repository 
 id sonatype-nexus-snapshots /id 
 name Sonatype Nexus Snapshots /name 
 url https://oss.sonatype.org/content/repositories/snapshots /url 
 releases 
 enabled false /enabled 
 /releases 
 snapshots 
 enabled true /enabled 
 /snapshots 
 /repository 
 /repositories 

image.gif

升级和兼容性指南2.5.0从2.3.3及更低版本升级到2.5.0会发生ClassCastException 如果你使用了MultiLevelCache或者cacheType.CacheType.BOTH 。 解决办法是先升级到2.4.4并且发布到生产环境 然后再升级到2.5.0。子类的注解会覆盖接口和父类。

本文转自网络,原文链接:https://developer.aliyun.com/article/787308
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!
上一篇:Java反射与内省 下一篇:没有了

推荐图文


随机推荐