• 使用yaml+groovy实现Java代码可配置化


    背景与目标###

    使用函数接口和枚举实现配置式编程(Java与Scala实现),使用了函数接口和枚举实现了配置式编程。读者可先阅读此文,再来阅读本文。

    有时,需要将一些业务逻辑,使用配置化的方式抽离出来,供业务专家或外部人员来编辑和修改。这样,就需要将一些代码用脚本的方式实现。在Java语言体系中,与Java粘合比较紧密的是Groovy语言,本例中,将使用Groovy实现Java代码的可配置化。

    目标: 指定字段集合,可输出指定对象的相应字段的值。实现可配置化目标。

    方法:使用groovy的语法和脚本实现相应功能,然后集成到Java应用中。

    实现###

    本文的示例代码都可以在工程 https://github.com/shuqin/ALLIN 下的包 zzz.study.groovy 下找到并运行。 记得安装 lombok 插件以及调整运行时到Java8。

    依赖JAR包####

    本文依赖如下Jar包:groovy-all, fastjson, yamlbeans, lombok ,以及 Java8 (函数语法)

    <dependency>
    			<groupId>org.codehaus.groovy</groupId>
    			<artifactId>groovy-all</artifactId>
    			<version>2.4.12</version>
    		</dependency>
    
    		<dependency>
    			<groupId>com.esotericsoftware.yamlbeans</groupId>
    			<artifactId>yamlbeans</artifactId>
    			<version>1.09</version>
    		</dependency>
    
    		<dependency>
    			<groupId>org.projectlombok</groupId>
    			<artifactId>lombok</artifactId>
    			<version>1.16.18</version>
    		</dependency>
    
    		<dependency>
    			<groupId>com.alibaba</groupId>
    			<artifactId>fastjson</artifactId>
    			<version>1.2.36</version>
    		</dependency>
    

    从脚本开始####

    要实现可配置化,显然要进行字段定义。 简单起见,字段通常包含三个要素: 标识、标题、字段逻辑。 采用 yaml + groovy 的方式来实现。放在 src/main/resources/scripts/ 下。 如下所示:

    name: studentId
    title: 学生编号
    script: |
      stu.studentId
    
    name: studentName
    title: 学生姓名
    script: |
      stu.name
    
    name: studentAble
    title: 特长
    script: |
      stu.able
    

    字段配置的定义类 :

    package zzz.study.groovy;
    
    import lombok.Data;
    
    /**
     * Created by shuqin on 17/11/22.
     */
    @Data
    public class ReportFieldConfig {
    
      /** 报表字段标识 */
      private String name;
    
      /** 报表字段标题 */
      private String title;
    
      /** 报表字段逻辑脚本 */
      private String script;
    
    }
    
    

    配置解析####

    接下来,需要编写配置解析器,将配置文件内容加载到内存,建立字段映射。 配置化的核心,实际就是建立映射关系。

    YamlConfigLoader 实现了单个配置内容的解析。

    package zzz.study.groovy;
    
    import com.alibaba.fastjson.JSON;
    import com.esotericsoftware.yamlbeans.YamlReader;
    
    import java.util.List;
    import java.util.stream.Collectors;
    
    /**
     * Created by yuankui on 17/6/13.
     */
    public class YamlConfigLoader {
    
      public static ReportFieldConfig loadConfig(String content) {
        try {
          YamlReader reader = new YamlReader(content);
          Object object = reader.read();
          return JSON.parseObject(JSON.toJSONString(object), ReportFieldConfig.class);
        } catch (Exception e) {
          throw new RuntimeException("load config failed:" + content, e);
        }
      }
    
      public static List<ReportFieldConfig> loadConfigs(List<String> contents) {
        return contents.stream().map(YamlConfigLoader::loadConfig).collect(Collectors.toList());
      }
    }
    
    

    YamlConfigDirLoader 从指定目录下加载所有配置文件,并使用 YamlConfigLoader 建立所有字段的映射关系。实际工程应用中,通常是将配置保存在DB中,并从DB里读取配置。

    package zzz.study.groovy;
    
    import org.springframework.util.StreamUtils;
    
    import java.io.File;
    import java.io.FileInputStream;
    import java.nio.charset.Charset;
    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Collectors;
    
    /**
     * Created by shuqin on 17/11/23.
     */
    public class YamlConfigDirLoader {
    
      private String dir;
    
      public YamlConfigDirLoader(String dir) {
        this.dir = dir;
      }
    
      public List<ReportFieldConfig> loadConfigs() {
        File[] files = new File(dir).listFiles();
        return Arrays.stream(files).map(
            file -> {
              try {
                String
                    content =
                    StreamUtils.copyToString(new FileInputStream(file), Charset.forName("utf-8"));
                return YamlConfigLoader.loadConfig(content);
              } catch (java.io.IOException e) {
                System.err.println(e.getMessage());
                throw new RuntimeException(e);
              }
            }
        ).collect(Collectors.toList());
      }
    
    }
    
    

    FieldsConfigLoader 在应用启动的时候,调用 YamlConfigDirLoader 的能力加载所有配置文件。

    package zzz.study.groovy;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    /**
     * Created by shuqin on 17/11/22.
     */
    public class FieldsConfigLoader {
    
      private static Logger logger = LoggerFactory.getLogger(FieldsConfigLoader.class);
    
      private static Map<String, ReportFieldConfig> fieldConfigMap = new HashMap<>();
      static {
        try {
          List<ReportFieldConfig> fieldConfigs = new YamlConfigDirLoader("src/main/resources/scripts/").loadConfigs();
          fieldConfigs.forEach(
              fc -> fieldConfigMap.put(fc.getName(), fc)
          );
          logger.info("fieldConfigs: {}", fieldConfigs);
        } catch (Exception ex) {
          logger.error("failed to load fields conf", ex);
        }
    
      }
    
      public static ReportFieldConfig getFieldConfig(String name) {
        return fieldConfigMap.get(name);
      }
    
    }
    
    

    客户端集成####

    package zzz.study.groovy;
    
    import groovy.lang.Binding;
    import groovy.lang.GroovyShell;
    
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Collectors;
    
    import zzz.study.function.basic.Person;
    import zzz.study.function.basic.Student;
    
    /**
     * Created by shuqin on 17/11/23.
     */
    public class StudentOutput {
    
      static List<String> fields = Arrays.asList("studentId", "studentName", "studentAble");
    
      public static void main(String[] args) {
        List<Person> students = getPersons();
        List<String> stundentInfos = students.stream().map(
            p -> getOneStudentInfo(p, fields)
        ).collect(
            Collectors.toList());
        System.out.println(String.join("
    ", stundentInfos));
      }
    
      private static String getOneStudentInfo(Person p, List<String> fields) {
        List<String> stuInfos = new ArrayList<>();
        fields.forEach(
            field -> {
              ReportFieldConfig fieldConfig = FieldsConfigLoader.getFieldConfig(field);
              Binding binding = new Binding();
              binding.setVariable("stu", p);
              GroovyShell shell = new GroovyShell(binding);
              Object result = shell.evaluate(fieldConfig.getScript());
              //System.out.println("result from groovy script: " + result);
              stuInfos.add(String.valueOf(result));
            }
        );
        return String.join(",", stuInfos);
      }
    
      private static List<Person> getPersons() {
        Person s1 = new Student("s1", "liming", "Study");
        Person s2 = new Student("s2", "xueying", "Piano");
        return Arrays.asList(new Person[]{s1, s2});
      }
    
    }
    
    

    这里使用了 GroovyShell, Binding 的基本功能来运行 groovy 。虽然例子中只是简单的取属性值,实际上还可以灵活调用传入对象的方法,展示更复杂的业务逻辑。比如 stu.name 还可写成 stu.getName() 。

    运行后得到如下结果:

     s1,liming,Study
    s2,xueying,Piano
    

    至此,DEMO 完成。实际工程集成的时候,需要先将所有字段定义的脚本配置加载到内存并解析和缓存起来,在需要的时候直接使用,而不会像demo里每个字段都new一次。

    脚本缓存####

    Groovy 脚本每次运行都会生成一个新的类。开销比较大,需要进行缓存。

    
    @Component("scriptExecutor")
    public class ScriptExecutor {
    
      private static Logger logger = LoggerFactory.getLogger(ScriptExecutor.class);
    
      private LoadingCache<String, GenericObjectPool<Script>> scriptCache;
    
      @Resource
      private GlobalConfig globalConfig;
    
      @PostConstruct
      public void init() {
        scriptCache = CacheBuilder
            .newBuilder().build(new CacheLoader<String, GenericObjectPool<Script>>() {
              @Override
              public GenericObjectPool<Script> load(String script) {
                GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
                poolConfig.setMaxTotal(globalConfig.getCacheMaxTotal());
                poolConfig.setMaxWaitMillis(globalConfig.getMaxWaitMillis());
                return new GenericObjectPool<Script>(new ScriptPoolFactory(script), poolConfig);
              }
            });
        logger.info("success init scripts cache.");
      }
    
      public Object exec(String scriptPassed, Binding binding) {
        GenericObjectPool<Script> scriptPool = null;
        Script script = null;
        try {
          scriptPool = scriptCache.get(scriptPassed);
          script = scriptPool.borrowObject();
          script.setBinding(binding);
          Object value = script.run();
          script.setBinding(null);
          return value;
        } catch (Exception ex) {
          logger.error("exxec script error: " + ex.getMessage(), ex);
          return null;
        } finally {
          if (scriptPool != null && script != null) {
            scriptPool.returnObject(script);
          }
        }
    
      }
    
    }
    
    

    小结###

    本文使用了yaml+groovy实现了Java代码的可配置化。可配置化的优势是,可以将一些简单的逻辑公开给外部编辑和使用,增强了互操作性;而对于复杂逻辑来说,可配置化代码的调试则会比较麻烦。因此,可配置化的度要掌握好。 配置本身就是代码,只是配置具有公开化的特点。

  • 相关阅读:
    UNP(2rd)第二卷源码编译
    A very hard mathematic problem
    并查集~
    N皇后问题 深搜dfs
    实践中的一点小问题
    java环境配置 HelloWorld
    精确时间计算方法
    C语言关于文件操作
    字典树应用~
    并查集应用2
  • 原文地址:https://www.cnblogs.com/lovesqcc/p/7882991.html
Copyright © 2020-2023  润新知