一、Groovy与Java集成

Groovy脚本引擎的执行本质只是接受context对象,然后基于context对象中的关键信息进行逻辑判断,输出结果。
参考文章:
文章1文章2文章3文章4

Java中运行Groovy

在java中运行Groovy脚本,有三种比较常用的类支持:GroovyShell、GroovyClassLoader 以及 Java-Script引擎(JSR-223).

GroovyShell
GroovyShell允许在Java类中(甚至Groovy类)求任意Groovy表达式的值。您可使用Binding对象输入参数给表达式,并最终通过GroovyShell返回Groovy表达式的计算结果。
通常用来运行"script片段"或者一些零散的表达式(Expression)

GroovyClassLoader
用 Groovy 的 GroovyClassLoader ,它会动态地加载一个脚本并执行它。GroovyClassLoader是一个Groovy定制的类装载器,负责解析加载Java类中用到的Groovy类。
如果脚本是一个完整的文件,特别是有API类型的时候,比如有类似于JAVA的接口,面向对象设计时,通常使用GroovyClassLoader.

GroovyScriptEngine
GroovyShell多用于推求对立的脚本或表达式,如果换成相互关联的多个脚本,使用GroovyScriptEngine会更好些。GroovyScriptEngine从您指定的位置(文件系统,URL,数据库,等等)加载Groovy脚本,并且随着脚本变化而重新加载它们。如同GroovyShell一样,GroovyScriptEngine也允许您传入参数值,并能返回脚本的值。

首先导入Groovy包

<properties>
    <groovy.version>2.5.6</groovy.version>
</properties>
<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-all</artifactId>
    <version>${groovy.version}</version>
</dependency>

注意:我的是这样导入包的,否则编译运行时会报异常:java.lang.ClassNotFoundException:org.codehaus.groovy.ast.MethodCallTransformation 参考解答:解决1解决2 说白了就是springboot2.x版本和groovy2.5.x不能完全兼容。

二、SpringBoot动态运行groovy脚本

该小节是在参考这篇文章后,加入一些自己的理解完成的。

简介

在SpringBoot项目中,通过容器和依赖注入,可以很方便的实现开发功能。那么如何通过加载实例来动态编译脚本呢。
groovy支持通过GroovyShell预设对象,在groovy动态脚本中直接调用预设对象的方法。因此我们可以通过将spring的bean预设到GroovyShell运行环境中,在groovy动态脚本中直接调用spring容器中bean来调用其方法。

项目目录

JAVA 规则在线 java groovy 规则引擎_java

项目解释

1、首先,自定义注解@GroovyFunction。用来标识用于绑定到GroovyShell的类。

import java.lang.annotation.*;

/**
 * @author Caocs
 * @date 2020/3/12
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE})
@Documented
public @interface GroovyFunction {
}

2、service层用来编写自定义的方法,用@GroovyFunction注解标识该方法可以被绑定到GroovyShell中。

import org.springframework.stereotype.Service;

/**
 * @author Caocs
 * @date 2020/3/12
 */
@Service
@GroovyFunction
public class TestService {
    public String testQuery(long id) {
        return "Test query success, id is " + id;
    }
}

3、然后,利用SpringBoot中的Configuration类来设置Binding
首先配置类实现org.springframework.context.ApplicationContextAware接口,用来获取应用上下文。然后在配置从上下文获取到的指定Bean实例,并注入到groovy的Binding中。
这里,我是利用上文提到的@GroovyFunction注解来过滤需要的实例

import groovy.lang.Binding;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Map;

/**
 * @author Caocs
 * @date 2020/3/12
 */
@Configuration
public class GroovyBindingConfig implements ApplicationContextAware {
    // 实现ApplicationContextAware接口后的方法类,可以获取Spring中已经实例化的bean
    private ApplicationContext applicationContext;
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
    
    /**
     * 将标注@GroovyFunction注解的类对象绑定到Binding中,并在spring容器中实例化出一个对象
     * @return
     */
    @Bean("groovyBinding")
    public Binding groovyBinding() {
        Binding groovyBinding = new Binding();
        // 根据注解过滤掉不需要的实例
        Map<String, Object> beanMap = applicationContext.getBeansWithAnnotation(GroovyFunction.class);
        for (String beanName : beanMap.keySet()) {
            groovyBinding.setVariable(beanName, beanMap.get(beanName));
        }
        return groovyBinding;
    }
}

4、然后,定义一个请求类
这个其实没啥好说的,因为我不会再controller层请求两个参数。

import java.util.Map;

/**
 * @author Caocs
 * @date 2020/3/12
 */
public class ScriptRequest {
    private String expression;
    private Map<String, Object> paramMap;

    @Override
    public String toString() {
        return "ScriptRequest{" +
                "expression='" + expression + '\'' +
                ", paramMap=" + paramMap +
                '}';
    }

    public String getExpression() {
        return expression;
    }

    public void setExpression(String expression) {
        this.expression = expression;
    }

    public Map<String, Object> getParamMap() {
        return paramMap;
    }

    public void setParamMap(Map<String, Object> paramMap) {
        this.paramMap = paramMap;
    }
}

5、最后,在controller层中,实现动态脚本运行
注意:下面是我的一些想法和实现
(1)采用单例模式,将GroovyShell在初始化时就实例化出来。以后每次都直接调用该实例。
(2)通过依赖注入,将已经绑定自定义方法的Binding实例注入进来
(3)维护一个HashTable,用于存放~~<expression,Script>~~ 的映射关系,这样就可以重复利用已经实例化过的Script的实例。
(4)多个请求同时操作HashTable,所以需要注意线程安全问题。

import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Hashtable;
import java.util.Map;

/**
 * @author Caocs
 * @date 2020/3/12
 */
// @RestController
@Controller
@RequestMapping("/groovy/single/script")
public class SingleScriptController {

    private static final Object lock = new Object();
    private static final GroovyShell groovyShell;
    private static Hashtable<String, Script> scriptCache = new Hashtable<>();

    @Autowired
    private Binding groovyBinding; // 默认绑定已有方法的实例

    static {
        CompilerConfiguration cfg = new CompilerConfiguration();
        groovyShell = new GroovyShell(cfg);
    }

    /**
     * 在客户端本地只实例化单例RuleExecutor
     * 然后多个线程同时操作scriptCache,需要保证线程安全。
     *
     * @param expression
     * @return
     */
    private Script getScriptFromCache(String expression) {
        if (scriptCache.containsKey(expression)) {
            return scriptCache.get(expression);
        }
        synchronized (lock) {
            if (scriptCache.containsKey(expression)) {
                return scriptCache.get(expression);
            }
            Script script = groovyShell.parse(expression);
            scriptCache.put(expression, script);
            return script;
        }
    }

    public Object ruleParse(String expression) {
        Script script = getScriptFromCache(expression);
        script.setBinding(groovyBinding);
        return script.run();
    }

    public Object ruleParse(String expression, Map<String, Object> paramMap) {
        Binding binding = groovyBinding;
        paramMap.forEach((key,value)->binding.setProperty(key,value));
        Script script = getScriptFromCache(expression);
        script.setBinding(binding);
        return script.run();
    }

    @WatchAspect
    @RequestMapping(value = "/execute", method = RequestMethod.POST)
    public @ResponseBody
    Object ruleExecutor(@RequestBody ScriptRequest request) {
        if (request.getParamMap() == null) {
            return ruleParse(request.getExpression());
        } else {
            return ruleParse(request.getExpression(), request.getParamMap());
        }
    }
}

6、利用AOP记录请求日志信息
(1)自定义注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author Caocs
 * @date 2020/3/11
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface WatchAspect {
}

(2)定义切面和切点

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;

/**
 * @author Caocs
 * @date 2020/3/11
 */
@Aspect
@Component
public class AspectIntercepter {

    @Pointcut(value = "@annotation(com.ctrip.flight.backendservice.backofficetool.rule.aop.WatchAspect)")
    public void pointCut() {
    }

    @Around(value = "pointCut()")
    public Object doAround(ProceedingJoinPoint join) throws Throwable {
        Instant start = Instant.now();
        StringBuilder loginfos = new StringBuilder();
        Object[] args = join.getArgs();
        loginfos.append("调用 ").append(join.getTarget().getClass().getName())
                .append(" 的 ").append(join.getSignature().getName()).append(" 方法。")
                .append("\n方法入参:").append(Arrays.toString(args));

        Object result = null;
        try {
            result = join.proceed();
            return result;
        } catch (Throwable e) {
            loginfos.append("\nerror:").append(e.getMessage()).append("\n");
            return e.getMessage();
        } finally {
            loginfos.append("\n方法返回值:").append(String.valueOf(result));
            loginfos.append("\n运行时间(ms):").append(Duration.between(start, Instant.now()).toMillis());
            System.out.println(loginfos); // 模拟记入日志
        }
    }
}

测试

使用postman进行测试,定义一个Post请求,执行后结果如下。

测试1:

JAVA 规则在线 java groovy 规则引擎_java_02


测试2:

JAVA 规则在线 java groovy 规则引擎_JAVA 规则在线_03


最后,因为个人能力问题,欢迎指正。

2020-03-26
突然发现代码会有问题(GC和线程安全问题)
详细内容参考:

JVM执行Groovy脚本导致堆外内存溢出问题排查
https://www.liangzl.com/get-article-detail-161916.html