[实习记录]缓存进阶——使用自定义注解+AOP+SpEL实现缓存

达芬奇密码2018-08-13 10:43

日前整理——使用Redis构建二层缓存,初步尝试使用了Redis和Spring提供给我们基于注解的缓存。非常方便!使用之余,心存疑惑:它到底是怎么实现的?于是,小猿花费一点时间,写了个小例子,不依赖Spring提供的Cacheable注解,自己实现一套类似的缓存。研究的并不深入,但足以知道它的工作流程。要知道,自定义注解、AOP、SpEL这些技术,小猿此前从来没有用过囧!所以,即使你啥都不知道,阅读此文,也可以对上述技术和Spring cache原理有初步领悟。

1. 自定义注解

定义两个注解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 "";
}

2. 缓存实现

简单使用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();
    }
}

3. AOP切面

如果用户在注解中不传参数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();
    }

}

4. 基于SpEL的keyGenerator

我们希望在使用缓存时能够自定义缓存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();
}

5. 使用缓存

好了,可以使用注解@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);
    }
}

6. 验证

写一个Controller用于显示缓存中内容。

@Controller
public class CacheController {
    @Resource
    private Cache cache;

    @RequestMapping("/cache")
    @ResponseBody
    public String showCache() {
        return cache.listAll();
    }

}

好了,运行APP!

6.1 测试@MyCacheable

访问getUser接口,获取userId为1的用户:

后台显示从数据库获取,并加入缓存。

查看缓存,发现已经存了一条数据。缓存的key为userCache:userId.1,其中1是方法参数userId.

再次获取该用户信息,后台显示从缓存命中。

获取获取用户2:

查看缓存,已经存了一条key为userCache:userId.2的数据。

6.2 测试@MyCacheEvict

修改一下用户1的名称:

后台数据显示进行了缓存清除。

此时查看缓存信息,删除了已经被修改过的userId为1的无效信息。

这种操作没有实现了缓存中数据的同步更新,但以最小代价保持了缓存一致性。



网易云新用户大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者葛志诚授权发布。