日前整理——使用Redis构建二层缓存,初步尝试使用了Redis和Spring提供给我们基于注解的缓存。非常方便!使用之余,心存疑惑:它到底是怎么实现的?于是,小猿花费一点时间,写了个小例子,不依赖Spring提供的Cacheable注解,自己实现一套类似的缓存。研究的并不深入,但足以知道它的工作流程。要知道,自定义注解、AOP、SpEL这些技术,小猿此前从来没有用过囧!所以,即使你啥都不知道,阅读此文,也可以对上述技术和Spring cache原理有初步领悟。
定义两个注解MyCacheable和MyCacheEvict,分别用于“从缓存中读取,不存在则加入缓存”和“清除缓存”。两个注解都只有一个参数——key,默认值为空字符串。注意@Target(ElementType.METHOD)注解, Spring只支持方法级AOP。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyCacheable {
/**
* 缓存key.
* @return
*/
String key() default "";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyCacheEvict {
/**
* 缓存key.
* @return
*/
String key() default "";
}
简单使用ConcurrentHashMap实现缓存,这不是重点。注意@Component注解,这个注解将Cache注入Bean,如此以来,Spring的单例模式保证了所有使用缓存的方法均使用该缓存,而无需将cache设置为static。
@Component("cache")
public class Cache {
/**
* ConcurrentHashMap实现简单缓存.
* ConcurrentHashMap的key和value均不为null.
*/
private Map<String, Object> cache = new ConcurrentHashMap<>();
public Object get(String key) {
return cache.get(key);
}
public void put(String key, Object obj) {
if(null != key && null != obj) {
cache.put(key, obj);
}
}
public void evict(String key) {
if (null != key) {
cache.remove(key);
}
}
public String listAll() {
return cache.toString();
}
}
如果用户在注解中不传参数key,则用反射取得注解使用类的签名,根据签名和实参值生成缓存key。
@Component
@Aspect
public class CacheAspect {
/**
* 缓存实体.
*/
@Resource
private Cache cache;
/**
* MyCacheable注解,用于缓存获取和缓存写入.
*
* @param joinPoint
* @param cacheable
* @return
* @throws Throwable
*/
@Around("execution(@MyCacheable * *.*(..)) && @annotation(cacheable)")
public Object aroundCacheable(ProceedingJoinPoint joinPoint, MyCacheable cacheable) throws Throwable {
String key; //缓存key
if (cacheable.key().equals("")) {
//根据方法签名生成key
key = generateKey(joinPoint);
} else {
//使用注解中的key, 支持SpEL表达式
String spEL = cacheable.key();
key = generateKeyBySpEL(spEL, joinPoint);
}
//尝试从缓存中获取
Object result = cache.get(key);
if (null != result) {
//缓存中存在即直接返回该值
System.out.println("cache hit! key = " + key);
return result;
}
//缓存中不存在则执行该方法
System.out.println("cache miss! put into cache...");
result = joinPoint.proceed();
//将通过执行方法获取到的值放入缓存并返回结果
cache.put(key, result);
return result;
}
/**
* MyCacheEvict注解,用于缓存清除.
*
* @param joinPoint
* @param cacheEvict
* @return
* @throws Throwable
*/
@Around("execution(@MyCacheEvict * *.*(..)) && @annotation(cacheEvict)")
public Object aroundCacheEvict(ProceedingJoinPoint joinPoint, MyCacheEvict cacheEvict) throws Throwable {
//首先执行方法
Object result = joinPoint.proceed();
//删除对应缓存
String key; //缓存key
if (cacheEvict.key().equals("")) {
//默认根据方法签名生成key
key = generateKey(joinPoint);
} else {
//使用注解中的key, 支持SpEL表达式
String spEL = cacheEvict.key();
key = generateKeyBySpEL(spEL, joinPoint);
}
System.out.println("evict cache! remove key " + key);
cache.evict(key);
//返回结果
return result;
}
/**
* 默认缓存key生成器.
* 注解中key不传参,根据方法签名和参数生成key.
*
* @param joinPoint
* @return
*/
private String generateKey(ProceedingJoinPoint joinPoint) {
Class itsClass = joinPoint.getTarget().getClass();
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
StringBuilder keyBuilder = new StringBuilder();
keyBuilder.append(itsClass.getName());
keyBuilder.append(".").append(methodSignature.getName());
keyBuilder.append("(");
for(Object arg : joinPoint.getArgs()) {
keyBuilder.append(arg.getClass().getSimpleName() + arg + ";");
}
keyBuilder.append(")");
return keyBuilder.toString();
}
}
我们希望在使用缓存时能够自定义缓存key,并且在指定key时可以把所在方法的参数值带入。例如:
@Cache(key = “’xxx’ + #arg”)
Value someMethod(ArgType arg)
我们希望把arg也用于key的生成,只需在CacheAspect中加入支持SpEL解析的keyGenerator即可。
/**
* 用于SpEL表达式解析.
*/
private SpelExpressionParser parser = new SpelExpressionParser();
/**
* 用于获取方法参数定义名字.
*/
private DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
/**
* SpEL表达式缓存Key生成器.
* 注解中传入key参数,则使用此生成器生成缓存.
*
* @param spELString
* @param joinPoint
* @return
*/
private String generateKeyBySpEL(String spELString, ProceedingJoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String[] paramNames = nameDiscoverer.getParameterNames(methodSignature.getMethod());
Expression expression = parser.parseExpression(spELString);
EvaluationContext context = new StandardEvaluationContext();
Object[] args = joinPoint.getArgs();
for(int i = 0 ; i < args.length ; i++) {
context.setVariable(paramNames[i], args[i]);
}
return expression.getValue(context).toString();
}
好了,可以使用注解@MyCacheable和注解@MyCacheEvict使用缓存了!
service:
@Service("userService")
public class UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
@MyCacheable(key = "'userCache:userId.' + #id")
public String getUser(String id) {
System.out.println("从数据库获取!!!!!!!!!!!!!!!!!!!!!!!!!!");
String sql = "select id,username from user where id=?";
RowMapper<User> rowMapper = new BeanPropertyRowMapper<>(User.class);
User user = jdbcTemplate.queryForObject(sql, rowMapper, id);
return user.toString();
}
@MyCacheEvict(key = "'userCache:userId.' + #id")
public String updateUser(String id, String name) {
String sql = "update user set username=? where id=?";
try {
return String.valueOf(jdbcTemplate.update(sql, name, id));
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
controller:
@Controller
public class SampleController {
@Resource
private UserService userService;
@RequestMapping("/")
@ResponseBody
public String home() {
return "Hello World";
}
@RequestMapping("/getUser/{id}")
@ResponseBody
public String getUser(@PathVariable String id) {
return userService.getUser(id);
}
@RequestMapping("/updateUser/{id}/{name}")
@ResponseBody
public String updateUser(@PathVariable String id, @PathVariable String name) {
return userService.updateUser(id, name);
}
}
写一个Controller用于显示缓存中内容。
@Controller
public class CacheController {
@Resource
private Cache cache;
@RequestMapping("/cache")
@ResponseBody
public String showCache() {
return cache.listAll();
}
}
好了,运行APP!
访问getUser接口,获取userId为1的用户:
后台显示从数据库获取,并加入缓存。
查看缓存,发现已经存了一条数据。缓存的key为userCache:userId.1,其中1是方法参数userId.
再次获取该用户信息,后台显示从缓存命中。
获取获取用户2:
查看缓存,已经存了一条key为userCache:userId.2的数据。
修改一下用户1的名称:
后台数据显示进行了缓存清除。
此时查看缓存信息,删除了已经被修改过的userId为1的无效信息。
这种操作没有实现了缓存中数据的同步更新,但以最小代价保持了缓存一致性。
网易云新用户大礼包:https://www.163yun.com/gift
本文来自网易实践者社区,经作者葛志诚授权发布。