Kotlin:可以通过元编程在编译期间修改函数吗?

Gri*_*ort 5 overloading self-modifying kotlin kotlin-extension

在JavaScript/Python等动态语言中,可以在运行时覆盖或"修改"函数.例如,为了修改alertJS中的函数,可以这样做:

const _prev_alert = window.alert;
window.alert = function() {
  _prev_alert.apply(this, arguments);
  console.log("Alert function was called!");
}
Run Code Online (Sandbox Code Playgroud)

这将输出"调用警报功能!" 每次alert调用函数时都到控制台.

现在,显然这样的事情在Kotlin-JVM或Kotlin-Native的运行时期间是不可能的,因为它们具有静态特性.但是,对于那些相同的语言,是否有可能在编译期间修改未编译的函数?我不是指来自库的预编译函数,而是我在我正在开发的同一个项目中编写的函数.

例如,假设我有一个我写的函数get_number.我可以修改get_number为返回一个不同的数字而不改变它在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?
Run Code Online (Sandbox Code Playgroud)

我一直在阅读Kotlin的Annotations and Reflections元编程,所以也许那些可以控制编译器的行为并覆盖get_number代码?或者这是完全的疯狂,这种性质的唯一方法是通过在Kotlin上开发我自己的,独立的元编程包装器?

此外,只是为了澄清,这个问题不是关于Kotlin-JS,答案(如果存在)应该适用于Kotlin-JVM或Native.

Rob*_*sen 7

正如我的评论中所述:在几乎所有情况下,使用适当的设计模式比开始依赖动态代理,反射或AOP之类的东西来解决这类问题更为可取.

话虽如此,问题是询问是否可以通过元编程在编译时修改Kotlin函数,答案是"是".为了演示,下面是一个使用AspectJ的完整示例.


项目结构

我建立了一个基于Maven的小项目,具有以下结构:

.
??? pom.xml
??? src
    ??? main
        ??? kotlin
            ??? Aop.kt
            ??? Main.kt
Run Code Online (Sandbox Code Playgroud)

我将在下面的部分中重现所有文件的内容.


应用代码

实际的应用程序代码位于名为的文件中Main.kt,除了我将您的函数重命名为符合Kotlin命名规则的事实- 这与您的问题中提供的代码完全相同.该getNumber()方法旨在返回3.

fun main(args: Array<String>) {
    println(getNumber())
}

fun getNumber(): Int {
    return 3
}
Run Code Online (Sandbox Code Playgroud)

AOP代码

与AOP相关的代码在Aop.kt,并且非常简单.它有一个@Around带有与getNumber()函数执行相匹配的切点的建议.该建议将拦截对getNumber()方法的调用并返回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
    }
}
Run Code Online (Sandbox Code Playgroud)

(注意Main.kt文件生成的类的名称是如何的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>
Run Code Online (Sandbox Code Playgroud)

建立和执行

要像任何Maven项目一样构建,您只需要运行:

mvn clean package
Run Code Online (Sandbox Code Playgroud)

这将在该target/kotlin-aop-1.0-SNAPSHOT.jar位置建立一个胖JAR .然后可以使用以下java命令执行此JAR :

java -jar target/kotlin-aop-1.0-SNAPSHOT.jar
Run Code Online (Sandbox Code Playgroud)

执行然后给我们以下结果,证明一切按预期工作:

42

(在编写1.8.0_181时,使用最新的Oracle Java 8 JDK构建并执行了应用程序)


结论

正如上面的例子所示,重新定义Kotlin函数当然是可能的,但是 - 重申我原来的观点 - 在几乎所有情况下,都有更优雅的解决方案可用于实现您的需求.

  • 我完全不同意您的说法,即“使用适当的设计模式比开始依赖动态代理、反射或 AOP 之类的东西更可取”。事实上,我认为正是这些东西构成了一个优雅的解决方案。请教我关于 AOP 解决的问题的其他“更优雅的解决方案”。例如,如果没有基于 AOP 的解决方案,您将如何实现域对象安全性之类的东西?你的解决方案的一个例子将大大改变我的观点。 (3认同)
  • 另外,我要对上面给出的示例项目进行澄清。您已经证明可以_编织_二进制 .class 文件 _after_ 它们已由 Kotlin 编译器和 kapt 编译。这与使用 ajc 一次性编译整个批次不同,就像在 Java 中一样。我对 OP 问题的解释是指后者,而不是前者。也许 OP 的问题应该类似于“我如何使用像 ajc 这样的 AOP 编译器编译我的 Kotlin 文件,以便在一个步骤中完成编译和编织?” 或类似。 (2认同)