前言
此文的主要目的是记录注解和动态代理的使用,因为已有很多日志框架;此文介绍了注解和JDK动态代理,以及如何结合二者实现方法级别的基于注解的日志框架。
注解
注解可以理解为作用于Java类、构造方法、属性、方法、参数、局部参数上的标签,本身并没有额外的功能。有关注解更详细的内容,请读者自行查阅。
动态代理
众所周知,有一种叫代理模式的设计模式,就是代理目标对象完成实际的操作,动态代理的基本思想也是如此,但是动态代理通过反射等技术,为开发者省去了编写代理类的工作,使得代码的可维护性更高,相应的灵活度也更好。
为什么要使用注解和动态代理实现日志
业务需求是记录API调用情况,不需要非常详细的日志,所以日志的作用范围是方法;当然,这种需求也可以使用字节码增强技术实现,其一是使用字节码增强意味着会增大程序包体积,其二是这种需求没必要使用字节码增强技术。
怎么实现
定义注解
- 定义方法注解
@Retention(AnnotationRetention.RUNTIME) // 表示该注解将保留到运行时 @Target(AnnotationTarget.FUNCTION) // 表示该注解仅能作用于方法上 annotation class Log(val value: String)
- 定义方法参数注解
@Retention(AnnotationRetention.RUNTIME) // 表示该注解将保留到运行时 @Target(AnnotationTarget.VALUE_PARAMETER) // 表示该注解仅能作用于参数上 annotation class Param(val value: String)
- 在代理中解析注解并生成对应log
fun <T> proxy(t: T, methodInterface: Class<T>) : T { return Proxy.newProxyInstance(t!!::class.java.classLoader, arrayOf(methodInterface), object : InvocationHandler{ override fun invoke(proxy: Any, method: Method, args: Array<out Any>): Any { val result = method.invoke(t, *args)?:Unit // 获取类方法而非接口方法,接口中并没有注解,日志实现应交给最终实现决定,所以接口中不应该有注解 val m = t!!::class.java.getMethod(method.name, *method.parameterTypes) if (m.isAnnotationPresent(Log::class.java)){ val annotation = m.getAnnotation(Log::class.java) // 获取方法注解 val annotationValue = StringBuilder(annotation.value) // 获取注解值 val reg = """#\{[0-9a-zA-Z_]+\}""".toRegex() val entitys = reg.findAll(annotationValue.toString()) // 获取所有需要被替换的字符串 for (entity in entitys) { // 遍历并替换为对应参数的值 val entityParam = entity.value.substring(2, entity.value.length - 1).trim() // 获取#{}中的值 for ((index,param) in m.parameters.withIndex()) { // 遍历方法参数 if (param.isAnnotationPresent(Param::class.java)) { val paramAnnotation = param.getAnnotation(Param::class.java) // 获取参数上的注解 if (paramAnnotation.value == (entityParam)) { // 对比参数注解值和#{}中的值是否匹配 val paramValue = args[index].toString() val replaceRegex = """#\{${entityParam}\}""".toRegex() // 替换字符串 val replace = annotationValue.replace(replaceRegex, paramValue) annotationValue.clear() annotationValue.append(replace) } } } } println("real log -> $annotationValue") } return result } }) as T }
- 使用
- 定义接口
interface IDemo{ fun test(action: String, code: Int) }
- 实现接口
class Demo : IDemo{ @Log("test action=#{action},code=#{code}") override fun test(@Param("action") action: String, @Param("code") code: Int) { println("action = [${action}], code = [${code}]") } }
- 调用
val demoProxy = proxy(Demo(), IDemo::class.java) demoProxy.test("Hello proxy log", 100)
- 效果
action = [Hello proxy log], code = [100] real log -> test action=Hello proxy log,code=100
- 定义接口
其它
- 在Android平台上,获取形参名称有些问题,所以这里使用了
Param
注解,如果有了解相关原理的同学可以底部留言。 - 文章中可能存在未发现的错误,发现错误的同学可以底部留言。
- 如果有更好的方案,欢迎大家指出。