把java接口写在数据库里(groovy)

3,390 阅读2分钟

业务复杂多变?那把接口写在数据库里吧,修改随改随用!本文使用了Groovy脚本,不了解的可以自行了解,直接上菜。

  1. 引入依赖
<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-all</artifactId>
    <version>2.5.16</version>
    <type>pom</type>
</dependency>
  1. 创建测试接口
public interface InterfaceA {

    /**
     * 执行规则
     */
    void testMethod();
}
  1. resource目录下创建.groovy实现上面的接口
@Slf4j
class GroovyInterfaceAImpl implements InterfaceA {

    @Override
    void testMethod() {
        log.info("我是groovy编写的InterfaceA接口实现类中的接口方法")
        GroovyScriptService groovyScriptService = SpringUtils.getBean(GroovyScriptService.class)
        GroovyScript groovyScript = Optional.ofNullable(groovyScriptService.getOne(new QueryWrapper<GroovyScript>()
                .eq("name", "groovy编写的java接口实现类")
                .eq("version", 1))).orElseThrow({ -> new RuntimeException("没有查询到脚本") })
        log.info("方法中进行了数据库查询,数据库中的groovy脚本是这个:{}", "\n" + groovyScript.getScript())
    }
}
  1. mysql数据库中建个表groovy_script

image.png 5. 将刚才编写的.groovy文件内容存入数据库

@RunWith(SpringRunner.class)
@SpringBootTest
public class GroovyTest {

    @Resource
    private GroovyScriptService groovyScriptService;

    @Test
    public void test01() {
        GroovyScript groovyScript = new GroovyScript();
        groovyScript.setScript("package groovy\n" +
                "\n" +
                "import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper\n" +
                "import com.demo.groovy.entity.GroovyScript\n" +
                "import com.demo.groovy.service.GroovyScriptService\n" +
                "import com.demo.groovy.service.InterfaceA\n" +
                "import com.demo.groovy.util.SpringUtils\n" +
                "import groovy.util.logging.Slf4j\n" +
                "\n" +
                "\n" +
                "@Slf4j\n" +
                "class GroovyInterfaceAImpl implements InterfaceA {\n" +
                "\n" +
                "    @Override\n" +
                "    void testMethod() {\n" +
                "        log.info("我是groovy编写的InterfaceA接口实现类中的接口方法")\n" +
                "        GroovyScriptService groovyScriptService = SpringUtils.getBean(GroovyScriptService.class)\n" +
                "        GroovyScript groovyScript = Optional.ofNullable(groovyScriptService.getOne(new QueryWrapper<GroovyScript>()\n" +
                "                .eq("name", "groovy编写的java接口实现类")\n" +
                "                .eq("version", 1))).orElseThrow({ -> new RuntimeException("没有查询到脚本") })\n" +
                "        log.info("方法中进行了数据库查询,数据库中的groovy脚本是这个:{}", "\n" + groovyScript.getScript())\n" +
                "    }\n" +
                "}");
        groovyScript.setVersion(1);
        groovyScript.setName("groovy编写的java接口实现类");
        groovyScriptService.save(groovyScript);
    }
}
  1. 从数据读取脚本,GroovyClassLoader加载脚本为Class(注意将Class对象进行缓存)
@Service("groovyScriptService")
@Slf4j
public class GroovyScriptServiceImpl extends ServiceImpl<GroovyScriptServiceMapper, GroovyScript> implements GroovyScriptService {

    private static final Map<String, Md5Clazz> SCRIPT_MAP = new ConcurrentHashMap<>();

    @Override
    public Object getInstanceFromDb(String name, Integer version) {
        //查询脚本
        GroovyScript groovyScript = Optional.ofNullable(baseMapper.selectOne(new QueryWrapper<GroovyScript>()
                .eq("name", name)
                .eq("version", version))).orElseThrow(() -> new RuntimeException("没有查询到脚本"));
        //将groovy脚本转换为java类对象
        Class<?> clazz = getClazz(name + version.toString(), groovyScript.getScript());
        Object instance;

        try {
            instance = clazz.newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return instance;
    }

    private Class<?> getClazz(String scriptKey, String scriptText) {
        String md5Hex = DigestUtil.md5Hex(scriptText);
        Md5Clazz md5Script = SCRIPT_MAP.getOrDefault(scriptKey, null);
        if (md5Script != null && md5Hex.equals(md5Script.getMd5())) {
            log.info("从缓存获取的Clazz");
            return md5Script.getClazz();
        } else {
            CompilerConfiguration config = new CompilerConfiguration();
            config.setSourceEncoding("UTF-8");
            GroovyClassLoader groovyClassLoader = new GroovyClassLoader(Thread.currentThread().getContextClassLoader(), config);
            try {
                Class<?> clazz = groovyClassLoader.parseClass(scriptText);
                SCRIPT_MAP.put(scriptKey, new Md5Clazz(md5Hex, clazz));
                groovyClassLoader.clearCache();
                log.info("groovyClassLoader parseClass");
                return clazz;
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                try {
                    groovyClassLoader.close();
                } catch (IOException e) {
                    log.error("close GroovyClassLoader error", e);
                }
            }
        }
    }

    @Data
    private static class Md5Clazz {
        private String md5;
        private Class<?> clazz;

        public Md5Clazz(String md5, Class<?> clazz) {
            this.md5 = md5;
            this.clazz = clazz;
        }
    }
}
  1. 测试
@RestController
@RequestMapping("/test")
@Slf4j
public class GroovyTestController {

    @Resource
    private GroovyScriptService groovyScriptService;

    @GetMapping("")
    public String testGroovy() {
        InterfaceA interfaceA = (InterfaceA) groovyScriptService.getInstanceFromDb("groovy编写的java接口实现类", 1);
        interfaceA.testMethod();
        return "ok";
    }
}
  1. 接口方法被执行。想要修改接口的话在idea里面把groovy文件编辑好更新到数据库就行了,即时生效。

image.png

本文简单给大家提供一种思路,希望能对大家有所帮助,如有不当之处还请大家指正。本人之前在项目中用的比较多的是Groovyshell,执行的是一些代码片段,而GroovyClassLoader则可以加载整个脚本为Class,Groovy对于java开发者来说还是比较友好的,上手容易。