Я начал играть с преобразованиями AST в Groovy несколько дней назад ( обзор главы о преобразованиях AST (GINA2) ). Будучи настолько взволнован этой технологией, я решил написать несколько постов (пошаговых руководств), описывающих, как добавить метапрограммирование времени компиляции в ваш повседневный арсенал.
В этом посте я собираюсь продемонстрировать, как написать локальное преобразование AST. Преобразования, которые я собираюсь написать, немного упрощены, но они действительно работают, и не будет проблемой добавить больше функциональности поверх них.
Быть убежденным в том, что код говорит лучше, чем слова, — это простой тест Спока:
class ExecuteOnceTransformationTest extends Specification{ def 'should execute a method only once'() { when: def testObject = new GroovyShell().evaluate(''' class TestClass { int counter = 0 @victorsavkin.sample1.ExecuteOnce void executeOnceMethod(){ counter ++ } } new TestClass() ''') then: testObject.counter == 0 when: testObject.executeOnceMethod() then: testObject.counter == 1 when: testObject.executeOnceMethod() then: testObject.counter == 1 } def 'should generate a compilation error if return type is not void'() { when: new GroovyShell().evaluate(''' class TestClass { @victorsavkin.sample1.ExecuteOnce def executeOnceMethod(){ 'ReturnValue' } } new TestClass() ''') then: def e = thrown(MultipleCompilationErrorsException) e.errorCollector.errorCount == 1 def firstError = e.errorCollector.errors.first() firstError.cause.message.startsWith('ExecuteOnce can be applied only to methods returning void') == true } }
Как вы можете видеть, преобразование, которое я собираюсь написать, может быть применено к методам, и оно изменит AST, чтобы гарантировать, что метод будет выполнен только один раз. Также это преобразование может быть применено только к методам, возвращающим void. Пример из реальной жизни, где его можно использовать: вы можете аннотировать ваш метод init с помощью аннотации @ExecuteOnce, а затем вызывать его во всех ваших методах. Инициализация будет выполнена только один раз. Когда наш тест готов, мы можем приступить к реализации всех частей.
Создать аннотацию
@Retention(RetentionPolicy.SOURCE) @Target([ElementType.METHOD]) @GroovyASTTransformationClass(['victorsavkin.sample1.ExecuteOnceTransformation']) @interface ExecuteOnce { }
Поскольку нам нужно только представить нашу аннотацию во время компиляции, мы используем политику хранения SOURCE. Нетрудно понять, что @GroovyASTTransformationClass определяет класс, который будет использоваться для выполнения нашего преобразования.
Создать класс, реализующий ASTTransformation
@GroovyASTTransformation(phase = SEMANTIC_ANALYSIS) class ExecuteOnceTransformation implements ASTTransformation { ... void visit(ASTNode[] astNodes, SourceUnit sourceUnit) { ... } }
SEMANTIC_ANALYSIS — это самая ранняя фаза, которую можно использовать для локального преобразования. Чем раньше ваш этап, тем меньше деталей реализации у вас есть в вашем AST. Так что если вы хотите манипулировать Groovy-кодом, вы должны использовать ранние фазы. Если вы хотите манипулировать сгенерированным кодом Java (включая сгенерированные методы получения и установки), возможно, лучшим выбором будет INSTRUCTION_SELECTION.
ASTTransformation — очень запутанный интерфейс, так как он используется как локальными, так и глобальными преобразованиями и используется по-разному. В этом примере мы будем использовать astNodes для манипулирования AST и sourceUnit для добавления ошибок компиляции.
Создайте объект спецификации для проверки объекта astNodes.
class ExecuteOnceTransformationSpecification { boolean isRightReturnType(astNodes){...} SyntaxException getSyntaxExceptionForInvalidReturnType(astNodes){...} boolean shouldSkipTransformation(astNodes){...} }
Чтобы сделать этот пост короче, я не включил код ExecuteOnceTransformationSpecification. Вы можете найти весь исходный код в репозитории github, который я создал для этого поста.
Создать AstFactory для создания частей АСТ
Создание фрагментов AST выглядит для меня немного как черная магия, поэтому я думаю, что было бы неплохо изолировать его внутри этой фабрики.
class ExecuteOnceAstFactory {
def createGuardIfStatement(fieldName) {
def ast = new AstBuilder().buildFromString SEMANTIC_ANALYSIS, true, """
if(! ${fieldName}){
${fieldName} = true
}
"""
ast[0].statements[0]
}
def generatePrivateFieldNode(fieldName) {
def ast = new AstBuilder().buildFromString SEMANTIC_ANALYSIS, false, """
class ${fieldName}_Class {
private boolean ${fieldName} = false
}
"""
ast[1].fields.find{it.name == fieldName}
}
}
Как вы можете видеть, я использую AstBuilder.buildFromString для создания фрагментов AST. Есть много способов создать AST, но buildFromString для меня пока выглядит самым простым. Я использую одну и ту же фазу компиляции SEMANTIC_ANALYSIS для обеих частей. Не волнуйтесь, если не поняли, что делает этот кусок кода. Я бы посоветовал вам запускать тесты с помощью отладчика и просто посмотреть, что будет генерироваться в каждом случае. После нескольких попыток понимание придет к вам.
Соберите все части вместе, чтобы закончить преобразование
@GroovyASTTransformation(phase = SEMANTIC_ANALYSIS) class ExecuteOnceTransformation implements ASTTransformation { private specification = new ExecuteOnceTransformationSpecification() private astFactory = new ExecuteOnceAstFactory() void visit(ASTNode[] astNodes, SourceUnit sourceUnit) { if(specification.shouldSkipTransformation(astNodes)) return if(!specification.isRightReturnType(astNodes)){ sourceUnit.addError specification.getSyntaxExceptionForInvalidReturnType(astNodes) return } MethodNode method = astNodes[1] def fieldName = createFieldName(method.name) addGuardFieldToClass fieldName, method addGuardIfStatementToMethod fieldName, method } private addGuardFieldToClass(fieldName, method) { def field = astFactory.generatePrivateFieldNode(fieldName) method.declaringClass.addField field } private addGuardIfStatementToMethod(fieldName, method) { def guardStatement = astFactory.createGuardIfStatement(fieldName) addAllStatementsOfMethodIntoGuardIf(guardStatement, method) method.code = guardStatement } private addAllStatementsOfMethodIntoGuardIf(guardIfStatement, method) { BlockStatement ifBlock = guardIfStatement.ifBlock ifBlock.statements.addAll(method.code.statements) } private createFieldName(methodName) { '$_victorsavkin_samples_execute_once_guard_for_' + methodName } }
Возвращаясь к нашему примеру, если у вас есть этот класс:
class TestClass {
int counter = 0
@victorsavkin.sample1.ExecuteOnce
void executeOnceMethod(){
counter ++
}
}
Скомпилированный код будет выглядеть так:
class TestClass {
private $_victorsavkin_samples_execute_once_guard_for_executeOnceMethod = false;
int counter = 0
void executeOnceMethod(){
if(! $_victorsavkin_samples_execute_once_guard_for_executeOnceMethod){
$_victorsavkin_samples_execute_once_guard_for_executeOnceMethod = true;
counter ++
}
}
}
Это так просто. В реальной жизни мы должны решать проблемы параллелизма, но это усложнит пример, поэтому я не делал этого здесь.
Подводить итоги:
- Написать собственное преобразование AST — не самая простая вещь, но она выполнима и, что также очень важно, тестируема. Кроме того, он позволяет вам изменить семантику языка, что невозможно сделать с помощью метапрограммирования во время выполнения.
- Я бы посоветовал вам купить Groovy в действии 2, если вы хотите узнать больше об этой теме. Я бы не стал ждать выхода финальной версии и купить ее прямо сейчас.
- Если вы хотите получить удовольствие от метапрограммирования во время компиляции, взгляните на Спока. Это хорошая иллюстрация того, что можно сделать с помощью преобразований AST и нельзя сделать с помощью метапрограммирования во время выполнения.
GitHub Repository
https://github.com/avix1000/AST-Transformations
С http://victorsavkin.com/post/4568615925/compile-time-metaprogramming-in-groovy-part-1