基于spring的缓存注解实现

本文介绍了一种基于Spring AOP的缓存管理方案,通过自定义注解和切面技术实现方法级别的缓存读取与清除功能。

时间原因,这里只贴代码,见谅。

package com.rd.ifaes.common.annotation;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.asm.*;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 切面编程工具类
 * @author lh
 * @version 3.0
 * @since 2016-8-26
 */
public class AopUtils {

	
	private static final Logger LOGGER = LoggerFactory.getLogger(AopUtils.class);
	private static final String DESC_DOUBLE = "D";
	private static final String DESC_SHORT = "J";
	
	private AopUtils() {	}
	
	/**
	 * <p>获取方法的参数名</p>
	 *
	 * @param m
	 * @return
	 */
	public static String[] getMethodParamNames(final Method m) {
		final String[] paramNames = new String[m.getParameterTypes().length];
		final String n = m.getDeclaringClass().getName();
		final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
		String className = m.getDeclaringClass().getSimpleName();
		ClassReader cr = null;
		InputStream resourceAsStream = null;
		try {
			resourceAsStream = Class.forName(n).getResourceAsStream(className + ".class");
			cr = new ClassReader(resourceAsStream);
		} catch (IOException | ClassNotFoundException e) {
			LOGGER.warn(e.getMessage(), e);
		} finally {
			if (resourceAsStream != null) {
				try {
					resourceAsStream.close();
				} catch (IOException e) {
					LOGGER.warn(e.getMessage(), e);
				}
			}
		}

		if (cr != null) {
			cr.accept(new ClassVisitor(Opcodes.ASM4, cw) {
				@Override
				public MethodVisitor visitMethod(final int access,
						final String name, final String desc,
						final String signature, final String[] exceptions) {
					final Type[] args = Type.getArgumentTypes(desc);
					// 方法名相同并且参数个数相同
					if (!name.equals(m.getName())
							|| !sameType(args, m.getParameterTypes())) {
						return super.visitMethod(access, name, desc, signature,
								exceptions);
					}
					MethodVisitor v = cv.visitMethod(access, name, desc, signature,
							exceptions);
					return new MethodVisitor(Opcodes.ASM4, v) {
						
						int fixCount = 0;//步长修正计数器
						
						@Override
						public void visitLocalVariable(String name, String desc,
								String signature, Label start, Label end, int index) {
							int i = index - 1;
							// 如果是静态方法,则第一就是参数
							// 如果不是静态方法,则第一个是"this",然后才是方法的参数
							if (Modifier.isStatic(m.getModifiers())) {
								i = index;
							}
							if (i > fixCount) {
								i -= fixCount;
							}
							if(desc.equals(DESC_SHORT) || desc.equals(DESC_DOUBLE)){
								fixCount++;
							}
							if (i >= 0 && i < paramNames.length) {
								paramNames[i] = name;
							}
							super.visitLocalVariable(name, desc, signature, start,
									end, index);
						}
						
					};
				}
			}, 0);			
		}
		return paramNames;
	}

	/**
	 * <p>比较参数类型是否一致</p>
	 *
	 * @param types   asm的类型({@link Type})
	 * @param clazzes java 类型({@link Class})
	 * @return
	 */
	private static boolean sameType(Type[] types, Class<?>[] clazzes) {
		// 个数不同
		if (types.length != clazzes.length) {
			return false;
		}

		for (int i = 0; i < types.length; i++) {
			if (!Type.getType(clazzes[i]).equals(types[i])) {
				return false;
			}
		}
		return true;
	}
	
	/**
	 * 取得切面调用的方法
	 * @param pjp
	 * @return
	 */
	public static Method getMethod(ProceedingJoinPoint pjp){
		Signature sig = pjp.getSignature();
        if (!(sig instanceof MethodSignature)) {
            throw new IllegalArgumentException("该注解只能用于方法");
        }
        MethodSignature msig = (MethodSignature) sig;
        Object target = pjp.getTarget();
        Method currentMethod = null;
		try {
			currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
		} catch (NoSuchMethodException | SecurityException e) {
			LOGGER.warn(e.getMessage(), e);
		}       
        return currentMethod;
	}
	
	public static List<String> getMatcher(String regex, String source) {  
        List<String> list = new ArrayList<>();
        Pattern pattern = Pattern.compile(regex);  
        Matcher matcher = pattern.matcher(source);  
        while (matcher.find()) {  
            list.add(matcher.group());
        }  
        return list;  
    }
	
	/**
	 * 取得注解参数
		(?=exp)	匹配exp前面的位置
		(?<=exp)	匹配exp后面的位置
		(?!exp)	匹配后面跟的不是exp的位置
		(?<!exp)	匹配前面不是exp的位置
	 * @param managers
	 * @return
	 */
	public static List<String> getAnnoParams(String source){
		String regex = "(?<=\\{)(.+?)(?=\\})";
        return getMatcher(regex, source);
    }
	

}

 

 

package com.rd.ifaes.common.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import com.rd.ifaes.common.dict.ExpireTime;
/**
 * 添加缓存
 * @author lh
 *
 */
@Retention(RetentionPolicy.RUNTIME)  
@Target({ElementType.METHOD}) 
public @interface Cacheable {

	/**
	 * 缓存key
	 * @return
	 */
	public String key() default ""; 
	
	/**
	 * 缓存时效,默认无限期
	 * @return
	 */
	public ExpireTime expire() default ExpireTime.NONE; 

}

 

package com.rd.ifaes.common.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import com.rd.ifaes.common.dict.ExpireTime;
/**
 * 缓存清除
 * @author lh
 * @version 3.0
 * @since 2016-8-28
 *
 */
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CacheEvict {

	/**
	 * 缓存key
	 * @return
	 */
	String key() default "";
	
	/**
	 * 缓存key数组
	 * @return
	 */
	String[] keys() default{};
	/**
	 * 操作之间的缓存时间(秒)
	 * @author  FangJun
	 * @date 2016年9月9日
	 * @return 默认0,不做限制
	 */
	ExpireTime interval() default ExpireTime.NONE;
	
}

 

package com.rd.ifaes.common.annotation;

import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import com.rd.ifaes.common.dict.ExpireTime;
import com.rd.ifaes.common.util.ReflectionUtils;
import com.rd.ifaes.common.util.StringUtils;
import com.rd.ifaes.core.core.constant.CacheConstant;
import com.rd.ifaes.core.core.constant.Constant;
import com.rd.ifaes.core.core.util.CacheUtils;
/**
 * 缓存操作切面
 * @author lh
 *
 */
@Aspect
@Component
public class CacheAspect {
	
	private static final Logger LOGGER = LoggerFactory.getLogger(CacheAspect.class);
	
	@SuppressWarnings("rawtypes")
	@Autowired
	private RedisTemplate redisTemplate;
	
	/**
	 * 添加缓存
	 * @param pjp
	 * @param cache
	 * @return
	 * @throws Throwable
	 */
	@Around("@annotation(cache)")
	public Object cacheable(final ProceedingJoinPoint pjp, Cacheable cache)throws Throwable {

		String key = getCacheKey(pjp, cache.key());

		//使用redisTemplate操作缓存
		@SuppressWarnings("unchecked")
		ValueOperations<String, Object> valueOper = redisTemplate.opsForValue();
		Object value =  valueOper.get(key); // 从缓存获取数据
		if (value != null) {
			return value; // 如果有数据,则直接返回
		}
		
		value = pjp.proceed();
		if(LOGGER.isInfoEnabled()){
			LOGGER.info("cachePut, key={}", key);			
		}
		// 缓存,到后端查询数据
		if (cache.expire().getTime() <= 0) { // 如果没有设置过期时间,则无限期缓存
			valueOper.set(key, value);
		} else { // 否则设置缓存时间
			valueOper.set(key, value, cache.expire().getTime(), TimeUnit.SECONDS);
		}
		return value;
	}
	
	
	@Around("@annotation(evict)")
	public Object cacheEvict(final ProceedingJoinPoint pjp, CacheEvict evict)throws Throwable {
		Object value;
		// 执行方法
		value = pjp.proceed();
		
		//单个key操作
		if (StringUtils.isNotBlank(evict.key())) {
			String keyname = evict.key();
			evictByKeyname(pjp, keyname,evict.interval());
		}
		//批量key操作
		if (evict.keys() != null && evict.keys().length > 0) {
			for (String keyname : evict.keys()) {
				evictByKeyname(pjp, keyname,evict.interval());
			}
		}
		return value;
	}

	@SuppressWarnings("unchecked")
	private void evictByKeyname(final ProceedingJoinPoint pjp, final String keyname, ExpireTime interval) {
		final String key = getCacheKey(pjp, keyname);
		//操作间隔判断
		if (!ExpireTime.NONE.equals(interval)) {
			final String intervalKey = CacheConstant.KEY_PREFIX_CACHE_EVICT + key;
			if (CacheUtils.incr(intervalKey, Constant.DOUBLE_ONE) > Constant.DOUBLE_ONE) {
				return;
			}
			CacheUtils.expire(intervalKey, interval);
		}
		
		if(LOGGER.isInfoEnabled()){
			LOGGER.info("cacheEvict, key={}", key);			
		}
		
		//使用redisTemplate操作缓存
		if (keyname.equals(key)) {// 支持批量删除
			Set<String> keys = redisTemplate.keys(key.concat("*"));
			if (!CollectionUtils.isEmpty(keys)) {
				redisTemplate.delete(keys);
			}
		} else {
			redisTemplate.delete(key);
		}
	}

	/**
	 * 获取缓存的key值
	 * 
	 * @param pjp
	 * @param key
	 * @return
	 */
	private String getCacheKey(final ProceedingJoinPoint pjp, final String key) {

		StringBuilder buf = new StringBuilder();
		final Object[] args = pjp.getArgs();
		
		if(StringUtils.isNotBlank(key)){
			buf.append(key);
			List<String> annoParamNames = AopUtils.getAnnoParams(key);
			String[] methodParamNames = AopUtils.getMethodParamNames(AopUtils.getMethod(pjp));
			if(!CollectionUtils.isEmpty(annoParamNames)){
				for (String ap : annoParamNames) {
					buf = replaceParam(buf, args, methodParamNames, ap);
				}				
			}
			
		}else{
			buf.append(pjp.getSignature().getDeclaringTypeName()).append(":").append(pjp.getSignature().getName());
			for (Object arg : args) {
				buf.append(":").append(arg.toString());
			}
		}	

		return buf.toString();
	}

	/**
	 * 替换占位参数
	 * @param buf
	 * @param args
	 * @param methodParamNames
	 * @param ap
	 * @return
	 */
	private StringBuilder replaceParam(StringBuilder buf, final Object[] args, String[] methodParamNames, String ap) {
		StringBuilder builder = new StringBuilder(buf);
		String paramValue = "";
		for (int i = 0; i < methodParamNames.length; i++) {
			if(ap.startsWith(methodParamNames[i])){
				final Object arg = args[i];
				if (ap.contains(".")) {
					paramValue = String.valueOf(ReflectionUtils.invokeGetter(arg, ap.substring(ap.indexOf('.') + 1)));
				} else {
					paramValue = String.valueOf(arg);
				}
				break;
			}
		}
		int start = builder.indexOf("{" + ap);
		int end = start + ap.length() + 2;
		builder =builder.replace(start, end, paramValue);
		return builder;
	}
}

   spring相关配置如下:

 	<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
		<property name="maxIdle" value="${redis.pool.maxIdle}" /> <!-- 最大能够保持idel状态的对象数  -->
		<property name="maxTotal" value="${redis.pool.maxTotal}" /> <!-- 最大分配的对象数 -->
		<property name="testOnBorrow" value="${redis.pool.testOnBorrow}" /> <!-- 当调用borrow Object方法时,是否进行有效性检查 -->
	</bean>
	 
 	<!-- sprin_data_redis 单机配置 -->
 	<bean id="jedisConnFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" >
         <property name="hostName" value="${redis.host}" />
         <property name="port" value="${redis.port}" />
         <property name="timeout" value="${redis.timeout}" />
         <property name="password" value="${redis.password}" />
         <property name="poolConfig" ref="jedisPoolConfig" />
 	</bean>
 	<!-- key序列化 -->
	<bean id="stringRedisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer" />
	 
	<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"
	 p:connectionFactory-ref="jedisConnFactory" p:keySerializer-ref="stringRedisSerializer" />
 	
	<!-- spring自己的缓存管理器 -->  
    <bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">  
        <property name="caches">  
            <set>  
            	<bean class="com.rd.ifaes.common.jedis.RdRedisCache" p:redis-template-ref="redisTemplate" p:name="sysCache"/> 
            </set>  
        </property>  
    </bean>  
	
	<!-- 启用缓存注解功能,这个是必须的,否则注解不会生效,另外,该注解一定要声明在spring主配置文件中才会生效 -->
	<cache:annotation-driven cache-manager="cacheManager" proxy-target-class="true" key-generator="rdKeyGenerator"/>
	<!-- 自定义主键生成策略 -->
	<bean id="rdKeyGenerator" class="com.rd.ifaes.common.jedis.RdKeyGenerator"/>

 

<think> 首先,用户提到了站内引用 "[^1]:SpringBoot缓存注解的使用。在springboot中使用缓存注解很简单,首先就只需要在依赖中引入spring-boot-starter-cache依赖,然后在application类中使用@EnableCaching注解开启缓存,然后在方法上根据需要使用具体的缓存注解即可。 [^1]。" 这似乎是缓存注解的基本使用方法。 用户的问题是:"我想查找Spring缓存注解的使用方法 请问Spring缓存注解的使用方法"。这是一个直接的查询,用户想了解Spring缓存注解的使用方法。 系统级指令要求: - 所有行内数学表达式使用$...$格式(但这里可能不涉及数学表达式,因为主题是Spring缓存注解)。 - 独立公式使用$$...$$并单独成段(同样,可能不适用)。 - LaTeX语法正确(但主题是编程,不是数学)。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识,例如 "[^1]"。 用户提到了参考站内引用,所以我必须引用提供的引用 "[^1]"。引用中说:"在springboot中使用缓存注解很简单,首先就只需要在依赖中引入spring-boot-starter-cache依赖,然后在application类中使用@EnableCaching注解开启缓存,然后在方法上根据需要使用具体的缓存注解即可。" 所以,我应该基于这个构建回答。 此外,系统指令说:"上述引用仅供参考,并不是用户真正的需求,请尽可能多的参考用户上一次的问题和你的回答。" 用户上一次的问题是关于Spring缓存注解的使用方法,所以我需要聚焦于此。 结构化回答: - 回答应该结构清晰,帮助用户逐步解决问题。 - 确保回答真实可靠。基于我对Spring缓存注解的了解: - 核心注解包括: - `@EnableCaching`:用于启动类或配置类来启用缓存。 - `@Cacheable`:方法调用结果被缓存。 - `@CacheEvict`:清除缓存。 - `@CachePut`:更新缓存。 - `@Caching`:组合多个缓存操作。 - 需要添加依赖:spring-boot-starter-cache。 - 配置缓存管理器。 - 示例代码。 用户指定了“Spring缓存注解”,但引用中提到的是“SpringBoot”,我应该保持一致,因为Spring Boot是Spring Framework的一部分。 回答结构: 1. **引言**:简要介绍Spring缓存注解。 2. **步骤**:逐步说明如何使用。 - 添加依赖。 - 启用缓存。 - 使用具体注解。 3. **示例**:提供代码示例。 4. **引用**:末尾添加引用标识。 5. **生成相关问题**:最后一部分。 生成相关问题: - 基于主题,如性能优化、常见问题等。 - 例如:如何配置不同的缓存提供者?缓存注解的常见问题?等。 使用中文回答。 大纲: - 解释Spring缓存注解的作用。 - 详细步骤: - 1. 添加依赖(如引用中提到)。 - 2. 在Spring Boot应用类中添加`@EnableCaching`。 - 3. 在方法上使用缓存注解如`@Cacheable`, `@CacheEvict`。 - 提供简单示例代码。 - 结束时添加引用。 - 然后生成相关问题。 引用标识:在回答中提到引用内容时添加 "[^1]",但引用是用户提供的,所以我需要自然地融入。 用户说:"参考站内引用:引用[1]:..." 所以我应该在回答中引用 "[^1]"。 最后,生成相关问题部分:"§§相关问题§§" 后列出几个问题。 完整回答: - 先回答用户的查询。 - 然后添加相关问题。 </think> ### Spring缓存注解的使用方法 Spring框架提供了强大的缓存抽象,允许开发者轻松地在应用中集成缓存功能,以提高性能。缓存注解基于Spring的AOP(面向切面编程)实现,主要适用于Spring Boot项目。以下是详细的使用方法,逐步分解: #### 1. **添加依赖** 首先,在Spring Boot项目中,需要引入Spring缓存模块依赖。这通常在项目的`pom.xml`(Maven项目)或`build.gradle`(Gradle项目)中完成。核心依赖是`spring-boot-starter-cache`: - **Maven示例**: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> ``` - **Gradle示例**: ```groovy implementation 'org.springframework.boot:spring-boot-starter-cache' ``` 添加此依赖后,Spring Boot会自动配置缓存的基础设施[^1]。 #### 2. **启用缓存** 在Spring Boot应用的主类(或配置类)上添加`@EnableCaching`注解,以激活缓存功能。这个注解告诉Spring扫描项目中所有缓存相关的注解: ```java import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCaching; @SpringBootApplication @EnableCaching // 启用Spring缓存 public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } } ``` 启用后,Spring会基于默认配置(如使用ConcurrentMap缓存)工作,但你可以自定义缓存提供者(如Redis、Ehcache)[^1]。 #### 3. **使用具体缓存注解** 在方法级别应用缓存注解Spring会在方法执行时自动处理缓存逻辑。以下是核心注解及其用法: - **`@Cacheable`**:标记方法的结果应该被缓存。如果第一次调用结果已缓存,后续调用直接返回缓存值,跳过方法执行。 - **参数**: - `value` 或 `cacheNames`:指定缓存名称(如 `"userCache"`)。 - `key`:可选,定义缓存键(支持SpEL表达式)。 - **示例**:缓存用户查询结果。 ```java import org.springframework.cache.annotation.Cacheable; @Service public class UserService { @Cacheable(value = "users", key = "#id") // 缓存键为id public User getUserById(Long id) { // 模拟数据库查询 return userRepository.findById(id).orElse(null); } } ``` - **`@CacheEvict`**:清除缓存条目,常用于更新或删除操作。 - **参数**: - `value`:缓存名称。 - `key`:指定要清除的键。 - `allEntries`:可选,是否清除整个缓存(默认为false)。 - **示例**:删除用户时清除缓存。 ```java @CacheEvict(value = "users", key = "#id") // 清除指定id的缓存 public void deleteUser(Long id) { userRepository.deleteById(id); } ``` - **`@CachePut`**:更新缓存内容,但始终执行方法(适合写入操作)。 - **示例**:更新用户信息并刷新缓存。 ```java @CachePut(value = "users", key = "#user.id") public User updateUser(User user) { return userRepository.save(user); } ``` - **`@Caching`**:组合多个缓存操作(如同时使用`@Cacheable`和`@CacheEvict`)。 - **示例**:复杂操作。 ```java @Caching( evict = {@CacheEvict(value = "users", allEntries = true)}, put = {@CachePut(value = "users", key = "#user.id")} ) public User updateAndRefresh(User user) { // 业务逻辑 } ``` #### 4. **配置缓存提供者** Spring支持多种缓存实现(如Redis、Caffeine)。通过在`application.properties`或`application.yml`中配置,切换缓存提供者: - **示例(使用Redis)**: ```properties # application.properties spring.cache.type=redis spring.redis.host=localhost spring.redis.port=6379 ``` 然后添加Redis依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> ``` #### 注意事项 - **键生成策略**:默认使用方法的参数作为键,但可以通过`keyGenerator`自定义。 - **缓存条件**:使用`condition`参数(SpEL表达式)控制何时缓存,例如`@Cacheable(value="users", condition="#id > 10")`。 - **性能优化**:缓存注解能显著减少数据库访问,但需注意缓存一致性(如使用`@CacheEvict`处理更新)。 - **测试**:在开发中,使用`spring.cache.type=none`临时禁用缓存进行调试。 通过以上步骤,您可以轻松地在Spring Boot项目中集成缓存功能。Spring缓存注解简化了缓存管理,提升了应用响应速度[^1]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值