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 个插件:

这是完整的 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 函数当然是可能的,但是——重申我原来的观点——在几乎所有情况下,都有更优雅的解决方案可以满足您的需求。