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

ImportNew2018-01-07 12:06:02

(点击上方公众号,可快速关注)


来源:琴水玉 ,

www.cnblogs.com/lovesqcc/p/7882991.html


背景与目标


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


http://www.cnblogs.com/lovesqcc/p/6649018.html


有时,需要将一些业务逻辑,使用的方式抽离出来,供业务专家或外部人员来编辑和修改。这样,就需要将一些代码用脚本的方式实现。在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("\n", 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一次。


小结


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


看完本文有收获?请转发分享给更多人

关注「ImportNew」,提升Java技能

阅读原文

TAGS: