在使用函數接口和枚舉實現配置式編程(Java與Scala實現),使用了函數接口和枚舉實現了配置式編程。讀者可先閱讀此文,再來閱讀本文。html
有時,須要將一些業務邏輯,使用配置化的方式抽離出來,供業務專家或外部人員來編輯和修改。這樣,就須要將一些代碼用腳本的方式實現。在Java語言體系中,與Java粘合比較緊密的是Groovy語言,本例中,將使用Groovy實現Java代碼的可配置化。java
目標: 指定字段集合,可輸出指定對象的相應字段的值。實現可配置化目標。git
方法:使用groovy的語法和腳本實現相應功能,而後集成到Java應用中。
github
本文的示例代碼均可以在工程 https://github.com/shuqin/ALLIN 下的包 zzz.study.groovy 下找到並運行。 記得安裝 lombok 插件以及調整運行時到Java8。spring
本文依賴以下Jar包:groovy-all, fastjson, yamlbeans, lombok ,以及 Java8 (函數語法)shell
<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
字段配置的定義類 :json
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 實現了單個配置內容的解析。ide
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代碼的可配置化。可配置化的優點是,能夠將一些簡單的邏輯公開給外部編輯和使用,加強了互操做性;而對於複雜邏輯來講,可配置化代碼的調試則會比較麻煩。所以,可配置化的度要掌握好。 配置自己就是代碼,只是配置具備公開化的特色。