Groovy 脚本静态分析器
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 协议》,转载必须注明作者和本文链接
关于 LearnKu