Что вы можете сделать с помощью 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 { public void greet() { println "Hello from the greet() method!" } }
Во время компиляции эта аннотация синтезирует правильный метод main и запускает тот же код, что и greet (). (Мы будем беспокоиться о вызове метода экземпляра из статического метода позже). Преобразование этой аннотации в AST означает, что и Java, и Groovy увидят метод main (). Давай сделаем это; Есть три вещи, которые вам нужны:
- Интерфейс маркера @Main
- Внедрение ASTTransformation вносит изменения в AST
- Испытательный жгут (поверьте, вы хотите этого)
@Main интерфейс маркера является простым куском. Интерфейс маркера используется для стандартной аннотации Java 5 для реализации ASTTransformation :
RetentionPolicy.SOURCE) ([ElementType.METHOD]) (["MainTransformation"])public @interface Main { } (
Это просто стандартная аннотация Java 5. Политика хранения — SOURCE, поэтому другие объекты не видят аннотации во время выполнения при использовании отражения (в этом нет необходимости). ElementType является МЕТОДОМ, потому что мы только хотим применить аннотацию к методам. А GroovyASTTransformationClass — это «ГлавнаяТрансформация». Это значение является полностью определенным именем класса реализации ASTTransformation, которое Groovy будет вызывать при обнаружении аннотации @Main. На практике вам нужно будет указать пакет … Я остановил его здесь для простоты.
Таким образом, эта аннотация соединит @Main с классом MainTransformation . Полагаю, нам лучше сделать этот класс …
Интерфейс ASTTransformation определяет один метод:
void visit(ASTNode[], SourceUnit)
Лучший способ понять содержание этих двух параметров — написать несколько тестов и проверить их в отладчике, а затем использовать браузер AST для анализа синтаксического дерева. Как правило, массив ASTNode содержит то, что вы хотите манипулировать, а SourceUnit предоставляет такие сервисы, как отчеты об ошибках и предупреждениях. Вот скелет нашего MainTransformation, который собирается добавить синтезированный метод main () в класс:
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 и понеслось.