第十九章、Groovy的DSL(Domain-Specific Language)

特定领域语言:描述某个事物,大家都理解,比如汉语,或者说sql语言,都是一种描述特定领域语言,约定一些关键字,语法之类,使得程序员或机器能够识别和解析语言的含义

1、Groovy自然灵活特性

  • groovy 语言是从java5衍生出来,所有完全支持java5所有语法
  • groovy可以省略一些括号和分号,声明类型之类的

1.1、忽略括号(命令式表达式)

  • /**
     * @author liangchen* @date 2020/12/19
     */
    enum  Direction{
        left, right, forward, backward
    }
    
    class Robot{
        void move(Direction direction) {
            println "root moved $direction"
        }
    }
    def robot = new Robot()
    // 可以省略括号
    robot.move Direction.left
    robot.move Direction.right
    
    // 使用groovyshell
    
    def shell= new GroovyShell(this.class.classLoader)
    shell.evaluate '''
    import com.jack.groovy.ch19.Direction
    import com.jack.groovy.ch19.Robot
    def robt = new Robot()
    robt.move Direction.left
    '''
    
  • 外部DSLfile解析

  • /**
     * @author liangchen* @date 2020/12/19
     */
    // 直接加载外部脚本,直接执行
    def shell = new GroovyShell()
    shell.evaluate './_19_1_RobotMove.groovy' as File
    
    
    
    

2、变量、常量和方法注入

  • Binding注入对象

  • /**
     * @author liangchen* @date 2020/12/19
     */
    // 使用binding 绑定变量,可以给脚本增加一些字段
    def binding = new Binding(distance: 11400, time: 5 * 60)
    
    def shell = new GroovyShell(binding)
    shell.evaluate'''
    speed = distance / time
    '''
    
    assert binding.distance == 11400
    assert binding.time == 5 * 60
    assert binding.speed == 38
    
    // 将Robot 类注入到脚本中
    binding = new Binding(robot: new Robot())
    shell = new GroovyShell(this.class.classLoader, binding)
    shell.evaluate '''
        import  com.jack.groovy.ch19.Direction
        robot.move Direction.left
    '''
    
    

2.1、通过binding注入常量

  • /**
     * @author liangchen* @date 2020/12/19
     */
    //通过binding注入常量(单个枚举对象注入)
    def binding = new Binding(
            robot : new Robot(),
            left: Direction.left,
            right: Direction.right,
            forward:Direction.forward,
            backward: Direction.backward
    )
    def shell = new GroovyShell(this.class.classLoader, binding)
    shell.evaluate '''
        robot.move left
    '''
    
    // 注入整个枚举类,通过*通配符, 利用collectEntries方法
    binding = new Binding(
            robot: new Robot(),
            *: Direction.values().collectEntries { [(it.name()): it] }
    
    )
    shell = new GroovyShell(this.class.classLoader, binding)
    shell.evaluate '''
        robot.move left
    '''
    

2.2、注入方法到脚本中

/**
 * @author liangchen* @date 2020/12/19
 */

//定义一个抽象robot基础脚本
abstract  class RobotBaseScript extends  Script{
    void move(Direction direction) {
        this.binding.robot.move  direction
    }
}

def binding = new Binding(
        robot: new Robot(),
        *: Direction.values().collectEntries { [(it.name()): it] }

)
//利用@BaseScript导入脚本,其实方法是可以共用
def shell = new GroovyShell(this.class.classLoader, binding)
shell.evaluate '''
    @BaseScript(com.jack.groovy.ch19.RobotBaseScript)  
    import groovy.transform.BaseScript  
    move left
'''

2.3、自动添加import和static import

import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.customizers.ImportCustomizer

/**
 * @author liangchen* @date 2020/12/19
 */
def binding = new Binding(robot : new Robot())
// 把要导入包先放到这里面,后面脚本需要可以直接导包
def importCustomer = new ImportCustomizer()
importCustomer.addStaticStars 'com.jack.groovy.ch19.Direction'
def config = new CompilerConfiguration()
config.addCompilationCustomizers importCustomer
def shell = new GroovyShell(this.class.classLoader, binding, config)
shell.evaluate '''
    robot.move left
'''

2.4、注入方法(复习)

  • import org.codehaus.groovy.control.CompilerConfiguration
    import org.codehaus.groovy.control.customizers.ImportCustomizer
    
    import javax.script.ScriptEngine
    
    /**
     * @author liangchen* @date 2020/12/19
     */
    // 利用RobotBaseScript脚本
    def binding = new Binding(robot: new Robot())
    def importCustomizer = new ImportCustomizer()
    importCustomizer.addStaticStars Direction.name
    def config = new CompilerConfiguration()
    config.addCompilationCustomizers importCustomizer
    config.scriptBaseClass = RobotBaseScript.name
    
    def shell = new GroovyShell(this.class.classLoader, binding, config)
    shell.evaluate '''
       move left
    '''
    
    
    // 使用@Delegate 注解
    
    abstract  class RobotBaseScriptDelegate extends Script{
        @Delegate
        @Lazy Robot robot = this.binding.robot
    }
    

2.5、给binding添加闭包

  • 自定义 binding 和ScriptBaseClass

  • package com.jack.groovy.ch19
    
    import org.codehaus.groovy.control.CompilerConfiguration
    
    
    /**
     * @author liangchen* @date 2020/12/19
     */
    def robot = new Robot()
    def binding = new Binding(
            robot: robot,
            // 引用robot的move方法
            move: robot.&move,
            *: Direction.values().collectEntries { [(it.name()): it] }
    )
    def shell = new GroovyShell(this.class.classLoader, binding)
    shell.evaluate '''
        move left
    '''
    
    // 自定义BaseScript 和 自定义Binding方法, 在调用方法时候忽略字符大小写
    abstract  class CaseRobotBaseScript extends RobotBaseScript{
        def invokeMethod(String name, args) {
            //调用root方法,同时方法变成小写
            getBinding().robot."${name.toLowerCase()}"(*args)
        }
    }
    
    class CustomBinding extends Binding{
    
        private Map variables
        CustomBinding(Map vars) {
           this.variables = [
                   *:vars,
                   *:Direction.values().collectEntries { [(it.name()): it] }
           ]
        }
    
        def getVariable(String name) {
            variables[name.toLowerCase()]
        }
    }
    
    
    // 使用上面自定义类
    binding = new CustomBinding(robot: new Robot())
    def config = new CompilerConfiguration()
    config.scriptBaseClass = CaseRobotBaseScript.name
    shell = new GroovyShell(this.class.classLoader, binding, config)
    shell.evaluate '''
        mOVe lEFt
    '''
    
    

3、数字添加属性

  • import groovy.transform.TupleConstructor
    import org.codehaus.groovy.control.CompilerConfiguration
    import org.codehaus.groovy.control.customizers.ImportCustomizer
    
    /**
     * @author liangchen* @date 2020/12/19
     */
    
    enum DistanceUnit{
        centimeter('cm', 0.01),
        meter ('m', 1),
        kilometer('km', 1000)
    
        String abbreviation
        double multiplier
    
        DistanceUnit(String abbr, double mult) {
            this.abbreviation = abbr
            this.multiplier = mult
        }
        String toString(){abbreviation}
    }
    
    
    @TupleConstructor
    class Distance{
        Number amount
        DistanceUnit unit
    
        String toString() { "$amount$unit" }
    
    }
    
    class DistanceCategory{
        static Distance getCentimeters(Number number) {
            new Distance(number, DistanceUnit.centimeter)
        }
    
        static Distance getMeters(Number number) {
            new Distance(number, DistanceUnit.meter)
        }
    
        static Distance getKilometers(Number number) {
            new Distance(number, DistanceUnit.kilometer)
        }
    
    }
    use(DistanceCategory) {
        def importCustomizer = new ImportCustomizer()
        importCustomizer.addStaticStars Direction.name
        def config = new CompilerConfiguration()
        config.scriptBaseClass = RobotBaseScript.name
        config.addCompilationCustomizers importCustomizer
       def  binding = new Binding(robot: new Robot())
        def shell = new GroovyShell(this.class.classLoader, binding,config)
       // 为啥跑不起来
       shell.evaluate '''
            move left
            move right, 3.meters
    
        '''
    
    }
    

4、利用命名参数

  • package com.jack.groovy.ch19
    
    import groovy.transform.TupleConstructor
    
    /**
     * @author liangchen* @date 2020/12/19
     */
    
    @TupleConstructor
    class Speed{
    
        Number amount
        DistanceUnit unit;
    
        String toString(){"$amount $unit/h"}
    }
    
    enum  Duration{
        hour
    }
    
    def binding = new Binding([
            robot : new Robot(),
            h: Duration.hour
    ])
    
    @TupleConstructor
    class DistanceOverride{
        double amount
        DistanceUnit unit
    
        Speed div(Duration duration) {
            new Speed(amount, unit)
        }
    
        String toString(){
            "$amount$unit"
        }
    }
    

5、命令链

  • 就是在控制台输入命令那样子使用,空格作为分隔符

  • def of = "silent word"
    
    def buy(n){
        [shares:{of ->
            [:].withDefault {ticker ->
                println "buy $n shares of $ticker"
            }
        }]
    }
    buy 200 shares of GOOG
    

6、定义你自己的控制结构

  • /**
     * @author liangchen* @date 2020/12/19
     */
    
    // 定义when结构
    def when(boolean condition, Closure block) {
        if(condition) block()
    }
    
    def a= 1
    def b = 2
    when(a < b, {println "a < b"})
    
    // 定义until结构
    def until(boolean  condition, Closure closure){
        while(!condition) closure();
    }
    def counter = 0
    until(counter == 10) {
        counter++
    
    }
    assert  counter == 10
    

待续。。。

7、使用闭包进行上下文切换

  • 建造器

  • package com.jack.groovy.ch19
    
    /**
     * @author liangchen* @date 2020/12/20
     */
    
    final class FetchOptions{
        private int limit, offset, chunkSize, prefetchSize
    
        private FetchOptions(){}
    
        FetchOptions limit(int lim) {
            this.limit = lim
            return this
        }
    
        FetchOptions offset(int offs) {
            this.offset = offs
            return  this
        }
    
        FetchOptions chunkSize(int cs) {
            this.chunkSize = cs
            return this
        }
    
        FetchOptions prefetchSize(int ps) {
            this.prefetchSize = ps
            return this
        }
    
        static  final class Builder{
            private Builder(){
    
            }
            static  FetchOptions withDefaults(){
                new FetchOptions()
            }
    
            static FetchOptions withLimit(int lim) {
                new FetchOptions().limit(lim)
            }
    
            static FetchOptions withOffset(int ofs) {
                new FetchOptions().offset(ofs)
            }
    
            static FetchOptions withChunkSize(int cs) {
                new FetchOptions().chunkSize(cs)
            }
    
            static FetchOptions withPrefetchSize(int ps) {
                new FetchOptions().prefetchSize(ps)
            }
        }
    }
    def options = FetchOptions.Builder.withLimit(10).offset(60).chunkSize(1000)
    import static com.jack.groovy.ch19.FetchOptions.Builder.*
    
    options = withLimit(10).offset(60).chunkSize(1000)
    //继续简写,去掉括号
    withLimit 10 offset 60 chunkSize 10
    
    // 定义构建器
    class FetchOptionsBuilder{
        static FetchOptions fetchOptions(Closure closure) {
            def opts = FetchOptions.Builder.withDefaults()
            opts.with  closure
            return opts
        }
    }
    import  static  com.jack.groovy.ch19.FetchOptionsBuilder.*
    fetchOptions {
        limit 10 offset 60
        chunkSize 1000
    }
    

8、构建器的其他技术

  • ruby 和python构建方法

  • package com.jack.groovy.ch19
    
    import groovy.transform.Canonical
    import groovy.transform.ToString
    
    /**
     * @author liangchen* @date 2020/12/20
     */
    
    @ToString
    class Car{
        String make
        String model
    }
    // ruby风格实例化对象
    @Newify
    def car = Car.new(make: 'Porsche', model: '911')
    assert car.toString() == 'com.jack.groovy.ch19.Car(Porsche, 911)'
    
    // python风格实例
    
    @Canonical
    class Country{
        String name
    }
    
    @Canonical
    class City{
        String name
        String zipCode
        Country country
    }
    
    @Newify([City, Country])
    def paris = City('Paris', '75000',Country('France'))
    println paris.toString()
    

9、 保护你的DSLs安全

9.1、介绍ASTCustomizer

  • import org.codehaus.groovy.control.CompilerConfiguration
    import org.codehaus.groovy.control.customizers.SecureASTCustomizer
    
    /**
     * @author liangchen* @date 2020/12/20
     */
    
    def secure = new SecureASTCustomizer()
    // java.io.*包类不能使用
    secure.starImportsBlacklist = ['java.io.*']
    secure.indirectImportCheckEnabled = true
    
    def config = new CompilerConfiguration()
    config.addCompilationCustomizers(secure)
    def shell = new GroovyShell(config)
    
    groovy.test.GroovyAssert.shouldFail {
        shell.evaluate '''
        new File('.')
    '''
    }
    
    
  • 限制访问类

9.2、限制数学运行需要类,其他类不允许执行

package com.jack.groovy.ch19

import org.codehaus.groovy.ast.expr.ArgumentListExpression
import org.codehaus.groovy.ast.expr.BinaryExpression
import org.codehaus.groovy.ast.expr.BooleanExpression
import org.codehaus.groovy.ast.expr.ClassExpression
import org.codehaus.groovy.ast.expr.ConstantExpression
import org.codehaus.groovy.ast.expr.ElvisOperatorExpression
import org.codehaus.groovy.ast.expr.MethodCallExpression
import org.codehaus.groovy.ast.expr.PostfixExpression
import org.codehaus.groovy.ast.expr.PrefixExpression
import org.codehaus.groovy.ast.expr.PropertyExpression
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression
import org.codehaus.groovy.ast.expr.TernaryExpression
import org.codehaus.groovy.ast.expr.UnaryMinusExpression
import org.codehaus.groovy.ast.expr.UnaryPlusExpression
import org.codehaus.groovy.ast.stmt.BlockStatement
import org.codehaus.groovy.ast.stmt.ExpressionStatement
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.customizers.ImportCustomizer
import org.codehaus.groovy.control.customizers.SecureASTCustomizer
import static org.codehaus.groovy.syntax.Types.*

/**
 * @author liangchen* @date 2020/12/20
 */

def imports = new ImportCustomizer().addStaticStars('java.lang.Math')
def secure = new SecureASTCustomizer()

secure.with {
    closuresAllowed = false
    methodDefinitionAllowed = false

    importsWhitelist = []
    staticImportsWhitelist = []
    staticStarImportsWhitelist = ['java.lang.Math']
    tokensWhitelist = [
            PLUS,MINUS,MULTIPLY,DIVIDE,MOD, POWER, PLUS_PLUS,MINUS_MINUS,
            COMPARE_EQUAL, COMPARE_NOT_EQUAL, COMPARE_LESS_THAN,
            COMPARE_LESS_THAN_EQUAL, COMPARE_GREATER_THAN, COMPARE_GREATER_THAN_EQUAL
    ]
    constantTypesClassesWhiteList = [
            Integer,Float, Long, Double, BigDecimal,
            Integer.TYPE,Long.TYPE, Float.TYPE, Double.TYPE
    ]
    receiversClassesWhiteList = [
            Math, Integer, Float, Double, Long, BigDecimal
    ]
    statementsWhitelist = [
            BlockStatement, ExpressionStatement
    ]
    expressionsWhitelist = [
            BinaryExpression, ConstantExpression,
            MethodCallExpression, StaticMethodCallExpression,
            ArgumentListExpression, PropertyExpression,
            UnaryMinusExpression, UnaryPlusExpression,
            PrefixExpression, PostfixExpression,
            TernaryExpression, ElvisOperatorExpression,
            BooleanExpression, ClassExpression
    ]
}
def config = new CompilerConfiguration()
config.addCompilationCustomizers(imports, secure)
def shell = new GroovyShell(config)
def result = shell.evaluate('1+cos(PI/2)')
assert result == 1

9.3、停止执行中程序

import groovy.test.GroovyAssert
import groovy.transform.TimedInterrupt
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer

import java.util.concurrent.TimeoutException

/**
 * @author liangchen* @date 2020/12/20
 */


// 超时控制
def config = new CompilerConfiguration()
config.addCompilationCustomizers(new ASTTransformationCustomizer(value: 2, TimedInterrupt))
def shell = new GroovyShell(config)
def te = GroovyAssert.shouldFail(TimeoutException) {
    shell.evaluate'''
    for (i in  1..100){
        sleep 1000
    }
   
'''
}

9.4、避免元编程

  • 拦截方法

  • def config = new CompilerConfiguration()
    // 在编译时候拦截System.exit方法
    def filter = new CompilationCustomizer(CompilePhase.CANONICALIZATION) {
    
        @Override
        void call(SourceUnit source, GeneratorContext context, ClassNode classNode) throws CompilationFailedException {
                new ClassCodeVisitorSupport(){
                    void visitMethodCallExpression(MethodCallExpression callExpression) {
                        if(callExpression.objectExpression.text == 'java.lang.System' &&
                        callExpression.method.text=='exit'){
                            source.addError(new SyntaxException(
                                    'System.exit() forbidden',
                                    callExpression.lineNumber, callExpression.columnNumber
                            ))
                        }
                        super.visitMethodCallExpression(callExpression)
                    }
    
                    @Override
                    protected SourceUnit getSourceUnit() {
                        return source
                    }
                }.visitClass(classNode)
            }
    }
    
    config.addCompilationCustomizers(filter)
    def shell = new GroovyShell(config)
    def ce = GroovyAssert.shouldFail (CompilationFailedException){
        shell.parse'''
        System.exit(0)
    '''
    }
    println ce.message
    

10、测试和错误报告

package com.jack.groovy.ch19

import groovy.transform.InheritConstructors

/**
 * @author liangchen* @date 2020/12/20
 */

class Query{
    static query(Closure closure){
        def q = closure.clone()
        q.resolveStrategy = Closure.DELEGATE_FIRST
        q.delegate = new Query()
        q()
    }

    def getProperty(String name){
        name
    }

    Query select(column){this}
    Query from(talbe){this}
    Query where(condition){this}
    def methodMissing(String name, args){
        def se = new SyntaxException(
                "No query verb '$name', only select/from/where allowed"
        )
        se.stackTrace = se.stackTrace.findAll{StackTraceElement ste ->
            ste.className != 'Query' &&
                    !(ste.methodName in ['invokeMethod', 'methodMissing'])
        }
        throw se

    }
}
@InheritConstructors
class SyntaxException extends Exception{}
def query = new Query();
query.get()
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值