Groovy DSL:如何让两个委托 类 处理 DSLScript 的不同部分?

Groovy DSL: How can I let two delegating classes handle different parts of a DSLScript?

假设我有这样的 DSL

setup {name = "aDSLScript"}
println "this is common groovy code"
doStuff {println "I'm doing dsl stuff"}

通常会有一个委派 class 实现方法 'setup' 和 'doStuff'。此外,可以编写要执行的通用 Groovy 代码(println...)。

我正在寻找的是一种分两步执行此操作的方法。在第一步中,只应处理设置方法(既不是 println)。第二步处理其他部分。

目前,我有两个委派 classes。一个实现 'setup',另一个实现 'doStuff'。但是两者都执行println语句,当然。

您可以创建一个单独的 class 来拦截来自脚本的方法调用,并让它协调以下方法调用。我是通过反思做到的,但如果你愿意,你可以去声明。这些是模型和脚本 classes:

class FirstDelegate {
  def setup(closure) { "firstDelegate.setup" }
}

class SecondDelegate {
  def doStuff(closure) { "secondDelegate.doStuff" }
}


class MethodInterceptor {
  def invokedMethods = []

  def methodMissing(String method, args) {
    invokedMethods << [method: method, args: args]
  }

  def delegate() {
    def lookupCalls = { instance ->
      def invokes = instance.metaClass.methods.findResults { method ->
        invokedMethods.findResult { invocation ->
          invocation.method == method.name ? 
              [method: method, invocation: invocation] : null 
        }
      }

      invokes.collect { invoked ->
        invoked.method.invoke(instance, invoked.invocation.args) 
      }
    }

    return lookupCalls(new FirstDelegate()) + lookupCalls(new SecondDelegate())
  }
}

这里是脚本和断言:

import org.codehaus.groovy.control.CompilerConfiguration

def dsl = '''
setup {name = "aDSLScript"}
println "this is common groovy code"
doStuff {println "Ima doing dsl stuff"}
'''


def compiler = new CompilerConfiguration()
compiler.scriptBaseClass = DelegatingScript.class.name

def shell = new GroovyShell(this.class.classLoader, new Binding(), compiler)

script = shell.parse dsl

interceptor = new MethodInterceptor()

script.setDelegate interceptor

script.run()

assert interceptor.invokedMethods*.method == [ 'setup', 'doStuff' ]

assert interceptor.delegate() == 
    ['firstDelegate.setup', 'secondDelegate.doStuff']

请注意,我没有费心拦截 println 调用,这是一个 DefaultGroovyMethods 因此,处理起来有点麻烦。

同时让 class MethodInterceptor 实现方法 delegate() 不是一个好主意,因为这允许用户定义的脚本调用它。

我找到了一种拆分 DSL 脚本执行的方法。我使用 CompilationCustomizer 从 AST 中删除了除 doFirst{} 之外的所有语句。所以第一个运行只会执行doFirst。第二个 运行 做其他一切。这是一些代码:

class DoFirstProcessor {
    def doFirst(Closure c) {
        c()
    }
}

class TheRestProcessor {
    def doStuff(Closure c) {
        c()
    }

    def methodMissing(String name, args) {
        //nothing to do
    }
}

def dsl = "
println 'this is text that will not be printed out in first line!'

doFirst { println 'First things first: e.g. setting up environment' }

doStuff { println 'doing some stuff now' }

println 'That is it!'
"


class HighlanderCustomizer extends CompilationCustomizer {
    def methodName

    HighlanderCustomizer(def methodName) {
        super(CompilePhase.SEMANTIC_ANALYSIS)
        this.methodName = methodName
    }

    @Override
    void call(SourceUnit sourceUnit, GeneratorContext generatorContext, ClassNode classNode) throws CompilationFailedException {
        def methods = classNode.getMethods()
        methods.each { MethodNode m ->
            m.code.each { Statement st ->
                if (!(st instanceof BlockStatement)) {
                    return
                }
                def removeStmts = []
                st.statements.each { Statement bst ->
                    if (bst instanceof ExpressionStatement) {
                        def ex = bst.expression
                        if (ex instanceof MethodCallExpression) {
                            if (!ex.methodAsString.equals(methodName)) {
                                removeStmts << bst
                            }
                        } else {
                            removeStmts << bst
                        }
                    } else {
                        removeStmts << bst
                    }
                }
                st.statements.removeAll(removeStmts)
            }
        }
    }
}

def cc = new CompilerConfiguration()
cc.addCompilationCustomizers new HighlanderCustomizer("doFirst")
cc.scriptBaseClass = DelegatingScript.class.name

def doFirstShell = new GroovyShell(new Binding(), cc)
def doFirstScript = doFirstShell.parse dsl
doFirstScript.setDelegate new DoFirstProcessor()
doFirstScript.run()

cc.compilationCustomizers.clear()
def shell = new GroovyShell(new Binding(), cc)
def script = shell.parse dsl
script.setDelegate new TheRestProcessor()
script.run()

我做了另一种变体,一步执行 DSL。请参阅我的博客 post:http://hackserei.metacode.de/?p=247