Spring AOP如何整合redis(注解方式)实现缓存统一管理详解
前言
项目使用redis作为缓存数据,但面临着问题,比如,项目a,项目b都用到redis,而且用的redis都是一套集群,这样会带来一些问题。
问题:比如项目a的开发人员,要缓存一些热门数据,想到了redis,于是乎把数据放入到了redis,自定义一个缓存key:hot_data_key,数据格式是项目a自己的数据格式,项目b也遇到了同样的问题,也要缓存热门数据,也是hot_data_key,数据格式是项目b是自己的数据格式,由于用的都是同一套redis集群,这样key就是同一个key,有的数据格式适合项目a,有的数据格式适合项目b,会报错的,我们项目中就遇到这样的一个错误,找不到原因,结果就是两个平台用到了同一key,很懊恼。
解决方式:
1、弄一个常量类工程,所有的redis的key都放入到这个工程里,加上各自的平台标识,这样就不错错了
2、spring aop结合redis,再相应的service层,加上注解,key的规范是包名+key名,这样就不错重复了
思路:
1、自定义注解,加在需要缓存数据的地方
2、spring aop 结合redis实现
3、spel解析注解参数,用来得到响应的注解信息
4、redis的key:包名+key 防止redis的key重复
实现如下:
项目准备,由于是maven项目,需要引入相关的包
fastjson包 <dependency> <groupid>com.alibaba</groupid> <artifactid>fastjson</artifactid> <version>${com.alibaba.fastjson}</version> </dependency> spring-data-redis <dependency> <groupid>org.springframework.data</groupid> <artifactid>spring-data-redis</artifactid> <version>${spring.redis.version}</version> </dependency> <dependency> <groupid>redis.clients</groupid> <artifactid>jedis</artifactid> <version>${jedis.redis.clients.version}</version> </dependency>
还有一些必备的就是spring工程相关的包
1、自定义注解
import java.lang.annotation.elementtype; import java.lang.annotation.retention; import java.lang.annotation.retentionpolicy; import java.lang.annotation.target; import java.util.concurrent.timeunit; /** * 缓存注解 * * @author shangdc * */ @target({elementtype.method}) @retention(retentionpolicy.runtime) public @interface rediscache { /** * 缓存key的名称 * @return */ string key(); /** * key是否转换成md5值,有的key是整个参数对象,有的大内容的,比如一个大文本,导致redis的key很长 * 需要转换成md5值作为redis的key * @return */ boolean keytransformmd5() default false; /** * key 过期日期 秒 * @return */ int expiretime() default 60; /** * 时间单位,默认为秒 * @return */ timeunit dateunit() default timeunit.seconds; }
2、定义切点pointcut
/** * redis 缓存切面 * * @author shangdc * */ public class rediscacheaspect { //由于每个人的环境,日志用的不一样,怕报错,就直接关掉了此日志输出,如有需要可加上 //private logger log = logger.getlogger(rediscacheaspect.class); /** * 这块可配置,每个公司都要自己的缓存配置方式,到时候可配置自己公司所用的缓存框架和配置方式 */ @resource(name = "redistemplate") private valueoperations<string, string> valueoperations; /** * 具体的方法 * @param jp * @return * @throws throwable */ public object cache(proceedingjoinpoint jp, rediscache cacheable) throws throwable{ // result是方法的最终返回结果 object result = null; // 得到类名、方法名和参数 object[] args = jp.getargs(); //获取实现类的方法 method method = getmethod(jp); //注解信息 key string key = cacheable.key(); //是否转换成md5值 boolean keytransformmd5 = cacheable.keytransformmd5(); //---------------------------------------------------------- // 用spel解释key值 //---------------------------------------------------------- //解析el表达式后的的redis的值 string keyval = springexpressionutils.parsekey(key, method, jp.getargs(), keytransformmd5); // 获取目标对象 object target = jp.gettarget(); //这块是全路径包名+目标对象名 ,默认的前缀,防止有的开发人员乱使用key,乱定义key的名称,导致重复key,这样在这加上前缀了,就不会重复使用key string target_class_name = target.getclass().getname(); stringbuilder redis_key = new stringbuilder(target_class_name); redis_key.append(keyval); //最终的redis的key string redis_final_key = redis_key.tostring(); string value = valueoperations.get(redis_final_key); if (value == null) { //这块是判空 // 缓存未命中,这块没用log输出,可以自定义输出 system.out.println(redis_final_key + "缓存未命中缓存"); // 如果redis没有数据则执行拦截的方法体 result = jp.proceed(args); //存入json格式字符串到redis里 string result_json_data = jsonobject.tojsonstring(result); system.out.println(result_json_data); // 序列化结果放入缓存 valueoperations.set(redis_final_key, result_json_data, getexpiretimeseconds(cacheable), timeunit.seconds); } else { // 缓存命中,这块没用log输出,可以自定义输出 system.out.println(redis_final_key + "命中缓存,得到数据"); // 得到被代理方法的返回值类型 class<?> returntype = ((methodsignature) jp.getsignature()).getreturntype(); //拿到数据格式 result = getdata(value, returntype); } return result; } /** * 根据不同的class返回数据 * @param value * @param clazz * @return */ public <t> t getdata(string value, class<t> clazz){ t result = jsonobject.parseobject(value, clazz); return result; } /** * 获取方法 * @param pjp * @return * @throws nosuchmethodexception */ public static method getmethod(proceedingjoinpoint pjp) throws nosuchmethodexception { // -------------------------------------------------------------------------- // 获取参数的类型 // -------------------------------------------------------------------------- object[] args = pjp.getargs(); class[] argtypes = new class[pjp.getargs().length]; for (int i = 0; i < args.length; i++) { argtypes[i] = args[i].getclass(); } string methodname = pjp.getsignature().getname(); class<?> targetclass = pjp.gettarget().getclass(); method[] methods = targetclass.getmethods(); // -------------------------------------------------------------------------- // 查找class<?>里函数名称、参数数量、参数类型(相同或子类)都和拦截的method相同的method // -------------------------------------------------------------------------- method method = null; for (int i = 0; i < methods.length; i++) { if (methods[i].getname() == methodname) { class<?>[] parametertypes = methods[i].getparametertypes(); boolean issamemethod = true; // 如果相比较的两个method的参数长度不一样,则结束本次循环,与下一个method比较 if (args.length != parametertypes.length) { continue; } // -------------------------------------------------------------------------- // 比较两个method的每个参数,是不是同一类型或者传入对象的类型是形参的子类 // -------------------------------------------------------------------------- for (int j = 0; parametertypes != null && j < parametertypes.length; j++) { if (parametertypes[j] != argtypes[j] && !parametertypes[j].isassignablefrom(argtypes[j])) { issamemethod = false; break; } } if (issamemethod) { method = methods[i]; break; } } } return method; } /** * 计算根据cacheable注解的expire和dateunit计算要缓存的秒数 * @param cacheable * @return */ public int getexpiretimeseconds(rediscache rediscache) { int expire = rediscache.expiretime(); timeunit unit = rediscache.dateunit(); if (expire <= 0) {//传入非法值,默认一分钟,60秒 return 60; } if (unit == timeunit.minutes) { return expire * 60; } else if(unit == timeunit.hours) { return expire * 60 * 60; } else if(unit == timeunit.days) { return expire * 60 * 60 * 24; }else {//什么都不是,默认一分钟,60秒 return 60; } } }
3、spring相关配置
由于是公司的项目,所有包就的路径就去掉了
<!-- aop配置,切面类 .rediscacheaspect类 bean--> <bean id="rediscacheaspect" class="包.rediscacheaspect"> </bean> <!-- 拦截所有指定 包和指定类型下的 下所有的方法 ,你是想这个在哪些包下可以实现--> <aop:config proxy-target-class="true"> <aop:aspect ref="rediscacheaspect"> <aop:pointcut id="rediscacheaoppointcut" expression="(execution(* 包.business.web.service.*.*(..)) and @annotation(cacheable))"/> <!-- 环绕 ,命中缓存则直接放回缓存数据,不会往下走,未命中直接放行,直接执行对应的方法--> <aop:around pointcut-ref="rediscacheaoppointcut" method="cache"/> </aop:aspect> </aop:config>
4、工具类 spel
/** * spring el表达式 * * @author shangdc * */ public class springexpressionutils { /** * 获取缓存的key key 定义在注解上,支持spel表达式 注: method的参数支持javabean和map * method的基本类型要定义为对象,否则没法读取到名称 * * example1: phone phone = new phone(); "#{phone.cpu}" 为对象的取值 、 * example2: map apple = new hashmap(); apple.put("name","good apple"); "#{apple[name]}" 为map的取值 * example3: "#{phone.cpu}_#{apple[name]}" * * * @param key * @param method * @param args * @return */ public static string parsekey(string key, method method, object[] args, boolean keytransformmd5) { // 获取被拦截方法参数名列表(使用spring支持类库) localvariabletableparameternamediscoverer u = new localvariabletableparameternamediscoverer(); string[] paranamearr = u.getparameternames(method); // 使用spel进行key的解析 expressionparser parser = new spelexpressionparser(); // spel上下文 standardevaluationcontext context = new standardevaluationcontext(); // 把方法参数放入spel上下文中 for (int i = 0; i < paranamearr.length; i++) { context.setvariable(paranamearr[i], args[i]); } parsercontext parsercontext = new templateparsercontext(); // ---------------------------------------------------------- // 把 #{ 替换成 #{# ,以适配spel模板的格式 // ---------------------------------------------------------- //例如,@注解名称(key="#{player.username}",expire = 200) //#{phone[cpu]}_#{phone[ram]} //#{player.username}_#{phone[cpu]}_#{phone[ram]}_#{pageno}_#{pagesize} object returnval = parser.parseexpression(key.replace("#{", "#{#"), parsercontext).getvalue(context, object.class); //这块这么做,是为了object和string都可以转成string类型的,可以作为key string return_data_key = jsonobject.tojsonstring(returnval); //转换成md5,是因为redis的key过长,并且这种大key的数量过多,就会占用内存,影响性能 if(keytransformmd5) { return_data_key = md5util.digest(return_data_key); } return returnval == null ? null : return_data_key; } }
5、redis相关配置
重要的是自己的redis配置,可能跟我的不太一样,用自己的就好
<!-- redistemplate defination --> <bean id="redistemplate" class="org.springframework.data.redis.core.stringredistemplate"> <property name="connectionfactory" ref="jedisconnectionfactory" /> </bean>
测试
public class rediscachetest { @test public void test() { applicationcontext context = new classpathxmlapplicationcontext("classpath:spring/spring-applicationcontext.xml"); rediscacheaopservice rediscacheaopservice = (rediscacheaopservice) context.getbean("rediscacheaopservice"); api api = rediscacheaopservice.getapi(1l); system.out.println(api.getclass()); system.out.println(jsonobject.tojsonstring(api)); apiparam param = new apiparam(); param.setid(2l); param.setapiname("短信服务接口数据"); // system.out.println("tostring:" + param.tostring()); // system.out.println(md5util.digest(param.tostring())); api api_1 = rediscacheaopservice.getapibyparam(param); system.out.println(api_1.getclass()); system.out.println(jsonobject.tojsonstring(api_1)); } }
测试打印信息:
大体思路是这样,需要自己动手实践,不要什么都是拿过来直接copy,使用,整个过程都不操作,也不知道具体的地方,该用什么,自己实际操作,可以得到很多信息。
辅助信息类:
public class api implements serializable { private static final long serialversionuid = 1l; /** * * 自增主键id */ private long id; /** * * api名称 */ private string apiname; /** * * api描述 */ private string apidescription; /** * * 有效时间 */ private integer valid; /** * 处理类 */ private string handlerclass; /** * * */ private date updatetime; /** * * */ private date createtime; public api() { } public string tostring() { return "id:" + id + ", apiname:" + apiname + ", apidescription:" + apidescription + ", valid:" + valid + ", updatetime:" + updatetime + ", createtime:" + createtime; } public long getid() { return this.id; } public void setid(long id) { this.id = id; } public string getapiname() { return this.apiname; } public void setapiname(string apiname) { this.apiname = apiname; } public string getapidescription() { return this.apidescription; } public void setapidescription(string apidescription) { this.apidescription = apidescription; } public integer getvalid() { return this.valid; } public void setvalid(integer valid) { this.valid = valid; } public date getupdatetime() { return this.updatetime; } public void setupdatetime(date updatetime) { this.updatetime = updatetime; } public date getcreatetime() { return this.createtime; } public void setcreatetime(date createtime) { this.createtime = createtime; } public string gethandlerclass() { return handlerclass; } public void sethandlerclass(string handlerclass) { this.handlerclass = handlerclass; } }
参数类信息
public class apiparam { /** * */ private static final long serialversionuid = 1l; /** * api表主键id */ private long id; /** * * api名称 */ private string apiname; /** * * 有效or无效 */ private integer valid; public string getapiname() { return apiname; } public void setapiname(string apiname) { this.apiname = apiname; } public integer getvalid() { return valid; } public void setvalid(integer valid) { this.valid = valid; } public long getid() { return id; } public void setid(long id) { this.id = id; } /*@override public string tostring() { return "apiparam [id=" + id + ", apiname=" + apiname + ", valid=" + valid + "]"; } */ }
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。