大家好,我是小义,这段时间有点事耽搁了,好久没写文章了,今天介绍一下如何构建一个基于springboot实现的自定义starter组件,即一个可灵活配置过期时间的多级缓存框架。
组件化是SpringBoot一大优点。 starter组件是Spring Boot生态系统的一部分,它们帮助开发者快速搭建项目,减少配置的复杂性,并且确保依赖管理的一致性。开发者可以根据自己的需求选择合适的starter来集成到项目中。
尽管各种starters在实现细节上各具特色,但它们普遍遵循两个核心原则:配置属性(ConfigurationProperties)和自动配置(AutoConfiguration)。这一设计哲学源自Spring Boot所倡导的“约定优于配置”(Convention Over Configuration)的理念。
一个简单的starter项目应包含以下目录。
代码语言:java复制demo-spring-boot-starter
|-src
|-main
|-java
|-xxx.xxx
|-DemoConfig.java
|-resource
|-META-INF
|-spring.factories
|-application.properties
|-pom.xml
自定义starter的步骤如下:
- 定义坐标:创建一个新的Maven项目,并定义其坐标(groupId, artifactId, version)。
- 编写自动配置类:创建一个带有@Configuration注解的类,实现自定义配置。
- 创建META-INF/spring.factories文件:指定自动配置类,让Spring Boot识别。
- 打包和发布:将starter下载到本地或发布到Maven仓库。
下面来一一实现。
首先,在spring.factories中,我们指定一下要自动装配的配置类,这样就可以将设置的包扫描路径下的相关bean部署到SpringBoot 中。
代码语言:java复制org.springframework.boot.autoconfigure.EnableAutoConfiguration=
com.xiaoyi.multiTtlCache.config.CustomizedRedisAutoConfiguration
在pom.xml中,需要引入autoconfigure自动装配的maven依赖包。
代码语言:java复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!--spring-boot-configuration-processor的作用是生成配置的元数据信息,即META-INF目录下的spring-configuration-metadata.json文件,从而告诉spring这个jar包中有哪些自定义的配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>${spring-boot.version}</version>
</dependency>
先设置缓存配置类,类名和spring.factories中的对应上。这其中涉及到本地缓存caffeine和redis缓存配置,关于caffeine的相关内容可以看之前的文章。
代码语言:java复制@Configuration
@Import(SpringUtil.class)
@ComponentScan(basePackages = "com.xiaoyi.multiTtlCache")
@EnableCaching
public class CustomizedRedisAutoConfiguration {
public static final String REDISTEMPLATE_BEAN_NAME = "cacheRedisTemplate";
@Bean(REDISTEMPLATE_BEAN_NAME)
public RedisTemplate<String, Object> cacheRedisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
@Bean
public MyCaffeineCache myCaffeineCache() {
MyCaffeineCache myCaffeineCache = new MyCaffeineCache();
myCaffeineCache.init();
return myCaffeineCache;
}
}
创建一个自定义注解类,添加ttl等时间设置属性。
代码语言:java复制@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CustomizedCacheable {
String[] value() default {};
String[] cacheNames() default {};
String key() default "";
String keyGenerator() default "";
String cacheResolver() default "";
String condition() default "";
String unless() default "";
boolean sync() default false;
//String cacheManager() default "redisCacheManager";
/**
* 过期时间
* @return
*/
long expiredTimeSecond() default 0;
/**
* 预刷新时间
* @return
*/
long preLoadTimeSecond() default 0;
/**
* 缓存级别,1-本地缓存,2-redis缓存,3-本地 redis
* @return
*/
String cacheType() default "1";
/**
* 一二级缓存之间的缓存过期时间差
* @return
*/
long expiredInterval() default 0;
/**
* 是否开启缓存
* @return
*/
String cacheEnabled() default "1";
long test() default 1;
}
增加自定义注解的拦截器,根据设置的缓存等级决定走本地缓存还是redis缓存,同时比较缓存的剩余过期时间是否小于阈值(preLoadTimeSecond),小于则重新刷新缓存,达到缓存预热的效果,同时减少缓存击穿的问题。
核心代码如下:
代码语言:java复制@Component
@Aspect
@Slf4j
@Order(1)
public class CacheReloadAspect {
@Autowired
private Environment environment;
@Autowired
private ApplicationContext applicationContext;
private ReentrantLock lock = new ReentrantLock();
@SneakyThrows
@Around(value = "@annotation(com.xiaoyi.multiTtlCache.annotation.CustomizedCacheable)")
public Object around(ProceedingJoinPoint proceedingJoinPoint){
//方法入参对象数组
Object[] args = proceedingJoinPoint.getArgs();
//方法实体
Method method = MethodSignature.class.cast(proceedingJoinPoint.getSignature()).getMethod();
//自定义注解
CustomizedCacheable cacheable = method.getAnnotation(CustomizedCacheable.class);
String cacheEnabled = cacheable.cacheEnabled();
//根据配置判断是否开启缓存
String property = environment.getProperty(cacheEnabled);
if (!ObjectUtil.isEmpty(property)) {
return proceedingJoinPoint.proceed();
}
//解析上下文
StandardEvaluationContext standardEvaluationContext = new StandardEvaluationContext();
//参数名称
String[] parameterNames = new LocalVariableTableParameterNameDiscoverer().getParameterNames(method);
for (int i = 0; i < parameterNames.length; i ) {
standardEvaluationContext.setVariable(parameterNames[i], args[i]);
}
//解析SPEL表达式的key,获取真正存入缓存中的key值
String key = parseSPELKey(cacheable, standardEvaluationContext);
Object result = null;
String cacheType = cacheable.cacheType();
switch (cacheType) {
case CacheConstant.LOCAL_CACHE:
result = useCaffeineCache(key, cacheable, proceedingJoinPoint);
case CacheConstant.REDIS_CACHE:
result = useRedisCache(key, cacheable, proceedingJoinPoint);
case CacheConstant.BOTH_CACHE:
result = useBothCache(key, cacheable, proceedingJoinPoint);
default:
result = null;
}
return result;
}
@SneakyThrows
private Object useBothCache(String key, CustomizedCacheable cacheable, ProceedingJoinPoint proceedingJoinPoint) {
long expiredInterval = cacheable.expiredInterval();
MyCaffeineCache myCaffeineCache = (MyCaffeineCache) SpringUtil.getBean(MyCaffeineCache.class);
RedisTemplate redisTemplate = (RedisTemplate) SpringUtil.getBean("cacheRedisTemplate");
Object o = myCaffeineCache.get(key);
if (o != null) {
Long ttl = myCaffeineCache.getTtl(key);
if(ObjectUtil.isNotEmpty(ttl) && ttl <= cacheable.preLoadTimeSecond()){
log.info(">>>>>>>>>>> cacheKey:{}, ttl: {},preLoadTimeSecond: {}",key,ttl,cacheable.preLoadTimeSecond());
ThreadUtil.execute(()->{
lock.lock();
try{
CachedInvocation cachedInvocation = buildCachedInvocation(proceedingJoinPoint, cacheable);
Object o1 = CacheHelper.exeInvocation(cachedInvocation);
myCaffeineCache.set(key, o1, cacheable.expiredTimeSecond());
redisTemplate.opsForValue().set(key, o1, cacheable.expiredTimeSecond() expiredInterval, TimeUnit.SECONDS);
}catch (Exception e){
log.error("{}",e.getMessage(),e);
}finally {
lock.unlock();
}
});
}
return o;
} else {
Object o1 = redisTemplate.opsForValue().get(key);
Long ttl = redisTemplate.getExpire(key, TimeUnit.SECONDS);
if(o1 != null){
myCaffeineCache.set(key, o1, ttl);
return o1;
}
}
Object result = proceedingJoinPoint.proceed();
myCaffeineCache.set(key, result, cacheable.expiredTimeSecond());
redisTemplate.opsForValue().set(key, result, cacheable.expiredTimeSecond() expiredInterval, TimeUnit.SECONDS);
return result;
}
@SneakyThrows
private Object useRedisCache(String key, CustomizedCacheable cacheable, ProceedingJoinPoint proceedingJoinPoint) {
RedisTemplate redisTemplate = (RedisTemplate) SpringUtil.getBean("cacheRedisTemplate");
Object o = redisTemplate.opsForValue().get(key);
if (o != null) {
Long ttl = redisTemplate.getExpire(key, TimeUnit.SECONDS);
if(ObjectUtil.isNotEmpty(ttl) && ttl <= cacheable.preLoadTimeSecond()){
log.info(">>>>>>>>>>> cacheKey:{}, ttl: {},preLoadTimeSecond: {}", key, ttl, cacheable.preLoadTimeSecond());
ThreadUtil.execute(()->{
lock.lock();
try{
CachedInvocation cachedInvocation = buildCachedInvocation(proceedingJoinPoint, cacheable);
Object o1 = CacheHelper.exeInvocation(cachedInvocation);
redisTemplate.opsForValue().set(key, o1, cacheable.expiredTimeSecond(), TimeUnit.SECONDS);
}catch (Exception e){
log.error("{}",e.getMessage(),e);
}finally {
lock.unlock();
}
});
}
return o;
}
Object result = proceedingJoinPoint.proceed();
redisTemplate.opsForValue().set(key, result, cacheable.expiredTimeSecond(), TimeUnit.SECONDS);
return result;
}
@SneakyThrows
private Object useCaffeineCache(String key, CustomizedCacheable cacheable, ProceedingJoinPoint proceedingJoinPoint) {
MyCaffeineCache myCaffeineCache = (MyCaffeineCache) SpringUtil.getBean(MyCaffeineCache.class);
Object o = myCaffeineCache.get(key);
if (o != null) {
Long ttl = myCaffeineCache.getTtl(key);
if(ObjectUtil.isNotEmpty(ttl) && ttl <= cacheable.preLoadTimeSecond()){
log.info(">>>>>>>>>>> cacheKey:{}, ttl: {},preLoadTimeSecond: {}",key,ttl,cacheable.preLoadTimeSecond());
ThreadUtil.execute(()->{
lock.lock();
try{
CachedInvocation cachedInvocation = buildCachedInvocation(proceedingJoinPoint, cacheable);
Object o1 = CacheHelper.exeInvocation(cachedInvocation);
myCaffeineCache.set(key, o1, cacheable.expiredTimeSecond());
}catch (Exception e){
log.error("{}",e.getMessage(),e);
}finally {
lock.unlock();
}
});
}
return o;
}
Object result = proceedingJoinPoint.proceed();
myCaffeineCache.set(key, result, cacheable.expiredTimeSecond());
return result;
}
private CachedInvocation buildCachedInvocation(ProceedingJoinPoint proceedingJoinPoint,CustomizedCacheable customizedCacheable){
Method method = this.getSpecificmethod(proceedingJoinPoint);
String[] cacheNames = customizedCacheable.cacheNames();
Object targetBean = proceedingJoinPoint.getTarget();
Object[] arguments = proceedingJoinPoint.getArgs();
Object key = customizedCacheable.key();
CachedInvocation cachedInvocation = CachedInvocation.builder()
.arguments(arguments)
.targetBean(targetBean)
.targetMethod(method)
.cacheNames(cacheNames)
.key(key)
.expiredTimeSecond(customizedCacheable.expiredTimeSecond())
.preLoadTimeSecond(customizedCacheable.preLoadTimeSecond())
.build();
return cachedInvocation;
}
private Method getSpecificmethod(ProceedingJoinPoint pjp) {
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Method method = methodSignature.getMethod();
// The method may be on an interface, but we need attributes from the
// target class. If the target class is null, the method will be
// unchanged.
Class<?> targetClass = AopProxyUtils.ultimateTargetClass(pjp.getTarget());
if (targetClass == null && pjp.getTarget() != null) {
targetClass = pjp.getTarget().getClass();
}
Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
// If we are dealing with method with generic parameters, find the
// original method.
specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
return specificMethod;
}
private String parseSPELKey(CustomizedCacheable cacheable, StandardEvaluationContext context) {
String keySpel = cacheable.key();
Expression expression = new SpelExpressionParser().parseExpression(keySpel);
String key = expression.getValue(context, String.class);
return key;
}
}
拦截器利用了AOP思想,达到对业务代码的无侵入性。通过mvn install下载到本地maven仓库或mvn deploy部署到远程仓库。这样其他项目在使用该组件时,只需要在pom中引入该依赖包,然后在方法上加上自定义注解即可。
代码语言:java复制@PostMapping("/2")
@CustomizedCacheable(value = "22", key = "11", cacheType = "2", expiredTimeSecond = 100, preLoadTimeSecond = 40)
public String test2() {
//...
}
完整项目地址:https://github.com/xiaoyir/multiTtlCache