背景與目標###
在使用函數接口和枚舉實現配置式編程(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("\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一次。
腳本緩存####
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代碼的可配置化。可配置化的優勢是,可以將一些簡單的邏輯公開給外部編輯和使用,增強了互操作性;而對於復雜邏輯來說,可配置化代碼的調試則會比較麻煩。因此,可配置化的度要掌握好。 配置本身就是代碼,只是配置具有公開化的特點。
