掘金 后端 ( ) • 2024-04-15 17:39

  在我们日常的开发过程中,经常会遇到一些功能的逻辑变更很频繁的需求,相信大部分小伙伴都会通过加if判断来解决(毕竟这样最简单,也可能是因为项目赶时间等一系列客观因素)。长此以往,我们项目代码中可能充斥着大量的if代码段,可读性不高。当然网上有很多方式消除if...else...代码的方法,比如说使用恰当的设计模式、使用规则引擎等等。

  上述所说的可读性不高,不是本文分享内容解决的重点问题,逻辑变更很频繁的需求,还带来的另一个问题就是需要经常重启服务。今天我们分享的就是利用Groovy脚本在Spring Boot项目中实现动态编程解决这一问题,使业务逻辑的动态化,极大地提升了开发效率和灵活性

为什么是Groovy

  Groovy语言作为一种基于JVM的动态语言,有着简洁灵活的语法、动态类型和闭包等特性。Groovy无缝地集成了Java的强大功能,并提供了许多额外的特性,如DSL(领域特定语言)的支持和元编程能力,使得开发者能够以更加简洁和优雅的方式表达复杂的逻辑(Groovy脚本的基础概念这里就不多说了,不是很清楚的小伙伴可以在网上找找)。

具体实现

  • 引入Maven依赖
<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-all</artifactId>
    <version>2.4.5</version>
</dependency>
  • GroovyEngineUtils工具类
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class GroovyEngineUtils{

    private static final GroovyScriptEngineImpl GROOVY_ENGINE = (GroovyScriptEngineImpl) new ScriptEngineManager().getEngineByName("groovy");

    /**
     * 编译脚本
     * @param script
     * @return
     * @throws ScriptException
     */
    public static CompiledScript compile(String script) throws ScriptException {
        return GROOVY_ENGINE.compile(script);
    }

    /**
     * 执行脚本
     * @param compiledScript 
     * @param args 脚本参数
     * @return
     */
    public static Object eval(CompiledScript compiledScript, Map<String, Object> args) {
        try {
            return compiledScript.eval(getScriptContext(args));
        } catch (ScriptException e) {
            log.error("//// exec GroovyEngineUtils.eval error!!!", e);
        }
        return null;
    }

    /**
     * 执行脚本
     * @param script 脚本
     * @return
     */
    public static Object eval(String script) {
        try {
            return GROOVY_ENGINE.eval(script);
        } catch (ScriptException e) {
            log.error("//// exec GroovyEngineUtils.eval error!!!", e);
        }
        return null;
    }

    /**
     * 执行脚本
     * @param script 脚本
     * @param args 脚本参数
     * @return
     */
    public static Object eval(String script, Map<String, Object> args) {
        try {
            return GROOVY_ENGINE.eval(script, getScriptContext(args));
        } catch (ScriptException e) {
            log.error("//// exec GroovyEngineUtils.eval error!!!", e);
        }
        return null;
    }

    private static ScriptContext getScriptContext(Map<String, Object> args) {
        ScriptContext scriptContext = new SimpleScriptContext();
        if (Objects.nonNull(args)) {
            args.forEach((k, v) -> scriptContext.setAttribute(k, v, ScriptContext.ENGINE_SCOPE));
        }
        return scriptContext;
    }
}
  • GroovyScriptVar类

  GroovyScriptVar类主要作用是在项目启动时,缓存编译后的脚本,也提供了刷新脚本的功能,这样在我业务逻辑变更的时候,我们可以通过刷新功能,重新加载脚本。防止了我们重启服务。

@Component
@Slf4j
public class GroovyScriptVar {

    // 缓存编译后的脚本
    private static final Map<String, CompiledScript> SCRIPT_MAP = new ConcurrentHashMap<>();

    @Resource
    public CommoncriptDao commonScriptDao;


    @PostConstruct
    public void init() {
        refresh();
    }

    @SneakyThrows
    public void refresh() {
        synchronized (GroovyScriptVar.class) {
            // 从数据库中加载脚本
            List<CommonScript> list = commonScriptDao.findAll();
            SCRIPT_MAP.clear();
            if(CollectionUtils.isEmpty(list)) return;
            for (CommonScript script : list) {
                SCRIPT_MAP.put(script.getUniqueKey(), GroovyEngineUtils.compile(script.getScript()));
            }
            log.info("//// Groovy脚本初始化,加载数量:{}",list.size());
        }
    }


    public CompiledScript get(String uniqueKey){
        return SCRIPT_MAP.get(uniqueKey);
    }

}
  • 脚本规则表设计
create table common_script
(
    id               int auto_increment comment '主键标识'
        primary key,
    unique_key       varchar(32)            null comment '唯一标识',
    script           mediumtext             null comment '脚本',
    creator          varchar(128)           null comment '创建人名称',
    create_date      datetime               null comment '创建时间',
    last_update_date datetime               null comment '更新时间'
)
    comment '脚本规则';

Groovy脚本在Spring Boot项目中使用核心逻辑到这基本就已经完成了。

实现效果

  • 测试脚本

定义一个非常简单的脚本根据不同状态,打印不同返回值

if ("COMPLETED" == status) {
    // 如果条件为COMPLETED
    return "条件为已完成"
}else{
    return "条件为未完成"
}
  • 测试service
@Slf4j
@Service
public class GroovyExecServiceImpl implements GroovyExecService {


    @Autowired
    private GroovyScriptVar groovyScriptVar;


    @Override
    public void exec() {
        Map<String, Object> args=new HashMap<>();
        args.put("status","COMPLETED");
        CompiledScript compiledScript = groovyScriptVar.get("test");
        Object result = GroovyEngineUtils.eval(compiledScript, args);
        log.info("/// 脚本执行结果{}",(String)result);
    }

    @Override
    public void refresh() {
        groovyScriptVar.refresh();
    }
}
  • 执行效果

可以看到项目在启动过程中已经加载了一个脚本 image.png

调用exec方法 image.png

总结

  通过Groovy脚本在Spring Boot项目中实现动态编程,将Groovy脚本存储在数据库等中间件中,我们可以实现诸如动态配置、动态路由以及动态业务逻辑的功能,极大地提高了项目的可扩展性和可维护性。