@Service public class ContentServiceImpl implements ContentService { @Autowired private TbContentMapper contentMapper; @Autowired private JedisClient jedisClient; @Value("${INDEX_CONTENT_REDIS_KEY}") private String INDEX_CONTENT_REDIS_KEY; @Override public List<TbContent> getContentList(long contentCid) { //从缓存中取内容 try { String result = jedisClient.hget(INDEX_CONTENT_REDIS_KEY, contentCid + ""); if (!StringUtils.isBlank(result)) { //把字符串转换成list List<TbContent> resultList = JsonUtils.jsonToList(result, TbContent.class); return resultList; } } catch (Exception e) { e.printStackTrace(); } //根据内容分类id查询内容列表 TbContentExample example = new TbContentExample(); Criteria criteria = example.createCriteria(); criteria.andCategoryIdEqualTo(contentCid); //执行查询 List<TbContent> list = contentMapper.selectByExample(example); //向缓存中添加内容 try { //把list转换成字符串 String cacheString = JsonUtils.objectToJson(list); jedisClient.hset(INDEX_CONTENT_REDIS_KEY, contentCid + "", cacheString); } catch (Exception e) { e.printStackTrace(); } return list; } }
上述代码是一个获取内容分类的方法,但实际和获取内容分类有关的代码仅仅是
这一段代码而已,前后的代码是从缓存取内容和把内容放入缓存的操作,这两个操作与业务无关,带来很多问题:
1)降低代码易读性;
2)这两个操作都需要加try-catch,以避免影响主业务流程的进行;
3)代码冗余度高,每个需要缓存的都需要写一遍,如果缓存方式变动,所有涉及的地方都需要更改,非常不灵活;
本文使用注解+AOP的方式,把缓存的操作从业务代码里分离出来
实现步骤:
1、创建自定义注解@MyCaChe
@Target({ElementType.METHOD,ElementType.TYPE}) //作用于类和方法 @Retention(RetentionPolicy.RUNTIME) //运行时 public @interface MyCache { String cacheName(); //缓存名 String key(); //键值,支持EL表达式 }
2、测试自定义注解
public class TestAnnotation { @MyCache(cacheName = "TestCache",key = "#param") public String Hello(String param){ return param; } @Test public void testAnnoMycache() throws NoSuchMethodException { Class cls = TestAnnotation.class; Method mtd = cls.getMethod("Hello",String.class); MyCache anno = mtd.getAnnotation(MyCache.class); String cacheName = anno.cacheName(); String key = anno.key(); System.out.println("cacheName:"+cacheName+"||key:"+key); } }
测试结果:
cacheName:TestCache||key:#param
3、修改业务代码
@Service public class ContentServiceImpl implements ContentService { @Autowired private TbContentMapper contentMapper; @Autowired private JedisClient jedisClient; // @Value("${INDEX_CONTENT_REDIS_KEY}") // private String INDEX_CONTENT_REDIS_KEY; private static final String INDEX_CONTENT_REDIS_KEY = "INDEX_CONTENT_REDIS_KEY"; //修改为常量 @Override @MyCache(cacheName = INDEX_CONTENT_REDIS_KEY,key="#contentCid") //加入这一行注解,其中key为EL表达式,实际值为形参contentCid的值 public List<TbContent> getContentList(Long contentCid) { //由于LocalVariableTableParameterNameDiscoverer无法获取类型为原始类型long.int等形参的形参名,所以使用包装类替换 //根据内容分类id查询内容列表 TbContentExample example = new TbContentExample(); Criteria criteria = example.createCriteria(); criteria.andCategoryIdEqualTo(contentCid); //执行查询 List<TbContent> list = contentMapper.selectByExample(example); return list; } }
4、创建切面类
@Component //记得要扫描 @Aspect public class MyCacheAspect { @Autowired private JedisClient jedisClient; @Around("@annotation(myCache)") //配置切入点,被MyCache注解的方法 public Object doCache(ProceedingJoinPoint pjp, MyCache myCache) throws Throwable { //获取key String key = getKey(myCache.key(),pjp); String cacheName = myCache.cacheName(); //从缓存中取内容 String result = jedisClient.hget(cacheName, key); if (!StringUtils.isBlank(result)) { //把字符串转换成Object Object ret = JsonUtils.jsonToPojo(result, Object.class); return ret; } Object ret = pjp.proceed(); //执行主业务代码 //向缓存中添加内容 //把业务执行返回转换成JSON字符串 String cacheString = JsonUtils.objectToJson(ret); jedisClient.hset(cacheName, key , cacheString); return ret; } /** * 解析key * @param key 键值,支持EL表达式 * @param pjp * @return */ private String getKey(String key, ProceedingJoinPoint pjp) { Method mtd = ((MethodSignature)pjp.getSignature()).getMethod(); //获取方法 String [] paramNames = new LocalVariableTableParameterNameDiscoverer().getParameterNames(mtd); //获取方法的形参名数组 Object[] args = pjp.getArgs(); //获取形参值数组 return SpelParser.parseEl(key, paramNames, args); //使用EL解析工具类进行解析 } }
5、SpelParser工具类
/** * EL表达式解析工具类 * @author mutongli * @date 2018-04-23 */ public class SpelParser { private static ExpressionParser parser = new SpelExpressionParser(); /** * 解析EL表达式 * @param key 表达式 * @param paramName 参数名数组 * @param args 参数值数组 * @return EL表达式运算结果 */ public static String parseEl(String key,String [] paramName,Object[] args){ Expression exp = parser.parseExpression(key); //将key解析为EL表达式 EvaluationContext context = new StandardEvaluationContext(exp); //创建赋值上下文 if(args.length <= 0){ return null; } //将形参和形参值以配对的方式配置到赋值上下文中 for (int i=0;i<args.length;i++){ context.setVariable(paramName[i],args[i]); } //根据赋值上下文运算EL表达式 return exp.getValue(context,String.class); } }
运行程序,进行调试发现切面类的getKey方法里的LocalVariableTableParameterNameDiscoverer取不到形参的名字,发现是因为pjp的getSignature取的是接口ContentService,而接口的字节码class文件里找不到形参名,所以对getKey方法进行调整(此处属于个人理解和试验所发现的问题,如果有发现不一样的或者其他正确的结论/方式,希望能够分享一下)
/** * 解析key * @param key 键值,支持EL表达式 * @param pjp * @return */ private String getKey(String key, ProceedingJoinPoint pjp) throws NoSuchMethodException { Class cls = pjp.getTarget().getClass(); //获取目标类信息 MethodSignature mts = (MethodSignature)pjp.getSignature(); String mtdname = mts.getName(); //获取方法名 Method mtd = cls.getMethod(mtdname,mts.getParameterTypes()); //获取目标方法信息 // String [] paramNames = ((MethodSignature)pjp.getSignature()).getParameterNames(); // Method mtd = ((MethodSignature) pjp.getSignature()).getMethod(); //获取方法(由于获取的是接口的方法信息,无法获取形参名数组) String [] paramNames = new LocalVariableTableParameterNameDiscoverer().getParameterNames(mtd); //获取方法的形参名数组 Object[] args = pjp.getArgs(); //获取形参值数组 return SpelParser.parseEl(key, paramNames, args); //使用EL解析工具类进行解析 }
6、调整后重新执行发现能够正常获取形参名数组
在redis里也可以查询到缓存信息
到此就结束了,该文章是本人的第二篇文章,文笔和描述会比较生疏,如有错误的地方,望指出!谢谢。