Kotlin: Можно ли изменять функции во время компиляции с помощью метапрограммирования?

В динамических языках, таких как JavaScript / Python, можно перезаписывать или «изменять» функции во время выполнения. Например, чтобы изменить функцию alert в JS, можно сделать:

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

Это приведет к выводу "Функция предупреждения была вызвана!" на консоль каждый раз, когда вызывается функция 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?

Я читал о метапрограммировании Котлина с помощью аннотаций и отражений, так что, возможно, они могут контролировать поведение компилятора и перезаписывать код get_number? Или это полное безумие, и единственный способ сделать что-то в этом роде возможно - это разработать мою собственную отдельную оболочку для метапрограммирования над Kotlin?

Кроме того, чтобы уточнить, этот вопрос не о Kotlin-JS, и ответ (если он существует) должен быть применим к Kotlin-JVM или Native.


person Griffort    schedule 29.08.2018    source источник
comment
Немного неясно, какова здесь конечная цель. С точки зрения дизайна приложения почти всегда более желательно использовать соответствующий шаблон проектирования, чем начинать полагаться на такие вещи, как динамические прокси, отражение или AOP. Итак, можно ли это сделать? да. Следует ли это делать? Возможно нет.   -  person Robby Cornelissen    schedule 30.08.2018
comment
@RobbyCornelissen Его цель - портировать обычную расширяемую образовательную библиотеку разработки игр, которая использует аналогичный стиль метапрограммирования с JavaScript. Модификации в основном абстрактны, просто для ясности, они просто используются для быстрой и совместимой разработки расширений, которые не вмешиваются в исходный код. Это не какой-то причудливый вопрос о том, как избежать реализации / расширения класса или общей структуры, это просто любопытный вопрос о потенциале метапрограммирования Kotlin. Поскольку вы утверждаете, что это можно сделать, возможно, вы могли бы расширить это в ответе?   -  person Griffort    schedule 30.08.2018


Ответы (1)


Как указано в моем комментарии: почти во всех случаях более желательно использовать соответствующий шаблон проектирования, чем начинать полагаться на такие вещи, как динамические прокси, отражение или AOP для решения такой проблемы.

При этом возникает вопрос, возможно ли изменять функции Kotlin во время компиляции посредством метапрограммирования, и ответ - «Да». Для демонстрации ниже приведен полный пример, в котором используется AspectJ.


Структура проекта

Я создал небольшой проект на основе Maven со следующей структурой:

.
├── pom.xml
└── src
    └── main
        └── kotlin
            ├── Aop.kt
            └── Main.kt

Я воспроизведу содержимое всех файлов в разделах ниже.


Код приложения

Фактический код приложения находится в файле с именем Main.kt, и - за исключением того факта, что я переименовал вашу функцию, чтобы она соответствовала Правила именования Kotlin - они идентичны коду, приведенному в вашем вопросе. Метод getNumber() предназначен для возврата 3.

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

fun getNumber(): Int {
    return 3
}

Код АОП

Код, связанный с АОП, находится в 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
    }
}

(Обратите внимание, как имя сгенерированного класса для файла Main.kt MainKt.)


POM файл

Файл POM объединяет все воедино. Я использую 4 плагина:

  • kotlin-maven-plugin позаботится о компиляции файлов Kotline. Конфигурация включает выполнение подключаемого модуля kapt для обработки аннотаций AspectJ.
  • jcabi-maven-plugin выполняет AspectJ compiler / weaver, чтобы объединить аспекты в двоичные классы.
  • maven-jar-plugin создает файл JAR с манифестом, который ссылается на основной класс.
  • maven-shade-plugin создает 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

Это создаст толстый JAR в месте target/kotlin-aop-1.0-SNAPSHOT.jar. Затем этот JAR можно запустить с помощью команды java:

java -jar target/kotlin-aop-1.0-SNAPSHOT.jar

Затем выполнение дает нам следующий результат, демонстрирующий, что все работает, как ожидалось:

42

(Приложение было создано и выполнено с использованием самой последней версии Oracle Java 8 JDK на момент написания - 1.8.0_181)


Заключение

Как демонстрирует приведенный выше пример, определенно можно переопределить функции Kotlin, но, повторяя мою исходную точку зрения, почти во всех случаях существуют более элегантные решения, позволяющие достичь того, что вам нужно.

person Robby Cornelissen    schedule 30.08.2018
comment
Я полностью не согласен с вашим утверждением, что более желательно использовать соответствующий шаблон проектирования, чем начинать полагаться на такие вещи, как динамические прокси, отражение или АОП. На самом деле я утверждаю, что именно эти вещи составляют элегантное решение. Пожалуйста, просветите меня относительно других, более элегантных решений проблем, которые решает АОП. Например, как бы вы реализовали что-то вроде защиты доменного объекта без решения на основе АОП? Пример вашего решения сильно изменит мое мнение. - person Matthew Adams; 09.05.2019
comment
Кроме того, есть одно уточнение, которое я бы сделал в отношении вашего примера проекта, приведенного выше. Вы продемонстрировали, что можно сплетать двоичные файлы .class после, которые были скомпилированы компилятором Kotlin & kapt. Это отличается, скажем, от использования ajc для компиляции всего пакета за один раз, как вы можете это сделать в Java. Моя интерпретация вопроса ОП относится ко второму, а не к первому. Возможно, вопрос OP должен был быть чем-то вроде «Как мне скомпилировать мои файлы Kotlin с помощью компилятора AOP, такого как ajc, чтобы компиляция и создание выполнялись за один шаг?» или похожие. - person Matthew Adams; 09.05.2019