使用注解和动态代理实现方法级别的日志

前言

此文的主要目的是记录注解和动态代理的使用,因为已有很多日志框架;此文介绍了注解和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
      

其它

  1. 在Android平台上,获取形参名称有些问题,所以这里使用了Param注解,如果有了解相关原理的同学可以底部留言。
  2. 文章中可能存在未发现的错误,发现错误的同学可以底部留言。
  3. 如果有更好的方案,欢迎大家指出。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值