使用Java中的annotations实现decorator设计模式 | Yet Another Thoughts
Decorator是一个经典的结构式设计模式,有着非常广泛的应用。在经典的Design Patterns:Elements of Reusable Object-Oriented Software中,它的用意被描述为:动态地为一个对象添加额外的责任与功能。对于扩展功能,装饰器提供了比子类化更加灵活的替代方案。
在许多编程语言中,比如Python,在语法上就提供了装饰器的支持,能够透明地使用装饰器。而Java则相比之下繁琐一些,通过Decorator接口的各类实现,针对被decorate的组件接口的实现来装饰。本文介绍一种基于annotation的decorator实现,虽然无法实现如python一般的透明使用装饰器,在某些情景下,也是一种灵活的实现方式。
通过decorator实现refactor_test
我们想要通过装饰器实现这样的一个测试工具:我们重新实现了一个函数A,原函数是B。在调用函数A时,能够自动运行函数B,对二者的结果作比较,如果不相等,将当前的环境信息输出到日志中,以便追查。同时,不应现对函数的正常使用。
这里的函数,我们要求是幂等的,无副作用的。
下列全部的代码在这里。
Python的decorator
使用python能够非常轻易地实现装饰器@refactor_test。代码如下(GitHub):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | import functools import logging LOGGER = logging.getLogger('refactor_test') def refactor_test(comp_func): def decorator(func): @functools.wraps(func) def wrapper(*args, **kws): comp_res = comp_func(*args, **kws) res = func(*args, **kws) if res != comp_res: message = "not equals for function:{} from {} with arguments:{}-{}".format(func.__name__, comp_func.__name__, args, kws) LOGGER.debug(message) print(message) return res return wrapper return decorator def refactor_from(message): return message @refactor_test(refactor_from) def refactor_to0(message): return message @refactor_test(refactor_from) def refactor_to1(message): return "!" + message if __name__ == '__main__': refactor_to0('Hello python!') refactor_to1('Hello python!') |
这是非常经典的python decorator实现,是完全透明的,调用者无需关注到我们在调用时候执行了一个refacor_test的过程。refactor_to0是一个符合要求的重构实现,而refactor_to1不是。
Java实现
由于java语法的限制,无法像动态语言python一样透明地为给定方法添加decorator。当然可以按照经典的设计实现,如下图所示。
对于我们想要解决的问题,在python中,通过装饰器语法,在编码时,就指定了由重构后方法到重构前方法的映射。而如果按照传统的方法实现,我们首先,需要维护一个重构后的方法到重构之前方法的映射表,另外,我们不能为每一个重构的方法都编写一个装饰器方法,不够灵活,过于繁琐。所以,我们需要使用java的反射机制,动态调用方法。第一点也是很繁琐的,或者写到配置文件,或者hard code到代码里,都是极不好的。我们通过java的annotation注解功能来实现。Oracle的官方tutorial中,有对java annotations比较细致的说明。我们来看看如何实现。
RefactorUtil.java (GitHub):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | import org.slf4j.Logger; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.AbstractMap; import java.util.Map; public class RefactorTestUtil { private static Logger LOGGER = null; public interface Equality <T, S> { public boolean isEqual(T obj0, S obj1); } public static void setLogger(Logger logger) { LOGGER = logger; } @Target( ElementType.METHOD ) @Retention( RetentionPolicy.RUNTIME ) public @interface RefactorTest { String classRef(); String methodName(); int[] paramClassIndex2ThisParams() default {}; } private static void testFailLog(String message, Map.Entry<Class<?>, String> migTo, Map.Entry<Class<?>, String> migFrom, Object ... params) { String argsStr = null; if (params != null && params.length > 0) { StringBuilder args = new StringBuilder(); for (Object param : params) { args.append(param).append(":").append(param.getClass().getSimpleName()); args.append(","); } if (args.length() > 0) { argsStr = args.substring(0, args.length() - 1); } else { argsStr = args.toString(); } } String logStr = String.format("[MigrationTest]%s-TO(%s)-FROM-(%s)-ARGS(%s)", message, migTo.toString(), migFrom.toString(), argsStr); if (LOGGER != null) { LOGGER.error(logStr); } else { System.err.println(logStr); } } public static <T> T decorateFunctionWithRefactorTest(Class<?> cls, String method, Object ... params) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { return decorateFunctionWithRefactorTest(cls, method, new Equality<T, Object>() { public boolean isEqual(T obj0, Object obj1) { return obj0.equals(obj1); } }, params); } public static <T, S> T decorateFunctionWithRefactorTest(Class<?> cls, String method, Equality<T, S> equals, Object... params) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { Method refactorTo = TypeUtil.getClassMethodWithNotAccurateTypedParams(cls, method, params); if (refactorTo == null) { throw new NoSuchMethodException(String.format("There is no method %s in class %s", method, cls .getSimpleName())); } T toResult = (T) refactorTo.invoke(null, params); RefactorTest refactorAnno = refactorTo.getAnnotation(RefactorTest.class); String refactorFromCls = refactorAnno.classRef(); String refactorFromMethod = refactorAnno.methodName(); int[] paramClassesIndex = refactorAnno.paramClassIndex2ThisParams(); try { Class<?> refactorFromClass = ClassLoader.getSystemClassLoader() .loadClass(refactorFromCls); Object[] fromParams = null; if (paramClassesIndex != null && paramClassesIndex.length > 0) { fromParams = new Object[paramClassesIndex.length]; for (int i = 0; i < paramClassesIndex.length; i ++) { fromParams[i] = params[paramClassesIndex[i]]; } } else { fromParams = params; } Method refactorFrom = TypeUtil.getClassMethodWithNotAccurateTypedParams (refactorFromClass, refactorFromMethod, fromParams); if (refactorFrom == null) { testFailLog("No refactor-from method found", new AbstractMap. SimpleEntry<Class<?>, String>(cls, method) , new AbstractMap.SimpleEntry<Class<?>,String> (refactorFromClass, refactorFromMethod), params); return toResult; } S fromResult = (S) refactorFrom.invoke(null, fromParams); if (! equals.isEqual(toResult, fromResult)) { testFailLog("Not equal after refactoring", new AbstractMap.SimpleEntry <Class<?>, String>(cls, method) , new AbstractMap.SimpleEntry<Class<?>, String> (refactorFromClass, refactorFromMethod), params); } } catch (ClassNotFoundException e) { testFailLog("No refactor-from Class found", new AbstractMap.SimpleEntry <Class<?>, String>(cls, method), new AbstractMap.SimpleEntry<Class<?>, String>(null, refactorFromMethod), params); } finally { return toResult; } } } |
RefactorTestUtil.decorateFunctionWithRefactorTest()方法通过传入对应类与方法名,还有参数列表,通过RefactorTest注解获取该方法对应重构前方法,动态比较两次调用的结果是否一致,决定是否计入日志。
@interface RefactorTest是一个注解的声明,再待注解的方法前添加@RefactorTest(…),通过三个属性classRef,methodName,paramClassIndex2ThisParams来给定重构前方法及调用参数的不对齐问题。
通过注解和反射我们实现了这个功能,而由于java反射的限制,对于参数列表的类型不是方法签名中参数列表的类型完全匹配无法找到确定的方法,我实现了TypeUtil,提供了简单的动态机制,找到对应方法。比如size(Collection)方法,再传入一个Set时,仅仅通过java的反射API,无法找到size(Collection)方法。
TypeUtil.java(GitHub):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | import java.lang.reflect.Method; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; public class TypeUtil { public static boolean isMatchedBoxingType(Class<?> cls0, Class<?> cls1) { if (cls0 == null || cls1 == null) { return false; } if (! cls0.isPrimitive() && ! cls1.isPrimitive()) { return cls0.equals(cls1); } if (cls0.isPrimitive() && cls1.isPrimitive()) { return cls0.equals(cls1); } Class<?> primitive = cls0.isPrimitive() ? cls0 : cls1, boxing = cls1.isPrimitive() ? cls0 : cls1; if (primitive.equals(int.class)) { return boxing.equals(Integer.class); } if (primitive.equals(short.class)) { return boxing.equals(Short.class); } if (primitive.equals(float.class)) { return boxing.equals(Float.class); } if (primitive.equals(double.class)) { return boxing.equals(Double.class); } if (primitive.equals(boolean.class)) { return boxing.equals(Boolean.class); } if (primitive.equals(long.class)) { return boxing.equals(Long.class); } if (primitive.equals(char.class)) { return boxing.equals(Character.class); } if (primitive.equals(byte.class)) { return boxing.equals(Byte.class); } return false; } private static boolean isSubClassOf(Class<?> subCls, Class<?> superCls) { if (subCls == null || superCls == null) { return false; } if (superCls.equals(Object.class)) { return true; } if (superCls.isInterface() && ! subCls.isInterface()) { for (Class<?> interf : subCls.getInterfaces()) { if (interf.equals(superCls)) { return true; } } return false; } Class<?> cls = subCls; for (; cls != null && ! cls.equals(superCls); cls = cls.getSuperclass()); return cls != null; } public static Method getClassMethodWithNotAccurateTypedParams(Class<?> cls, String methodName, Object ... params) { if (cls == null || methodName == null) { return null; } Class<?>[] paramClasses = new Class<?>[params.length]; int i = 0; for (Object param : params) { paramClasses[i++] = param.getClass(); } Method method = null; try { method = cls.getMethod(methodName, paramClasses); } catch (NoSuchMethodException e) { Method[] methods = cls.getMethods(); List<Method> capableMethods = new ArrayList<Method>(); for (Method candidateMethod : methods) { if (! candidateMethod.getName().equals(methodName)) { continue; } if (! candidateMethod.isVarArgs() && candidateMethod.getParameterTypes().length != params.length) { continue; } Class<?>[] methodParamClasses = candidateMethod.getParameterTypes(); boolean found = true; for (int j = 0; j < methodParamClasses.length; j ++) { Class<?> methodParamClass = methodParamClasses[j]; if(! TypeUtil.isInstanceOf(methodParamClass, params[j])) { found = false; |