前言:
由於項目的原因,需要對項目中大量訪問多修改少的數據進行緩存並管理,為達到開發過程中通過Annotation簡單的配置既可以完成對緩存的設置與更新的需求,故而設計的該簡易的解決方案。
涉及技術:
1、Spring AOP
2、Java Annotation
3、Memcache (項目中使用的緩存組件)
4、JVM基礎 (Class文件結構,用於解析出方法中的形參名稱,動態生成緩存key,目測效率不高0.0)
5、Ognl (用於動態解析緩存的key)
實現細節:
Annotation:LoadFromMemcached 用與method之上的注解,作用是使帶有該注解的method在調用的時候先經過緩存查詢,緩存中查詢不到再去數據庫查詢並將結果緩存至緩存服務器Memcache中,

1 import java.lang.annotation.ElementType; 2 import java.lang.annotation.Retention; 3 import java.lang.annotation.RetentionPolicy; 4 import java.lang.annotation.Target; 5 6 @Retention(RetentionPolicy.RUNTIME) 7 @Target(ElementType.METHOD) 8 public @interface LoadFromMemcached { 9 10 String value();//緩存的key 11 12 int timeScope() default 600;//默認過期時間,單位秒 13 14 String condition() default "";//執行緩存查詢的條件 15 16 }
Annotation:UpdateForMemcached 類似於LoadFromMemcached,作用是使帶有該注解的method在調用的時候更新緩存服務器中的緩存,

1 import java.lang.annotation.ElementType; 2 import java.lang.annotation.Retention; 3 import java.lang.annotation.RetentionPolicy; 4 import java.lang.annotation.Target; 5 6 @Retention(RetentionPolicy.RUNTIME) 7 @Target(ElementType.METHOD) 8 public @interface UpdateForMemcached { 9 10 String[] value();//可能有多個key需要更新 11 12 String condition() default "";//執行緩存的條件 13 14 }
AOP:MemcachedCacheInterceptor 緩存AOP實現的核心類,用於對Annotation注解了的method進行攔截並進行相應的操作,

1 import java.lang.annotation.Annotation; 2 import java.lang.reflect.Method; 3 import java.util.ArrayList; 4 import java.util.HashMap; 5 import java.util.List; 6 import java.util.Map; 7 import java.util.concurrent.TimeoutException; 8 import java.util.regex.Matcher; 9 import java.util.regex.Pattern; 10 11 import javax.annotation.Resource; 12 13 import net.rubyeye.xmemcached.MemcachedClient; 14 import net.rubyeye.xmemcached.exception.MemcachedException; 15 16 import ognl.Ognl; 17 import ognl.OgnlException; 18 19 import org.aspectj.lang.ProceedingJoinPoint; 20 import org.aspectj.lang.annotation.Around; 21 import org.aspectj.lang.annotation.Aspect; 22 import org.aspectj.lang.reflect.MethodSignature; 23 import org.slf4j.Logger; 24 import org.slf4j.LoggerFactory; 25 import org.springframework.stereotype.Component; 26 27 @Component 28 @Aspect 29 public class MemcachedCacheInterceptor { 30 31 private final String GET = "@annotation(LoadFromMemcached)"; 32 private final String UPDATE = "@annotation(UpdateForMemcached)"; 33 //替換為其他緩存組件即可切換為其他緩存系統,這里是使用的Memcached。如果再抽象一層緩存系統管理,則可以動態的更換緩存系統。 34 @Resource 35 private MemcachedClient cache; 36 37 private Logger log = LoggerFactory.getLogger(MemcachedCacheInterceptor.class); 38 39 /** 40 * 41 * @Title: get 42 * @Description: 首先從緩存中加載數據,緩存命中則返回數據,未命中則從數據庫查找,並加入緩存 43 * @param @param call 44 * @param @return 45 * @param @throws Throwable 46 * @return Object 47 * @throws 48 */ 49 @Around(GET) 50 public Object get(ProceedingJoinPoint call) throws Throwable { 51 52 LoadFromMemcached anno = getAnnotation(call,LoadFromMemcached.class); 53 String key = anno.value(); 54 int timeSocpe = anno.timeScope(); 55 56 if(!executeCondition(anno.condition(),call)){//不滿足條件,直接調用方法,不進行緩存AOP操作 57 return call.proceed(); 58 } 59 60 key = getKeyNameFromParam(key,call); 61 62 Object value = null; 63 64 try { 65 value = cache.get(key); 66 } catch (TimeoutException e) { 67 log.error("Get Data From Memcached TimeOut!About Key:"+key,e); 68 e.printStackTrace(); 69 } catch (InterruptedException e) { 70 log.error("Get Data From Memcached TimeOut And Interrupted!About Key:"+key,e); 71 e.printStackTrace(); 72 } catch (MemcachedException e) { 73 log.error("Get Data From Memcached And Happend A Unexpected Error!About Key:"+key,e); 74 e.printStackTrace(); 75 } 76 77 if(value == null){ 78 value = call.proceed(); 79 if(value != null){ 80 try { 81 cache.add(key, timeSocpe, value); 82 log.info("Add Data For Memcached Success!About Key:"+key); 83 } catch (TimeoutException e) { 84 log.error("Add Data For Memcached TimeOut!About Key:"+key,e); 85 e.printStackTrace(); 86 } catch (InterruptedException e) { 87 log.error("Add Data For Memcached TimeOut And Interrupted!About Key:"+key,e); 88 e.printStackTrace(); 89 } catch (MemcachedException e) { 90 log.error("Add Data For Memcached And Happend A Unexpected Error!About Key:"+key,e); 91 e.printStackTrace(); 92 } 93 } 94 } 95 96 return value; 97 } 98 99 /** 100 * 101 * @Title: update 102 * @Description: 執行方法的同時更新緩存中的數據 103 * @param @param call 104 * @param @return 105 * @param @throws Throwable 106 * @return Object 107 * @throws 108 */ 109 @Around(UPDATE) 110 public Object update(ProceedingJoinPoint call) throws Throwable { 111 112 UpdateForMemcached anno = getAnnotation(call,UpdateForMemcached.class); 113 String[] key = anno.value();//可能需要更新多個key 114 115 Object value = call.proceed(); 116 if(!executeCondition(anno.condition(),call)){//不滿足條件,直接調用方法,不進行緩存AOP操作 117 return value; 118 } 119 120 if(value != null){ 121 try { 122 for(String singleKey:key){//循環處理所有需要更新的key 123 String tempKey = getKeyNameFromParam(singleKey, call); 124 cache.delete(tempKey); 125 } 126 log.info("Update Data For Memcached Success!About Key:"+key); 127 } catch (TimeoutException e) { 128 log.error("Update Data For Memcached TimeOut!About Key:"+key,e); 129 e.printStackTrace(); 130 } catch (InterruptedException e) { 131 log.error("Update Data For Memcached TimeOut And Interrupted!About Key:"+key,e); 132 e.printStackTrace(); 133 } catch (MemcachedException e) { 134 log.error("Update Data For Memcached And Happend A Unexpected Error!About Key:"+key,e); 135 e.printStackTrace(); 136 } 137 138 } 139 return value; 140 } 141 142 /** 143 * 144 * @Title: getAnnotation 145 * @Description: 獲得Annotation對象 146 * @param @param <T> 147 * @param @param jp 148 * @param @param clazz 149 * @param @return 150 * @return T 151 * @throws 152 */ 153 private <T extends Annotation> T getAnnotation(ProceedingJoinPoint jp,Class<T> clazz){ 154 MethodSignature joinPointObject = (MethodSignature) jp.getSignature(); 155 Method method = joinPointObject.getMethod(); 156 return method.getAnnotation(clazz); 157 } 158 159 /** 160 * 161 * @Title: getKeyNameFromParam 162 * @Description: 獲得組合后的KEY值 163 * @param @param key 164 * @param @param jp 165 * @param @return 166 * @return String 167 * @throws 168 */ 169 private String getKeyNameFromParam(String key,ProceedingJoinPoint jp){ 170 if(!key.contains("$")){ 171 return key; 172 } 173 174 String regexp = "\\$\\{[^\\}]+\\}"; 175 Pattern pattern = Pattern.compile(regexp); 176 Matcher matcher = pattern.matcher(key); 177 List<String> names = new ArrayList<String>(); 178 try{ 179 while(matcher.find()){ 180 names.add(matcher.group()); 181 } 182 key = executeNames(key,names,jp); 183 }catch (Exception e) { 184 log.error("Regex Parse Error!", e); 185 } 186 187 188 return key; 189 } 190 191 /** 192 * 193 * @Title: executeNames 194 * @Description: 對KEY中的參數進行替換 195 * @param @param key 196 * @param @param names 197 * @param @param jp 198 * @param @return 199 * @param @throws OgnlException 200 * @return String 201 * @throws 202 */ 203 private String executeNames(String key, List<String> names,ProceedingJoinPoint jp) throws OgnlException { 204 205 Method method = ((MethodSignature)jp.getSignature()).getMethod(); 206 207 //形參列表 208 List<String> param = MethodParamNamesScaner.getParamNames(method); 209 210 if(names==null||names.size()==0){ 211 return key; 212 } 213 214 Object[] params = jp.getArgs(); 215 216 Map<String,Object> map = new HashMap<String,Object>(); 217 for(int i=0;i<param.size();i++){ 218 map.put(param.get(i), params[i]); 219 } 220 221 for(String name:names){ 222 String temp = name.substring(2); 223 temp = temp.substring(0,temp.length()-1); 224 key = myReplace(key,name, (String)Ognl.getValue(temp, map)); 225 } 226 227 return key; 228 } 229 230 /** 231 * 232 * @Title: myReplace 233 * @Description: 不依賴Regex的替換,避免$符號、{}等在String.replaceAll方法中當做Regex處理時候的問題。 234 * @param @param src 235 * @param @param from 236 * @param @param to 237 * @param @return 238 * @return String 239 * @throws 240 */ 241 private String myReplace(String src,String from,String to){ 242 int index = src.indexOf(from); 243 if(index == -1){ 244 return src; 245 } 246 247 return src.substring(0,index)+to+src.substring(index+from.length()); 248 } 249 250 251 /** 252 * 253 * @Title: executeCondition 254 * @Description: 判斷是否需要進行緩存操作 255 * @param @param condition parm 256 * @param @return 257 * @return boolean true:需要 false:不需要 258 * @throws 259 */ 260 private boolean executeCondition(String condition,ProceedingJoinPoint jp){ 261 262 if("".equals(condition)){ 263 return true; 264 } 265 266 Method method = ((MethodSignature)jp.getSignature()).getMethod(); 267 268 //形參列表 269 List<String> param = MethodParamNamesScaner.getParamNames(method); 270 271 if(param==null||param.size()==0){ 272 return true; 273 } 274 275 Object[] params = jp.getArgs(); 276 277 Map<String,Object> map = new HashMap<String,Object>(); 278 for(int i=0;i<param.size();i++){ 279 map.put(param.get(i), params[i]); 280 } 281 boolean returnVal = false; 282 try { 283 returnVal = (Boolean) Ognl.getValue(condition, map); 284 } catch (OgnlException e) { 285 e.printStackTrace(); 286 } 287 288 return returnVal; 289 } 290 291 public void setCache(MemcachedClient cache) { 292 this.cache = cache; 293 } 294 295 }
輔助類:借用MethodParamNamesScaner類與Ognl結合完成對緩存key的動態解析功能,

1 //引用至:https://gist.github.com/wendal/2011728,用於解析方法的形參名稱 2 import java.io.BufferedInputStream; 3 import java.io.DataInputStream; 4 import java.io.IOException; 5 import java.io.InputStream; 6 import java.lang.reflect.Constructor; 7 import java.lang.reflect.Method; 8 import java.util.ArrayList; 9 import java.util.HashMap; 10 import java.util.List; 11 import java.util.Map; 12 13 /** 14 * 通過讀取Class文件,獲得方法形參名稱列表 15 * @author wendal(wendal1985@gmail.com) 16 * 17 */ 18 public class MethodParamNamesScaner { 19 20 /** 21 * 獲取Method的形參名稱列表 22 * @param method 需要解析的方法 23 * @return 形參名稱列表,如果沒有調試信息,將返回null 24 */ 25 public static List<String> getParamNames(Method method) { 26 try { 27 int size = method.getParameterTypes().length; 28 if (size == 0) 29 return new ArrayList<String>(0); 30 List<String> list = getParamNames(method.getDeclaringClass()).get(getKey(method)); 31 if (list != null && list.size() != size) 32 return list.subList(0, size); 33 return list; 34 } catch (Throwable e) { 35 throw new RuntimeException(e); 36 } 37 } 38 39 /** 40 * 獲取Constructor的形參名稱列表 41 * @param constructor 需要解析的構造函數 42 * @return 形參名稱列表,如果沒有調試信息,將返回null 43 */ 44 public static List<String> getParamNames(Constructor<?> constructor) { 45 try { 46 int size = constructor.getParameterTypes().length; 47 if (size == 0) 48 return new ArrayList<String>(0); 49 List<String> list = getParamNames(constructor.getDeclaringClass()).get(getKey(constructor)); 50 if (list != null && list.size() != size) 51 return list.subList(0, size); 52 return list; 53 } catch (Throwable e) { 54 throw new RuntimeException(e); 55 } 56 } 57 58 //--------------------------------------------------------------------------------------------------- 59 60 /** 61 * 獲取一個類的所有方法/構造方法的形參名稱Map 62 * @param klass 需要解析的類 63 * @return 所有方法/構造方法的形參名稱Map 64 * @throws IOException 如果有任何IO異常,不應該有,如果是本地文件,那100%遇到bug了 65 */ 66 public static Map<String, List<String>> getParamNames(Class<?> klass) throws IOException { 67 InputStream in = klass.getResourceAsStream("/" + klass.getName().replace('.', '/') + ".class"); 68 return getParamNames(in); 69 } 70 71 public static Map<String, List<String>> getParamNames(InputStream in) throws IOException { 72 DataInputStream dis = new DataInputStream(new BufferedInputStream(in)); 73 Map<String, List<String>> names = new HashMap<String, List<String>>(); 74 Map<Integer, String> strs = new HashMap<Integer, String>(); 75 dis.skipBytes(4);//Magic 76 dis.skipBytes(2);//副版本號 77 dis.skipBytes(2);//主版本號 78 79 //讀取常量池 80 int constant_pool_count = dis.readUnsignedShort(); 81 for (int i = 0; i < (constant_pool_count - 1); i++) { 82 byte flag = dis.readByte(); 83 switch (flag) { 84 case 7://CONSTANT_Class: 85 dis.skipBytes(2); 86 break; 87 case 9://CONSTANT_Fieldref: 88 case 10://CONSTANT_Methodref: 89 case 11://CONSTANT_InterfaceMethodref: 90 dis.skipBytes(2); 91 dis.skipBytes(2); 92 break; 93 case 8://CONSTANT_String: 94 dis.skipBytes(2); 95 break; 96 case 3://CONSTANT_Integer: 97 case 4://CONSTANT_Float: 98 dis.skipBytes(4); 99 break; 100 case 5://CONSTANT_Long: 101 case 6://CONSTANT_Double: 102 dis.skipBytes(8); 103 i++;//必須跳過一個,這是class文件設計的一個缺陷,歷史遺留問題 104 break; 105 case 12://CONSTANT_NameAndType: 106 dis.skipBytes(2); 107 dis.skipBytes(2); 108 break; 109 case 1://CONSTANT_Utf8: 110 int len = dis.readUnsignedShort(); 111 byte[] data = new byte[len]; 112 dis.read(data); 113 strs.put(i + 1, new String(data, "UTF-8"));//必然是UTF8的 114 break; 115 case 15://CONSTANT_MethodHandle: 116 dis.skipBytes(1); 117 dis.skipBytes(2); 118 break; 119 case 16://CONSTANT_MethodType: 120 dis.skipBytes(2); 121 break; 122 case 18://CONSTANT_InvokeDynamic: 123 dis.skipBytes(2); 124 dis.skipBytes(2); 125 break; 126 default: 127 throw new RuntimeException("Impossible!! flag="+flag); 128 } 129 } 130 131 dis.skipBytes(2);//版本控制符 132 dis.skipBytes(2);//類名 133 dis.skipBytes(2);//超類 134 135 //跳過接口定義 136 int interfaces_count = dis.readUnsignedShort(); 137 dis.skipBytes(2 * interfaces_count);//每個接口數據,是2個字節 138 139 //跳過字段定義 140 int fields_count = dis.readUnsignedShort(); 141 for (int i = 0; i < fields_count; i++) { 142 dis.skipBytes(2); 143 dis.skipBytes(2); 144 dis.skipBytes(2); 145 int attributes_count = dis.readUnsignedShort(); 146 for (int j = 0; j < attributes_count; j++) { 147 dis.skipBytes(2);//跳過訪問控制符 148 int attribute_length = dis.readInt(); 149 dis.skipBytes(attribute_length); 150 } 151 } 152 153 //開始讀取方法 154 int methods_count = dis.readUnsignedShort(); 155 for (int i = 0; i < methods_count; i++) { 156 dis.skipBytes(2); //跳過訪問控制符 157 String methodName = strs.get(dis.readUnsignedShort()); 158 String descriptor = strs.get(dis.readUnsignedShort()); 159 short attributes_count = dis.readShort(); 160 for (int j = 0; j < attributes_count; j++) { 161 String attrName = strs.get(dis.readUnsignedShort()); 162 int attribute_length = dis.readInt(); 163 if ("Code".equals(attrName)) { //形參只在Code屬性中 164 dis.skipBytes(2); 165 dis.skipBytes(2); 166 int code_len = dis.readInt(); 167 dis.skipBytes(code_len); //跳過具體代碼 168 int exception_table_length = dis.readUnsignedShort(); 169 dis.skipBytes(8 * exception_table_length); //跳過異常表 170 171 int code_attributes_count = dis.readUnsignedShort(); 172 for (int k = 0; k < code_attributes_count; k++) { 173 int str_index = dis.readUnsignedShort(); 174 String codeAttrName = strs.get(str_index); 175 int code_attribute_length = dis.readInt(); 176 if ("LocalVariableTable".equals(codeAttrName)) {//形參在LocalVariableTable屬性中 177 int local_variable_table_length = dis.readUnsignedShort(); 178 List<String> varNames = new ArrayList<String>(local_variable_table_length); 179 for (int l = 0; l < local_variable_table_length; l++) { 180 dis.skipBytes(2); 181 dis.skipBytes(2); 182 String varName = strs.get(dis.readUnsignedShort()); 183 dis.skipBytes(2); 184 dis.skipBytes(2); 185 if (!"this".equals(varName)) //非靜態方法,第一個參數是this 186 varNames.add(varName); 187 } 188 names.put(methodName + "," + descriptor, varNames); 189 } else 190 dis.skipBytes(code_attribute_length); 191 } 192 } else 193 dis.skipBytes(attribute_length); 194 } 195 } 196 dis.close(); 197 return names; 198 } 199 200 /** 201 * 傳入Method或Constructor,獲取getParamNames方法返回的Map所對應的key 202 */ 203 public static String getKey(Object obj) { 204 StringBuilder sb = new StringBuilder(); 205 if (obj instanceof Method) { 206 sb.append(((Method)obj).getName()).append(','); 207 getDescriptor(sb, (Method)obj); 208 } else if (obj instanceof Constructor) { 209 sb.append("<init>,"); //只有非靜態構造方法才能用有方法參數的,而且通過反射API拿不到靜態構造方法 210 getDescriptor(sb, (Constructor<?>)obj); 211 } else 212 throw new RuntimeException("Not Method or Constructor!"); 213 return sb.toString(); 214 } 215 216 public static void getDescriptor(StringBuilder sb ,Method method){ 217 sb.append('('); 218 for (Class<?> klass : method.getParameterTypes()) 219 getDescriptor(sb, klass); 220 sb.append(')'); 221 getDescriptor(sb, method.getReturnType()); 222 } 223 224 public static void getDescriptor(StringBuilder sb , Constructor<?> constructor){ 225 sb.append('('); 226 for (Class<?> klass : constructor.getParameterTypes()) 227 getDescriptor(sb, klass); 228 sb.append(')'); 229 sb.append('V'); 230 } 231 232 /**本方法來源於ow2的asm庫的Type類*/ 233 public static void getDescriptor(final StringBuilder buf, final Class<?> c) { 234 Class<?> d = c; 235 while (true) { 236 if (d.isPrimitive()) { 237 char car; 238 if (d == Integer.TYPE) { 239 car = 'I'; 240 } else if (d == Void.TYPE) { 241 car = 'V'; 242 } else if (d == Boolean.TYPE) { 243 car = 'Z'; 244 } else if (d == Byte.TYPE) { 245 car = 'B'; 246 } else if (d == Character.TYPE) { 247 car = 'C'; 248 } else if (d == Short.TYPE) { 249 car = 'S'; 250 } else if (d == Double.TYPE) { 251 car = 'D'; 252 } else if (d == Float.TYPE) { 253 car = 'F'; 254 } else /* if (d == Long.TYPE) */{ 255 car = 'J'; 256 } 257 buf.append(car); 258 return; 259 } else if (d.isArray()) { 260 buf.append('['); 261 d = d.getComponentType(); 262 } else { 263 buf.append('L'); 264 String name = d.getName(); 265 int len = name.length(); 266 for (int i = 0; i < len; ++i) { 267 char car = name.charAt(i); 268 buf.append(car == '.' ? '/' : car); 269 } 270 buf.append(';'); 271 return; 272 } 273 } 274 } 275 }
使用案例:
1.使用緩存:
1 /** 2 * value:緩存中的鍵,${map.name}會動態替換為傳入參數map里面的key為name的值。 3 * comdition:緩存執行條件:!map.containsKey('execute')表示map中不包含execute這個key的時候才進行緩存操作。 4 * 這里面的map是傳入的參數名稱。 5 * 執行到該方法會自動去緩存里面查找該key,有就直接返回,沒有就執行該方法,如果返回值不為空則同時存入緩存並返回結果。 6 */ 7 @LoadFromMemcached(value="Resource_selectByMap_${map.name}",condition="!map.containsKey('execute')" ) 8 public List<Resource> selectByMap(Object map) { 9 return super.selectByMap(map); 10 }
表示執行該method(selectByMap)的時候會首先去緩存組件中查找數據,如果查找到數據就直接返回,如果找不到數據就執行方法體,並將返回值記錄入緩存中。
2.更新緩存:
1 /* 2 * 同樣value為緩存中的key,${t.name}會動態替換為update方法傳入參數Resource的name字段 3 * comdition:字段作用同上,不演示了 4 */ 5 @UpdateForMemcached(value="Resource_selectByMap_${t.name}") 6 public int update(Resource t) { 7 return super.update(t); 8 }
表示執行該method(update)的時候會同步將緩存中的key置為過期(並不是把該方法的返回值放入緩存,只是將對應的緩存設為過期,下次再執行selectByMap的時候獲取的就是最新的數據了)。
擴展:
本文只是簡單的解決方案,可能有很多不足的地方,歡迎交流0.0,以此簡單的結構為基礎進行擴展,將MemcachedClient以及相關的緩存操作方法提取出來並完善細節即可完成基本通用的緩存組件。