Статьи

Groovy AST преобразования на примере: добавление методов в классы

Что вы можете сделать с помощью Groovy AST Transformation ? Сложный вопрос, учитывая ответ «почти все». Одно из часто используемых преобразований AST — записывать изменения Groovy в файлы JVM .class, чтобы о них знали стандартные инструменты Java. Например, изменения байт-кода, произведенные @Delegate , видны и могут быть использованы из простых старых классов Java. В этом примере вы узнаете, как добавить синтезированные методы в класс во время компиляции, используя Groovy AST Transformation и AST Builder. Это интересный тестовый пример, поскольку в нем рассматриваются некоторые крайние случаи и общие подводные камни. Это должно быть весело … 

(А для тех из вас, кто хочет пропустить все это и просто посмотреть код, посмотрите последний пример Groovy в SVN и просто запустите скрипт Ant).

svn co http://svn.codehaus.org/groovy/trunk/groovy/groovy-core/src/examples/astbuilder

Для начала давайте лучше определимся с нашим примером. Рассмотрим класс с открытым API, а также стандартный метод main (). Часто метод main () просто создает экземпляр класса и вызывает один метод. Разве не было бы хорошо аннотировать метод так, чтобы он рассматривался как основной метод? Что если бы это было возможно:

class MainExample {   @Main   public void greet() {       println "Hello from the greet() method!"   }   }

Во время компиляции эта аннотация синтезирует правильный метод main и запускает тот же код, что и greet (). (Мы будем беспокоиться о вызове метода экземпляра из статического метода позже). Преобразование этой аннотации в AST означает, что и Java, и Groovy увидят метод main (). Давай сделаем это; Есть три вещи, которые вам нужны:

  1. Интерфейс маркера @Main
  2. Внедрение ASTTransformation вносит изменения в AST
  3. Испытательный жгут (поверьте, вы хотите этого)

@Main интерфейс маркера является простым куском. Интерфейс маркера используется для стандартной аннотации Java 5 для реализации ASTTransformation :

@Retention (RetentionPolicy.SOURCE)@Target ([ElementType.METHOD])@GroovyASTTransformationClass (["MainTransformation"])public @interface Main {  }

Это просто стандартная аннотация Java 5. Политика хранения — SOURCE, поэтому другие объекты не видят аннотации во время выполнения при использовании отражения (в этом нет необходимости). ElementType является МЕТОДОМ, потому что мы только хотим применить аннотацию к методам. А GroovyASTTransformationClass — это «ГлавнаяТрансформация». Это значение является полностью определенным именем класса реализации ASTTransformation, которое Groovy будет вызывать при обнаружении аннотации @Main. На практике вам нужно будет указать пакет … Я остановил его здесь для простоты.

Таким образом, эта аннотация соединит @Main с классом MainTransformation . Полагаю, нам лучше сделать этот класс …

Интерфейс ASTTransformation определяет один метод:

void visit(ASTNode[], SourceUnit)

Лучший способ понять содержание этих двух параметров — написать несколько тестов и проверить их в отладчике, а затем использовать браузер AST для анализа синтаксического дерева. Как правило, массив ASTNode содержит то, что вы хотите манипулировать, а SourceUnit предоставляет такие сервисы, как отчеты об ошибках и предупреждениях. Вот скелет нашего MainTransformation, который собирается добавить синтезированный метод main () в класс:

@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)public class MainTransformation implements ASTTransformation {     void visit(ASTNode[] astNodes, SourceUnit sourceUnit) {       if (!astNodes) return       if (!astNodes[0]) return       if (!astNodes[1]) return       if (!(astNodes[0] instanceof AnnotationNode)) return       if (astNodes[0].classNode?.name != Main.class.getName()) return       if (!(astNodes[1] instanceof MethodNode)) return       MethodNode annotatedMethod = astNodes[1]       ClassNode declaringClass = astNodes[1].declaringClass       // makeMainMethod synthesizes a method!       MethodNode mainMethod = makeMainMethod(annotatedMethod)       declaringClass.addMethod(mainMethod)   }   ...

Опять же, здесь есть несколько интересных вещей, на которые стоит обратить внимание …

  • Фаза компилятора была выбрана INSTRUCTION_SELECTION, что было совершенно произвольным решением. Пожалуйста, если вы хотите знать, на какую фазу ориентироваться, спросите в списке рассылки groovy-user (и запрос даст нам лучшие подсказки относительно того, что нам нужно документировать в вики).
  • Тело преобразований содержит множество защитных предложений в качестве защиты от исключений NullPointerException и неожиданного ввода. AST классов меняется от версии к версии. Настоятельно рекомендуется защищенное программирование и тщательные тесты.
  • Аннотированный MethodNode входит в массив ASTNode, и оттуда вы можете перемещаться в любом месте дерева синтаксиса.
  • MakeMainMethod собирается скопировать тело аннотированного метода, а затем мы добавим этот метод обратно в объявленный класс.

Последнее, что нужно сделать, это определить makeMainMethod, который на самом деле создает main ():

MethodNode makeMainMethod(MethodNode source) {   def ast = new AstBuilder().buildFromSpec {       method('main', ACC_PUBLIC | ACC_STATIC, Void.TYPE) {           parameters {               parameter 'args': String[].class           }           exceptions {}           block { }       }   }   MethodNode newMainMethod = ast[0]   newMainMethod.code = source.code   newMainMethod}

Опять же, несколько кусочков, чтобы отметить:

  • AstBuilder.buildFromSpec — удобство DSL по сравнению с конкретными конструкторами ASTNode и его подклассов. Таким образом, API buildFromSpec отражает API классов ASTNode. Если вы хотите понять аргументы и типы параметров, посмотрите на Javadoc ASTNode. В этом отношении buildFromSpec больше предназначен для того, чтобы сделать код более легким для чтения, нежели простым для написания.
  • Тип возвращаемого значения метода Void.TYPE и notVoid.class . Void.class — это реальный класс с реальным использованием, Void.TYPE — это символ «маленький ve void».
  • Модификаторы — ACC_PUBLIC | ACC_STATIC. Обратите внимание на использование | а не &. Позвольте мне кратко отредактировать: я не понимал битовые маски, я никогда не понимал их, и люди, которые разрабатывают API-интерфейсы вокруг параметров, типизированных как «int modifers», будут первыми против стены, когда придет революция. Если ноль был ошибкой в миллиард долларов, то битовые маски, по крайней мере, заслуживают того, чтобы быть в диапазоне десятков миллионов. Так или иначе…
  • Кодовый блок нового метода main — это просто ссылка на кодовый блок метода, помеченный @Main. Это приведет к тому, что дублирующийся байт-код будет записан в файл .class (плохо), но для небольшого, ясного примера (хорошо). В производственной среде вы, вероятно, захотите вызвать метод @Main вместо копирования тела.

Теперь, как вы можете видеть из выходных данных javap, скомпилированный MainExample.groovy содержит настоящий живой основной класс Java:

hdarcy:$ groovyc MainExample.groovyhdarcy:$ javap MainExampleCompiled from "MainExample.groovy"public class MainExample extends java.lang.Object implements groovy.lang.GroovyObject{   ...   public void greet();   ...   public static void main(java.lang.String[]);   ...}

Уф! Это было много информации. Последнее, что нужно добавить, это простой тестовый пример с использованием класса TranformTestHelper в Groovy. Отладчик очень помогает в разработке AST Transformation, и этот интеграционный тест — один из способов подключить его к вашей трансформации. Я проверил это в IDEA, но оно должно работать и в Eclipse / Netbeans:

class MainIntegrationTest extends GroovyTestCase {    public void testInvokeUnitTest() {       def file = new File('./MainExample.groovy')       assert file.exists()       def invoker = new TranformTestHelper(new MainTransformation(), CompilePhase.CANONICALIZATION)       def clazz = invoker.parse(file)       def tester = clazz.newInstance()       tester.main(null)       // main method added with AST transform   }}

Этот тестовый пример принимает пример класса как простой файл, а затем компилирует его с помощью определенной нами MainTransformation. Это отличный способ вызвать как локальные, так и глобальные преобразования из модульных тестов. Я уверен, что есть простой способ вызвать статический метод main без создания экземпляра класса, но я устал и мне все равно. Это просто пример!

И это все … мы создали интерфейс маркера для запуска аннотации, создали ASTTransformation для добавления метода в класс и написали тестовый набор. Есть только два свободных конца:

Как статический метод main может вызывать нестатический метод, такой как Greet? Мы, это не может на самом деле. Для этого примера мы просто скопировали тело метода в основной метод. На практике вы хотите убедиться, что аннотируются только статические методы.

Как обрабатываются внутренние классы? Краткий ответ: не так. У вас есть тестовый пример, так что просто напишите метод тестирования и узнайте!

Весь код был проверен в папке Groovy examples вместе со скриптом Ant. Нет лучшего времени, чтобы получить источник:

svn co http://svn.codehaus.org/groovy/trunk/groovy/groovy-core/src/examples/astbuilder

cd examples / astbuilder и понеслось.

С http://hamletdarcy.blogspot.com/