Kotlin中为一组方法添加装饰(如日志)的惯用实现方式
问题背景
我是一名正在熟悉Java/Kotlin静态编译环境的Python开发者,目前遇到一个问题:我有一个WebServiceClient类,提供了多个与服务交互的方法,示例代码如下:
class WebServiceClient { fun createUser(name: String, age: Int) { // 业务实现 } fun deleteUser(id: Int) { // 业务实现 } fun updateUser(id: Int, name: String, age: Int) { // 业务实现 } // 更多其他方法 }
我希望为其中部分方法添加日志功能。最直接的方式是逐个修改方法:
... fun createUser(name: String, age: Int) { log.info("执行前:createUser") // 业务实现 log.info("执行后:createUser") } fun deleteUser(id: Int) { log.info("执行前:deleteUser") // 业务实现 log.info("执行后:deleteUser") } ...
但这种方式会引入重复代码、样板代码,还会让方法的业务逻辑与无关代码混杂(违背AOP原则)。在Python中用装饰器很容易实现,但我不知道如何在Kotlin中合理实现,既避免重复代码,又能将日志逻辑与方法业务逻辑分离。
我尝试用高阶函数实现类似逻辑,示例代码如下:
class WebServiceClient { fun createUser(name: String, age: Int) { } ... private fun <T> decorator(action: (vararg args: Any) -> T, vararg args: Any) : T { log.info("before action") val result = action(*args) log.info("after action") return result } }
但出现了多个编译错误:
- Unsupported [modifier on parameter in function type] - 针对action类型定义中的
vararg - *The spread operator (foo) may only be applied in a vararg position - 针对调用
action(*args)
要么是Kotlin无法定义这类泛型函数,要么是我不了解如何正确使用Kotlin类型系统。而且这可能根本不是Kotlin的惯用方式。
我还没深入研究注解和反射,因为这看起来过于复杂(对比Python的实现方式),但可能这是唯一正确的方向。
请问在Kotlin中,如何在避免代码重复、遵循AOP原则的前提下解决这个问题?
解决方案
1. 高阶函数的正确实现(Kotlin惯用方式)
Kotlin的函数类型不支持直接在参数里写vararg,但可以通过lambda捕获参数的方式实现,既符合类型系统规则,又能封装通用日志逻辑:
import org.slf4j.LoggerFactory class WebServiceClient { private val log = LoggerFactory.getLogger(javaClass) // 通用日志包装函数,接收方法名和无参lambda(参数由外部lambda捕获) private inline fun <T> withLogging(actionName: String, action: () -> T): T { log.info("执行前:{}", actionName) val result = action() log.info("执行后:{}", actionName) return result } // 业务方法通过lambda传递逻辑,参数自动捕获 fun createUser(name: String, age: Int) = withLogging("createUser") { // 原业务逻辑 println("创建用户:$name,年龄$age") } fun deleteUser(id: Int) = withLogging("deleteUser") { // 原业务逻辑 println("删除用户ID:$id") } fun updateUser(id: Int, name: String, age: Int) = withLogging("updateUser") { // 原业务逻辑 println("更新用户ID:$id,新名称$name,新年龄$age") } }
这种方式的优势:
- 日志逻辑完全封装,无重复代码
- 业务方法只关注核心逻辑,与日志代码彻底分离
inline关键字消除lambda的性能开销,执行效率接近直接编写代码
2. 注解+反射实现(适合批量标记方法)
如果需要给大量方法统一加日志,且不想修改业务方法的写法,可以用注解+反射实现“标记式”日志,类似Python装饰器的用法:
第一步:定义日志注解
@Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) annotation class Loggable
第二步:封装反射代理工具
import org.slf4j.LoggerFactory import java.lang.reflect.InvocationHandler import java.lang.reflect.Method import java.lang.reflect.Proxy object LoggingProxy { fun <T> createProxy(target: T): T { val log = LoggerFactory.getLogger(target.javaClass) return Proxy.newProxyInstance( target.javaClass.classLoader, target.javaClass.interfaces, object : InvocationHandler { override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any? { // 仅对标记了@Loggable的方法执行日志逻辑 method?.takeIf { it.isAnnotationPresent(Loggable::class.java) }?.let { log.info("执行前:{},参数:{}", it.name, args?.contentToString()) } val result = method?.invoke(target, *(args ?: emptyArray())) method?.takeIf { it.isAnnotationPresent(Loggable::class.java) }?.let { log.info("执行后:{},返回值:{}", it.name, result) } return result } } ) as T } }
第三步:改造客户端类(需实现接口)
// 定义业务接口 interface WebService { fun createUser(name: String, age: Int) fun deleteUser(id: Int) fun updateUser(id: Int, name: String, age: Int) } // 实现类 class WebServiceClientImpl : WebService { override fun createUser(name: String, age: Int) { println("创建用户:$name,年龄$age") } override fun deleteUser(id: Int) { println("删除用户ID:$id") } @Loggable // 仅给需要日志的方法标记注解 override fun updateUser(id: Int, name: String, age: Int) { println("更新用户ID:$id,新名称$name,新年龄$age") } } // 使用示例 fun main() { val client = LoggingProxy.createProxy(WebServiceClientImpl()) client.updateUser(1, "张三", 25) // 会打印日志 client.createUser("李四", 30) // 不会打印日志 }
注意事项:
- 目标类必须实现接口,因为Kotlin动态代理仅支持基于接口的代理
- 反射存在一定性能开销,高频调用场景优先选择高阶函数方案
3. Spring AOP(Spring生态下最优解)
如果项目基于Spring框架,Spring AOP是最省心的方案,完全无需修改业务代码,通过切面批量织入日志逻辑:
定义切面类
import org.aspectj.lang.JoinPoint import org.aspectj.lang.annotation.AfterReturning import org.aspectj.lang.annotation.Aspect import org.aspectj.lang.annotation.Before import org.slf4j.LoggerFactory import org.springframework.stereotype.Component @Aspect @Component class WebServiceLoggingAspect { private val log = LoggerFactory.getLogger(javaClass) // 匹配所有标记了@Loggable注解的方法 @Before("@annotation(com.yourpackage.Loggable)") fun beforeMethod(joinPoint: JoinPoint) { log.info("执行前:{},参数:{}", joinPoint.signature.name, joinPoint.args.contentToString()) } @AfterReturning(value = "@annotation(com.yourpackage.Loggable)", returning = "result") fun afterMethod(joinPoint: JoinPoint, result: Any?) { log.info("执行后:{},返回值:{}", joinPoint.signature.name, result) } }
之后只需给需要日志的方法添加@Loggable注解,Spring会自动完成日志逻辑的织入,完全符合AOP的解耦思想。
内容的提问来源于stack exchange,提问作者sunprophit

