Kotlin:可以通过元编程在编译期间修改函数吗?
Kotlin: Possible to modify functions during compile time through metaprogramming?
在 JavaScript/Python 等动态语言中,可以在 运行 时间内覆盖或 "modify" 函数。例如,为了修改 JS 中的 alert
函数,可以这样做:
const _prev_alert = window.alert;
window.alert = function() {
_prev_alert.apply(this, arguments);
console.log("Alert function was called!");
}
这将在每次调用 alert
函数时向控制台输出 "Alert function was called!"。
现在,由于 Kotlin-JVM 或 Kotlin-Native 的静态特性,显然在 运行 时间里不可能有这样的事情发生。但是,对于那些相同的语言,是否有可能在编译期间修改非编译函数?我指的不是来自库的预编译函数,而是我在正在开发的同一个项目中编写的函数。
例如,假设我有一个我编写的名为 get_number
的函数。我可以将 get_number
修改为 return 一个不同的数字而不改变它在 main 中的调用方式并且不直接修改它的代码吗? (或者有什么方法可以让我写出原来的 get_number
以便可以修改吗?)
fun main(args: Array<String>) {
println(get_number())
}
fun get_number(): Int {
return 3
}
// Without modifying the code above, can I get main to print something besides 3?
我一直在阅读 Kotlin 的注释和反射元编程,所以也许它们可以控制编译器的行为并覆盖 get_number
的代码?或者这完全是疯了,唯一可能实现这种性质的方法是通过开发我自己的、独立的、基于 Kotlin 的元编程包装器?
此外,再次澄清一下,这个问题与 Kotlin-JS 无关,答案(如果存在)应该适用于 Kotlin-JVM 或 Native。
如我的评论所述:在几乎所有情况下,使用适当的设计模式比开始依赖 dynamic proxies, reflection, or AOP 来解决此类问题更可取。
话虽如此,问题是可能通过元编程在编译时修改Kotlin函数,答案是"Yes"。为了演示,下面是一个使用 AspectJ.
的完整示例
项目结构
我建立了一个基于 Maven 的小型项目,结构如下:
.
├── pom.xml
└── src
└── main
└── kotlin
├── Aop.kt
└── Main.kt
我将在以下部分中复制所有文件的内容。
申请代码
实际的应用程序代码在名为 Main.kt
的文件中,并且——除了我将您的函数重命名为符合 Kotlin naming rules—它与您问题中提供的代码相同。 getNumber()
方法设计为return3.
fun main(args: Array<String>) {
println(getNumber())
}
fun getNumber(): Int {
return 3
}
AOP代码
AOP相关的代码在Aop.kt
,非常简单。它有一个 @Around
建议,带有与 getNumber()
函数的执行相匹配的切点。该建议将拦截对 getNumber()
方法和 return 42(而不是 3)的调用。
import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.annotation.Aspect
@Aspect
class Aop {
@Around("execution(* MainKt.getNumber(..))")
fun getRealNumber(joinPoint: ProceedingJoinPoint): Any {
return 42
}
}
(注意 Main.kt
文件生成的 class 的名称是 MainKt
。)
POM 文件
POM 文件将所有内容放在一起。我正在使用 4 个插件:
- 用于处理 AspectJ 注释的
kotlin-maven-plugin
takes care of compiling the Kotline files. The configuration includes the execution of the kapt 插件。
jcabi-maven-plugin
executes the AspectJ compiler/weaver 将方面编织成二进制 classes。
maven-jar-plugin
使用引用主要 class 的清单构建 JAR 文件。
- 包含所有库依赖项的
maven-shade-plugin
builds a fat JAR。
这是完整的 POM 文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>x.y.z</groupId>
<artifactId>kotlin-aop</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<kotlin.version>1.2.61</kotlin.version>
<aspectj.version>1.9.1</aspectj.version>
</properties>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${aspectj.version}</version>
</dependency>
</dependencies>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<plugins>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>kapt</id>
<goals>
<goal>kapt</goal>
</goals>
</execution>
<execution>
<id>compile</id>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.jcabi</groupId>
<artifactId>jcabi-maven-plugin</artifactId>
<version>0.14.1</version>
<executions>
<execution>
<goals>
<goal>ajc</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>MainKt</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
构建和执行
要构建,与任何 Maven 项目一样,您只需要 运行:
mvn clean package
这将在 target/kotlin-aop-1.0-SNAPSHOT.jar
位置构建一个胖 JAR。然后可以使用 java
命令执行此 JAR:
java -jar target/kotlin-aop-1.0-SNAPSHOT.jar
执行然后给我们以下结果,证明一切都按预期工作:
42
(在撰写本文时,应用程序是使用最新的 Oracle Java 8 JDK 构建和执行的—1.8.0_181)
结论
如上面的示例所示,重新定义 Kotlin 函数当然是可能的,但是——重申我原来的观点——在几乎所有情况下,都有更优雅的解决方案可以满足您的需求。
在 JavaScript/Python 等动态语言中,可以在 运行 时间内覆盖或 "modify" 函数。例如,为了修改 JS 中的 alert
函数,可以这样做:
const _prev_alert = window.alert;
window.alert = function() {
_prev_alert.apply(this, arguments);
console.log("Alert function was called!");
}
这将在每次调用 alert
函数时向控制台输出 "Alert function was called!"。
现在,由于 Kotlin-JVM 或 Kotlin-Native 的静态特性,显然在 运行 时间里不可能有这样的事情发生。但是,对于那些相同的语言,是否有可能在编译期间修改非编译函数?我指的不是来自库的预编译函数,而是我在正在开发的同一个项目中编写的函数。
例如,假设我有一个我编写的名为 get_number
的函数。我可以将 get_number
修改为 return 一个不同的数字而不改变它在 main 中的调用方式并且不直接修改它的代码吗? (或者有什么方法可以让我写出原来的 get_number
以便可以修改吗?)
fun main(args: Array<String>) {
println(get_number())
}
fun get_number(): Int {
return 3
}
// Without modifying the code above, can I get main to print something besides 3?
我一直在阅读 Kotlin 的注释和反射元编程,所以也许它们可以控制编译器的行为并覆盖 get_number
的代码?或者这完全是疯了,唯一可能实现这种性质的方法是通过开发我自己的、独立的、基于 Kotlin 的元编程包装器?
此外,再次澄清一下,这个问题与 Kotlin-JS 无关,答案(如果存在)应该适用于 Kotlin-JVM 或 Native。
如我的评论所述:在几乎所有情况下,使用适当的设计模式比开始依赖 dynamic proxies, reflection, or AOP 来解决此类问题更可取。
话虽如此,问题是可能通过元编程在编译时修改Kotlin函数,答案是"Yes"。为了演示,下面是一个使用 AspectJ.
的完整示例项目结构
我建立了一个基于 Maven 的小型项目,结构如下:
.
├── pom.xml
└── src
└── main
└── kotlin
├── Aop.kt
└── Main.kt
我将在以下部分中复制所有文件的内容。
申请代码
实际的应用程序代码在名为 Main.kt
的文件中,并且——除了我将您的函数重命名为符合 Kotlin naming rules—它与您问题中提供的代码相同。 getNumber()
方法设计为return3.
fun main(args: Array<String>) {
println(getNumber())
}
fun getNumber(): Int {
return 3
}
AOP代码
AOP相关的代码在Aop.kt
,非常简单。它有一个 @Around
建议,带有与 getNumber()
函数的执行相匹配的切点。该建议将拦截对 getNumber()
方法和 return 42(而不是 3)的调用。
import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.annotation.Aspect
@Aspect
class Aop {
@Around("execution(* MainKt.getNumber(..))")
fun getRealNumber(joinPoint: ProceedingJoinPoint): Any {
return 42
}
}
(注意 Main.kt
文件生成的 class 的名称是 MainKt
。)
POM 文件
POM 文件将所有内容放在一起。我正在使用 4 个插件:
- 用于处理 AspectJ 注释的
kotlin-maven-plugin
takes care of compiling the Kotline files. The configuration includes the execution of the kapt 插件。 jcabi-maven-plugin
executes the AspectJ compiler/weaver 将方面编织成二进制 classes。maven-jar-plugin
使用引用主要 class 的清单构建 JAR 文件。- 包含所有库依赖项的
maven-shade-plugin
builds a fat JAR。
这是完整的 POM 文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>x.y.z</groupId>
<artifactId>kotlin-aop</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<kotlin.version>1.2.61</kotlin.version>
<aspectj.version>1.9.1</aspectj.version>
</properties>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${aspectj.version}</version>
</dependency>
</dependencies>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<plugins>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>kapt</id>
<goals>
<goal>kapt</goal>
</goals>
</execution>
<execution>
<id>compile</id>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.jcabi</groupId>
<artifactId>jcabi-maven-plugin</artifactId>
<version>0.14.1</version>
<executions>
<execution>
<goals>
<goal>ajc</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>MainKt</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
构建和执行
要构建,与任何 Maven 项目一样,您只需要 运行:
mvn clean package
这将在 target/kotlin-aop-1.0-SNAPSHOT.jar
位置构建一个胖 JAR。然后可以使用 java
命令执行此 JAR:
java -jar target/kotlin-aop-1.0-SNAPSHOT.jar
执行然后给我们以下结果,证明一切都按预期工作:
42
(在撰写本文时,应用程序是使用最新的 Oracle Java 8 JDK 构建和执行的—1.8.0_181)
结论
如上面的示例所示,重新定义 Kotlin 函数当然是可能的,但是——重申我原来的观点——在几乎所有情况下,都有更优雅的解决方案可以满足您的需求。