zoukankan      html  css  js  c++  java
  • [项目回顾]基于Annotation与SpringAOP的缓存简单解决方案

    前言:

        由于项目的原因,需要对项目中大量访问多修改少的数据进行缓存并管理,为达到开发过程中通过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 }
    LoadFromMemcached

     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 }
    UpdateForMemcached

     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 }
    MemcachedCacheInterceptor

    辅助类:借用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 }
    MethodParamNamesScaner

     使用案例:

    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以及相关的缓存操作方法提取出来并完善细节即可完成基本通用的缓存组件。

  • 相关阅读:
    201571030321 马玉婷 实验二 小学四则运算
    构建之法浅思
    个人学期总结
    201571030320/201571030335《小学四则运算练习软件软件需求说明》结对项目报告
    201571030320/201571030335《小学四则运算练习软件》结对项目报告
    201571030320+小学四则运算练习软件项目报告
    初读《构建之法》所思所问
    个人学期总结
    201571030318/201574010343《小学四则运算练习软件软件需求说明》结对项目报告
    201571030318/201574010343《小学四则混合运算》结队报告 马麒
  • 原文地址:https://www.cnblogs.com/warden/p/simple_cache_solutions_base_on_springaop_and_annotation.html
Copyright © 2011-2022 走看看