Groovy 脚本静态分析器

AI摘要
本文介绍了一个用于Web端提交Groovy脚本的静态分析器组件,属于技术知识分享。该组件旨在通过语法检测、AST解析和一系列自定义规则(如if语句必须包含return、脚本最终return、未使用/未定义变量检测、if/else完整性、死循环检测和复杂度限制)来防止错误脚本进入规则引擎,适用于风控、决策引擎等系统,并提供了核心Java实现代码和项目结构。

Groovy 脚本静态分析器

一、组件目标

Web端提交 Groovy 脚本时进行静态检测,防止错误脚本进入规则引擎。

支持能力:

能力 说明
语法检测 Groovy编译校验
错误行号 返回具体错误位置
if必须return 防止逻辑路径缺失
脚本最终return 确保规则返回值
未使用变量检测 防止无效变量
未定义变量检测 防止运行时错误
if/else return完整性 确保逻辑闭环
死循环检测 防止 while(true)
脚本复杂度限制 控制规则复杂度

适用于:

  • 风控规则引擎

  • 决策引擎

  • 低代码规则平台

  • 脚本配置系统


二、项目结构

src/main/java/com/xxx/script
│
├── GroovyScriptAnalyzer.java
│
├── model
│     ├── ScriptError.java
│     └── ValidationResult.java
│
└── rule
      ├── ScriptRule.java
      ├── IfReturnRule.java
      ├── FinalReturnRule.java
      ├── UnusedVariableRule.java
      ├── UndefinedVariableRule.java
      ├── IfElseReturnRule.java
      ├── LoopSafetyRule.java
      └── ComplexityRule.java

三、核心入口类

GroovyScriptAnalyzer

package com.xxx.script;

import com.xxx.script.model.ScriptError;
import com.xxx.script.model.ValidationResult;
import com.xxx.script.rule.*;
import groovy.lang.GroovyShell;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.builder.AstBuilder;
import org.codehaus.groovy.control.*;
import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
import org.codehaus.groovy.syntax.SyntaxException;
import org.springframework.stereotype.Component;

import java.util.*;

@Component
public class GroovyScriptAnalyzer {

    private final List<ScriptRule> rules = new ArrayList<>();

    public GroovyScriptAnalyzer() {

        rules.add(new IfReturnRule());
        rules.add(new FinalReturnRule());
        rules.add(new UnusedVariableRule());
        rules.add(new UndefinedVariableRule());
        rules.add(new IfElseReturnRule());
        rules.add(new LoopSafetyRule());
        rules.add(new ComplexityRule());

    }

    public ValidationResult validate(String script) {

        ValidationResult result = new ValidationResult();

        List<ScriptError> syntaxErrors = checkSyntax(script);

        if (!syntaxErrors.isEmpty()) {

            result.setSuccess(false);
            result.setSyntaxErrors(syntaxErrors);
            return result;

        }

        List<ASTNode> nodes = parseAST(script);

        List<ScriptError> errors = new ArrayList<>();

        for (ScriptRule rule : rules) {

            errors.addAll(rule.check(nodes));

        }

        result.setLogicErrors(errors);
        result.setSuccess(errors.isEmpty());

        return result;
    }

    private List<ScriptError> checkSyntax(String script) {

        List<ScriptError> errors = new ArrayList<>();

        GroovyShell shell = new GroovyShell();

        try {

            shell.parse(script);

        } catch (MultipleCompilationErrorsException e) {

            ErrorCollector collector = e.getErrorCollector();

            for (Object obj : collector.getErrors()) {

                if (obj instanceof SyntaxErrorMessage) {

                    SyntaxException se =
                            ((SyntaxErrorMessage) obj).getCause();

                    ScriptError error = new ScriptError();

                    error.setLine(se.getLine());
                    error.setColumn(se.getStartColumn());
                    error.setMessage(se.getMessage());

                    errors.add(error);

                }
            }
        }

        return errors;
    }

    private List<ASTNode> parseAST(String script) {

        AstBuilder builder = new AstBuilder();

        return builder.buildFromString(
                CompilePhase.SEMANTIC_ANALYSIS,
                false,
                script
        );
    }
}

四、返回对象

ScriptError

package com.xxx.script.model;

public class ScriptError {

    private int line;
    private int column;
    private String message;

    public int getLine() {
        return line;
    }

    public void setLine(int line) {
        this.line = line;
    }

    public int getColumn() {
        return column;
    }

    public void setColumn(int column) {
        this.column = column;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

ValidationResult

package com.xxx.script.model;

import java.util.List;

public class ValidationResult {

    private boolean success;

    private List<ScriptError> syntaxErrors;

    private List<ScriptError> logicErrors;

    public boolean isSuccess() {
        return success;
    }

    public void setSuccess(boolean success) {
        this.success = success;
    }

    public List<ScriptError> getSyntaxErrors() {
        return syntaxErrors;
    }

    public void setSyntaxErrors(List<ScriptError> syntaxErrors) {
        this.syntaxErrors = syntaxErrors;
    }

    public List<ScriptError> getLogicErrors() {
        return logicErrors;
    }

    public void setLogicErrors(List<ScriptError> logicErrors) {
        this.logicErrors = logicErrors;
    }
}

五、规则接口

package com.xxx.script.rule;

import com.xxx.script.model.ScriptError;
import org.codehaus.groovy.ast.ASTNode;

import java.util.List;

public interface ScriptRule {

    List<ScriptError> check(List<ASTNode> nodes);

}

六、规则实现


1 If 必须包含 Return

public class IfReturnRule extends CodeVisitorSupport implements ScriptRule {

    private final List<ScriptError> errors = new ArrayList<>();

    @Override
    public List<ScriptError> check(List<ASTNode> nodes) {

        errors.clear();

        for (ASTNode node : nodes) {

            if (node instanceof BlockStatement) {

                ((BlockStatement) node).visit(this);

            }
        }

        return errors;
    }

    @Override
    public void visitIfElse(IfStatement ifElse) {

        boolean hasReturn = containsReturn(ifElse.getIfBlock());

        if (!hasReturn) {

            ScriptError error = new ScriptError();

            error.setLine(ifElse.getLineNumber());
            error.setMessage("if 语句缺少 return");

            errors.add(error);

        }

        super.visitIfElse(ifElse);
    }

    private boolean containsReturn(Statement stmt) {

        if (stmt instanceof ReturnStatement) return true;

        if (stmt instanceof BlockStatement) {

            for (Statement s : ((BlockStatement) stmt).getStatements()) {

                if (containsReturn(s)) return true;

            }
        }

        return false;
    }
}

2 脚本必须最终 Return

public class FinalReturnRule implements ScriptRule {

    @Override
    public List<ScriptError> check(List<ASTNode> nodes) {

        List<ScriptError> errors = new ArrayList<>();

        for (ASTNode node : nodes) {

            if (node instanceof BlockStatement) {

                List<Statement> stmts = ((BlockStatement) node).getStatements();

                if (stmts.isEmpty()) continue;

                Statement last = stmts.get(stmts.size() - 1);

                if (!(last instanceof ReturnStatement)) {

                    ScriptError error = new ScriptError();
                    error.setLine(last.getLineNumber());
                    error.setMessage("脚本必须以 return 结束");

                    errors.add(error);

                }
            }
        }

        return errors;
    }
}

3 未使用变量检测

public class UnusedVariableRule extends CodeVisitorSupport implements ScriptRule {

    private final Map<String, Integer> declaredVars = new HashMap<>();
    private final Set<String> usedVars = new HashSet<>();
    private final List<ScriptError> errors = new ArrayList<>();

    @Override
    public List<ScriptError> check(List<ASTNode> nodes) {

        declaredVars.clear();
        usedVars.clear();
        errors.clear();

        for (ASTNode node : nodes) {

            if (node instanceof BlockStatement) {

                ((BlockStatement) node).visit(this);

            }
        }

        for (String var : declaredVars.keySet()) {

            if (!usedVars.contains(var)) {

                ScriptError error = new ScriptError();
                error.setLine(declaredVars.get(var));
                error.setMessage("变量未使用: " + var);

                errors.add(error);

            }
        }

        return errors;
    }

    @Override
    public void visitDeclarationExpression(DeclarationExpression expr) {

        String name = expr.getVariableExpression().getName();

        declaredVars.put(name, expr.getLineNumber());

        super.visitDeclarationExpression(expr);
    }

    @Override
    public void visitVariableExpression(VariableExpression expr) {

        usedVars.add(expr.getName());

        super.visitVariableExpression(expr);
    }
}

4 未定义变量检测

public class UndefinedVariableRule extends CodeVisitorSupport implements ScriptRule {

    private final Set<String> declared = new HashSet<>();
    private final List<ScriptError> errors = new ArrayList<>();

    @Override
    public List<ScriptError> check(List<ASTNode> nodes) {

        declared.clear();
        errors.clear();

        for(ASTNode node : nodes){

            if(node instanceof BlockStatement){

                ((BlockStatement) node).visit(this);

            }

        }

        return errors;
    }

    @Override
    public void visitDeclarationExpression(DeclarationExpression expr) {

        declared.add(expr.getVariableExpression().getName());

        super.visitDeclarationExpression(expr);
    }

    @Override
    public void visitVariableExpression(VariableExpression expr) {

        if(!declared.contains(expr.getName())){

            ScriptError error = new ScriptError();
            error.setLine(expr.getLineNumber());
            error.setMessage("变量未定义: " + expr.getName());

            errors.add(error);

        }

        super.visitVariableExpression(expr);
    }
}

5 If / Else Return完整性

public class IfElseReturnRule extends CodeVisitorSupport implements ScriptRule {

    private final List<ScriptError> errors = new ArrayList<>();

    @Override
    public List<ScriptError> check(List<ASTNode> nodes) {

        errors.clear();

        for(ASTNode node : nodes){

            if(node instanceof BlockStatement){

                ((BlockStatement) node).visit(this);

            }

        }

        return errors;
    }

    @Override
    public void visitIfElse(IfStatement stmt) {

        boolean ifReturn = containsReturn(stmt.getIfBlock());
        boolean elseReturn = containsReturn(stmt.getElseBlock());

        if(!(ifReturn && elseReturn)){

            ScriptError error = new ScriptError();
            error.setLine(stmt.getLineNumber());
            error.setMessage("if/else 必须保证所有路径 return");

            errors.add(error);

        }

        super.visitIfElse(stmt);
    }

    private boolean containsReturn(Statement stmt){

        if(stmt instanceof ReturnStatement) return true;

        if(stmt instanceof BlockStatement){

            for(Statement s : ((BlockStatement) stmt).getStatements()){

                if(containsReturn(s)) return true;

            }

        }

        return false;
    }
}

6 死循环检测

public class LoopSafetyRule extends CodeVisitorSupport implements ScriptRule {

    private final List<ScriptError> errors = new ArrayList<>();

    @Override
    public List<ScriptError> check(List<ASTNode> nodes) {

        errors.clear();

        for(ASTNode node : nodes){

            if(node instanceof BlockStatement){

                ((BlockStatement) node).visit(this);

            }

        }

        return errors;
    }

    @Override
    public void visitWhileLoop(WhileStatement loop) {

        if(loop.getBooleanExpression().getText().equals("true")){

            ScriptError error = new ScriptError();
            error.setLine(loop.getLineNumber());
            error.setMessage("禁止 while(true) 死循环");

            errors.add(error);

        }

        super.visitWhileLoop(loop);
    }
}

7 脚本复杂度限制

public class ComplexityRule extends CodeVisitorSupport implements ScriptRule {

    private int ifCount = 0;
    private int loopCount = 0;

    @Override
    public List<ScriptError> check(List<ASTNode> nodes) {

        List<ScriptError> errors = new ArrayList<>();

        for(ASTNode node : nodes){

            if(node instanceof BlockStatement){

                ((BlockStatement) node).visit(this);

            }

        }

        if(ifCount > 10){

            ScriptError error = new ScriptError();
            error.setMessage("脚本if数量超过限制");

            errors.add(error);

        }

        if(loopCount > 3){

            ScriptError error = new ScriptError();
            error.setMessage("脚本循环数量过多");

            errors.add(error);

        }

        return errors;
    }

    @Override
    public void visitIfElse(IfStatement stmt){

        ifCount++;

        super.visitIfElse(stmt);
    }

    @Override
    public void visitWhileLoop(WhileStatement stmt){

        loopCount++;

        super.visitWhileLoop(stmt);
    }
}

七、Spring Controller 示例

@RestController
@RequestMapping("/script")
public class ScriptController {

    @Autowired
    private GroovyScriptAnalyzer analyzer;

    @PostMapping("/validate")
    public ValidationResult validate(@RequestBody String script){

        return analyzer.validate(script);

    }
}

八、执行流程

Web脚本提交
      │
      ▼
GroovyScriptAnalyzer
      │
      ├── 语法检测
      │
      ├── AST解析
      │
      ├── 规则检测
      │      ├── if return
      │      ├── final return
      │      ├── unused variable
      │      ├── undefined variable
      │      ├── if/else return
      │      ├── loop safety
      │      └── complexity
      │
      ▼
ValidationResult

九、生产环境建议

建议再增加:

规则 说明
方法白名单 限制脚本调用
脚本长度限制 防止恶意脚本
变量类型校验 防止类型错误
AST缓存 提升性能
执行超时控制 防止规则卡死
本作品采用《CC 协议》,转载必须注明作者和本文链接
每天一点小知识,到那都是大佬,哈哈
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!