1. 程式人生 > >利用Java的動態編譯、動態載入結合EasyRules實現業務規則的動態性

利用Java的動態編譯、動態載入結合EasyRules實現業務規則的動態性

作為一名專門寫bug的Java程式猿,相信大家都會遇到過這樣的問題:專案的業務邏輯很複雜,而且還經常變化,今天的一個辦理條件是小於5,明天就變成了大於10或者條件作廢。這就很頭疼了,裡面的數字可以抽取到配置檔案,但是大於和小於呢?條件作廢呢?

對於業務規則的複雜性,我們可以使用一些規則引擎來解決程式碼可讀性差的問題。市面上也有不少的規則引擎框架,開源的不開源的,收費的不收費的,我們這裡推薦使用的是EasyRules(https://github.com/j-easy/easy-rules)。

對於業務規則的變化性,部分變數的值可以抽取出來放到配置檔案裡面。但是大部分的需求變化,可不是改變一下變數的值那麼簡單,可能是一大段程式碼的重寫,這就需要利用Java6之後提供的動態編譯來實現了。

廢話不多說,精彩馬上來!

思路:在EasyRules中,一個if (...) {...}對應一條規則,也對應著一個類。這樣我們可以將這個類的資訊(原始碼、編譯後位元組碼、類名、所屬分組等)存到資料庫,以提供系統在執行時修改原始碼、重新編譯、動態載入、替換規則的功能。

具體實現:定義規則類,這個類除了有EasyRule的類名、原始碼、編譯後位元組碼等資訊之外,還有一些其它屬性,比如規則所屬分組、執行優先順序、啟動狀態等。當我們在頁面新增(或者修改)了原始碼,提交之後對其進行編譯,將得到類名和位元組碼,然後將這些資料儲存到資料庫。如果規則是啟用狀態,還要建立一個例項存放到到我們維護的一個map集合裡(如果存在同類名的例項就替換),以供規則引擎去呼叫。

一、EasyRules

什麼是EasyRules

首先EasyRule是一個規則引擎.這個名字由來是受到了Martin Fowler 的文章 Should I use a Rules Engine

You can build a simple rules engine yourself. All you need is to create a bunch of objects with conditions and actions, store them in a collection, and run through them to evaluate the conditions and execute the actions.

框架特點

  • 輕量級類庫和容易上手
  • 基於POJO的開發與註解的程式設計模型
  • 方便且適用於java的抽象的業務模型規則
  • 支援從簡單的規則建立組合規則

Useful abstractions to define business rules and apply them easily with Java
The ability to create composite rules from primitive ones

官方demo

1. First, define your rule..

Either in a declarative way using annotations:

@Rule(name = "weather rule", description = "if it rains then take an umbrella" )
public class WeatherRule {

    @Condition
    public boolean itRains(@Fact("rain") boolean rain) {
        return rain;
    }
    
    @Action
    public void takeAnUmbrella() {
        System.out.println("It rains, take an umbrella!");
    }
}

Or in a programmatic way with a fluent API:

Rule weatherRule = new RuleBuilder()
        .name("weather rule")
        .description("if it rains then take an umbrella")
        .when(facts -> facts.get("rain").equals(true))
        .then(facts -> System.out.println("It rains, take an umbrella!"))
        .build();

Or using an Expression Language:

Rule weatherRule = new MVELRule()
        .name("weather rule")
        .description("if it rains then take an umbrella")
        .when("rain == true")
        .then("System.out.println(\"It rains, take an umbrella!\");");

Or using a rule descriptor:

Like in the following weather-rule.yml example file:

name: "weather rule"
description: "if it rains then take an umbrella"
condition: "rain == true"
actions:
  - "System.out.println(\"It rains, take an umbrella!\");"
Rule weatherRule = MVELRuleFactory.createRuleFrom(new File("weather-rule.yml"));

2. Then, fire it!

public class Test {
    public static void main(String[] args) {
        // define facts
        Facts facts = new Facts();
        facts.put("rain", true);

        // define rules
        Rule weatherRule = ...
        Rules rules = new Rules();
        rules.register(weatherRule);

        // fire rules on known facts
        RulesEngine rulesEngine = new DefaultRulesEngine();
        rulesEngine.fire(rules, facts);
    }
}

上面例子沒有提到的是,規則類可以設定執行優先順序,具體做法就是在類裡面定義一個返回型別是int的方法,然後在方法上面加一個註解@Priority。

二、規則資訊類

我們將類名叫做JavaRuleDO,所有屬性都對應著資料庫JAVA_RULE表的欄位。

這裡使用了lombok外掛,因此可以省略了getter、setter和toString方法,還有其它註解,挺好用的,感興趣的童鞋可以去看看。

@Getter
@Setter
@ToString
@Entity
@Table(name = "JAVA_RULE")
public class JavaRuleDO implements Serializable {
	private static final long serialVersionUID = 830103606495004702L;

	@Id
	private Long id;
	// 目標,一般指哪個系統
	@Column
	private String target;
	// 檔名
	@Column
	private String fileName;
	// 全類名
	@Column
	private String fullClassName;
	// 類名
	@Column
	private String simpleClassName;
	// 原始碼
	@Column
	private String srcCode;
	// 編譯後位元組碼
	@Column
	private byte[] byteContent;
	// 建立時間
	@Column
	private Date createTime;
	// 建立使用者id
	@Column
	private Long createUserId = Consts.Entity.NULL_ID_PLACEHOLDER;
	// 建立使用者名稱稱
	@Column
	private String createUserName;
	// 更新時間
	@Column
	private Date updateTime;
	// 更新使用者id
	@Column
	private Long updateUserId = Consts.Entity.NULL_ID_PLACEHOLDER;
	// 更新使用者名稱稱
	@Column
	private String updateUserName;
	// 是否已刪除,1是 0否
	@Column
	private Integer isDeleted = Consts.Entity.NOT_DELETED;
	// 狀態,1有效 0無效
	@Column
	private Integer status = Consts.Entity.NOT_VALID;
	// 組別名稱,一般指哪一系列規則
	@Column
	private String groupName;
	// 順序(優先順序)
	@Column
	private Integer sort = Integer.MAX_VALUE;
	// 規則名稱
	@Column
	private String name;
	// 規則描述
	@Column
	private String description;
	
}

下面附上Oracle版本的建表SQL。

create table JAVA_RULE
(
  id                NUMBER(11) not null,
  target            VARCHAR2(32) not null,
  file_name         VARCHAR2(32) not null,
  full_class_name   VARCHAR2(64) not null,
  simple_class_name VARCHAR2(32) not null,
  src_code          CLOB not null,
  byte_content      BLOB not null,
  create_time       DATE not null,
  create_user_id    NUMBER(11) not null,
  create_user_name  VARCHAR2(128) not null,
  update_time       DATE,
  update_user_id    NUMBER(11),
  update_user_name  VARCHAR2(128),
  is_deleted        NUMBER(1) default 0 not null,
  status            NUMBER(1) default 1 not null,
  group_name        VARCHAR2(32) not null,
  sort              NUMBER(3) default 999,
  name              VARCHAR2(512) not null,
  description       VARCHAR2(2048)
)
;
comment on column JAVA_RULE.id
  is '主鍵';
comment on column JAVA_RULE.target
  is '目標,一般指哪個系統';
comment on column JAVA_RULE.file_name
  is '檔名';
comment on column JAVA_RULE.full_class_name
  is '全類名';
comment on column JAVA_RULE.simple_class_name
  is '類名';
comment on column JAVA_RULE.src_code
  is '原始碼';
comment on column JAVA_RULE.byte_content
  is '編譯後位元組碼';
comment on column JAVA_RULE.create_time
  is '建立時間';
comment on column JAVA_RULE.create_user_id
  is '建立使用者id';
comment on column JAVA_RULE.create_user_name
  is '建立使用者名稱稱';
comment on column JAVA_RULE.update_time
  is '更新時間';
comment on column JAVA_RULE.update_user_id
  is '更新使用者id';
comment on column JAVA_RULE.update_user_name
  is '更新使用者名稱稱';
comment on column JAVA_RULE.is_deleted
  is '是否已刪除,1是 0否';
comment on column JAVA_RULE.status
  is '狀態,1有效 0無效';
comment on column JAVA_RULE.group_name
  is '組別名稱,一般指哪一系列規則';
comment on column JAVA_RULE.sort
  is '順序(優先順序)';
comment on column JAVA_RULE.name
  is '規則名稱';
comment on column JAVA_RULE.description
  is '規則描述';
create unique index IDX_JAVA_RULE_FULL_CLASS_NAME on JAVA_RULE (FULL_CLASS_NAME);
alter table JAVA_RULE
  add constraint PK_JAVA_RULE primary key (ID);

三、動態編譯

動態編譯一直是Java的夢想,從Java 6版本它開始支援動態編譯了,可以在執行期直接編譯.java檔案,執行.class。但是還要慎用,因為存在效能和安全問題。

下面提供了一個編譯原始碼的工具方法。如果編譯成功,返回一個CompileResult物件(自定義型別,下面給出了原始碼);如果編譯失敗,返回具體的編譯不通過原因。其中,Result是貴公司封裝的一個通用返回值包裝器,這裡不方便提供原始碼,抱歉。

程式碼1:動態編譯原始碼的工具方法(想了解更多細節可以去參考其它資料),暫時先去掉了後面會用到的其它工具方法。

/**
 * 規則工具類
 * @author z_hh  
 * @date 2018年12月7日
 */
@Slf4j
public class DynamicRuleUtils {
    // 編譯版本
    private static final String TARGET_CLASS_VERSION = "1.8";

	/**
	 * auto fill in the java-name with code, return null if cannot find the public class
	 * 
	 * @param javaSrc source code string
	 * @return return the Map, the KEY means ClassName, the VALUE means bytecode.
	 * @throws RuntimeException
	 */
	public static Result<CompileResult> compile(String javaSrc) throws RuntimeException {
		Pattern pattern = Pattern.compile("public\\s+class\\s+(\\w+)");
		Matcher matcher = pattern.matcher(javaSrc);
		if (matcher.find()) {
			return compile(matcher.group(1) + ".java", javaSrc);
		}
		return Results.error("找不到類名稱!");
	}

	/**
	 * @param javaName the name of your public class,eg: <code>TestClass.java</code>
	 * @param javaSrc source code string
	 * @return return the Map, the KEY means ClassName, the VALUE means bytecode.
	 * @throws RuntimeException
	 */
	public static Result<CompileResult> compile(String javaName, String javaSrc) throws RuntimeException {
		JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
		StandardJavaFileManager stdManager = compiler.getStandardFileManager(null, null, null);
		try (MemoryJavaFileManager manager = new MemoryJavaFileManager(stdManager)) {
			JavaFileObject javaFileObject = MemoryJavaFileManager.makeStringSource(javaName, javaSrc);
			DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<>();
			List<String> options = new ArrayList<>();
	        options.add("-target");
	        options.add(TARGET_CLASS_VERSION);
			JavaCompiler.CompilationTask task = compiler.getTask(null, manager, collector, options, null,
					Arrays.asList(javaFileObject));
			if (task.call()) {
				return Results.success(CompileResult.builder()
						.mainClassFileName(javaName)
						.byteCode(manager.getClassBytes())
						.build());
			}
			String errorMessage = collector.getDiagnostics().stream()
				.map(diagnostics -> diagnostics.toString())
				.reduce("", (s1, s2) -> s1 + "\n" +s2);
			return Results.error(errorMessage);
		} catch (IOException e) {
			log.error("編譯出錯啦!", e);
			return Results.error(e.getMessage());
		}
	}
}

/**
 * JavaFileManager that keeps compiled .class bytes in memory.
 */
@SuppressWarnings({ "unchecked", "rawtypes" })
final class MemoryJavaFileManager extends ForwardingJavaFileManager {
	/**
	 * Java source file extension.
	 */
	private final static String EXT = ".java";
	private Map<String, byte[]> classBytes;

	public MemoryJavaFileManager(JavaFileManager fileManager) {
		super(fileManager);
		classBytes = new HashMap<String, byte[]>();
	}

	public Map<String, byte[]> getClassBytes() {
		return classBytes;
	}

	@Override
	public void close() throws IOException {
		classBytes = new HashMap<String, byte[]>();
	}

	@Override
	public void flush() throws IOException {
	}

	/**
	 * A file object used to represent Java source coming from a string.
	 */
	private static class StringInputBuffer extends SimpleJavaFileObject {
		final String code;

		StringInputBuffer(String name, String code) {
			super(toURI(name), Kind.SOURCE);
			this.code = code;
		}

		public CharBuffer getCharContent(boolean ignoreEncodingErrors) {
			return CharBuffer.wrap(code);
		}

		@SuppressWarnings("unused")
		public Reader openReader() {
			return new StringReader(code);
		}
	}

	/**
	 * A file object that stores Java bytecode into the classBytes map.
	 */
	private class ClassOutputBuffer extends SimpleJavaFileObject {
		private String name;

		ClassOutputBuffer(String name) {
			super(toURI(name), Kind.CLASS);
			this.name = name;
		}

		public OutputStream openOutputStream() {
			return new FilterOutputStream(new ByteArrayOutputStream()) {
				public void close() throws IOException {
					out.close();
					ByteArrayOutputStream bos = (ByteArrayOutputStream) out;
					classBytes.put(name, bos.toByteArray());
				}
			};
		}
	}

	@Override
	public JavaFileObject getJavaFileForOutput(JavaFileManager.Location location, String className,
			JavaFileObject.Kind kind, FileObject sibling) throws IOException {
		if (kind == JavaFileObject.Kind.CLASS) {
			return new ClassOutputBuffer(className);
		} else {
			return super.getJavaFileForOutput(location, className, kind, sibling);
		}
	}

	static JavaFileObject makeStringSource(String name, String code) {
		return new StringInputBuffer(name, code);
	}

	static URI toURI(String name) {
		File file = new File(name);
		if (file.exists()) {
			return file.toURI();
		} else {
			try {
				final StringBuilder newUri = new StringBuilder();
				newUri.append("mfm:///");
				newUri.append(name.replace('.', '/'));
				if (name.endsWith(EXT))
					newUri.replace(newUri.length() - EXT.length(), newUri.length(), EXT);
				return URI.create(newUri.toString());
			} catch (Exception exp) {
				return URI.create("mfm:///com/sun/script/java/java_source");
			}
		}
	}
}

程式碼2: CompileResult類,之所以設定mainClassFileName,是因為我們暫時不支援內部類,只儲存頂級類的位元組碼。

/**
 * 類編譯結果
 * @author z_hh  
 * @date 2018年12月5日
 */
@Getter
@Setter
@Builder
@ToString
public class CompileResult {
	
	// 主類全類名
	private String mainClassFileName;
	
	// 編譯出來的全類名和對應class位元組碼
	private Map<String, byte[]> byteCode;
	
}

程式碼3:將編譯得到的資訊設定到實體物件。

// 編譯
    private Result<JavaRuleDO> compiler(JavaRuleDO entity) {
    	Result<?> result = DynamicRuleUtils.compile(entity.getSrcCode());
    	if (result.isError()) {
			return (Result<JavaRuleDO>) result;
		}
    	CompileResult compileResult = (CompileResult) result.get();
    	for (String classFullName : compileResult.getByteCode().keySet()) {
    		int lastIndex = classFullName.lastIndexOf(".");
			String simpleName = lastIndex != -1 ? classFullName.substring(lastIndex + 1) : classFullName,
				fileName = compileResult.getMainClassFileName();
			// 只要最外層的類
			if (fileName.startsWith(simpleName)) {
				entity.setFileName(fileName);
				entity.setFullClassName(classFullName);
				entity.setSimpleClassName(simpleName);
				entity.setByteContent(compileResult.getByteCode().get(classFullName));
				return Results.success(entity);
			}
		}
    	return Results.error("沒有找到最外層類!");
    }

四、動態載入

在動態編譯階段,我們已經得到了原始碼對應的位元組碼,這樣就可以將其載入到JVM裡面了。

在這裡有必要提兩點:第一,在Java虛擬機器層面,相同的一個類,除了有相同的全類名以外,還要由相同的類載入器進行載入;第二,類載入的雙親委派模型,工作過程是:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳送到頂層的啟動類載入器中,只有當父類載入器反饋自己無法完成這個載入請求(它的搜尋範圍中沒有找到所需的類)時,子載入器才會嘗試自己去載入。

因此,我們需要定義自己的類載入器,每次將class位元組碼載入到JVM都需要建立一個新的類載入器物件。主要是重寫findClass方法,將我們傳進去的位元組碼陣列進行載入。

    /*
	 * a temp memory class loader
	 */
        private static class MemoryClassLoader extends URLClassLoader {
		Map<String, byte[]> classBytes = new HashMap<String, byte[]>();

		public MemoryClassLoader(Map<String, byte[]> classBytes) {
			super(new URL[0], MemoryClassLoader.class.getClassLoader());
			this.classBytes.putAll(classBytes);
		}

		@Override
		protected Class<?> findClass(String name) throws ClassNotFoundException {
			byte[] buf = classBytes.get(name);
			if (buf == null) {
				return super.findClass(name);
			}
			classBytes.remove(name);
			return defineClass(name, buf, 0, buf.length);
		}
	}

具體使用方式。

/**
	 * 從JavaRuleDO獲取規則Class物件
	 * @param javaRule
	 * @throws Exception
	 */
	public static Class<?> getRuleClass(JavaRuleDO javaRule) throws Exception {
		Map<String, byte[]> bytecode = new HashMap<>();
		String fullName = Objects.requireNonNull(javaRule.getFullClassName(), "全類名不能為空!");
		bytecode.put(fullName, javaRule.getByteContent());
		try (MemoryClassLoader classLoader = new MemoryClassLoader(bytecode)) {
			return classLoader.findClass(fullName);
		} catch (Exception e) {
			log.error("載入類{}異常!", fullName);
			throw e;
		}
	}

五、定義規則基類

這裡強制所有規則類都要繼承我們定義好的規則基類,有兩個原因:

1、定義優先順序屬性,並提供set方法供外部設值、get方法供規則引擎獲取。

2、重寫hashCode方法和equals方法,以讓容器(HashSet)認定相同型別的兩個元素是相同的。

/**
 * 規則基類
 * @author z_hh  
 * @date 2018年12月12日
 */
public class BaseRule {

	private int priority = Integer.MAX_VALUE;
	
	/*重寫equals方法和hashCode方法,讓Set集合判定同類型的兩個物件相同*/
	
	@Override
	public boolean equals(Object obj) {
		return Objects.nonNull(obj) 
				&& Objects.equals(this.getClass().getName(), obj.getClass().getName());
	}
	
	@Override
	public int hashCode() {
		return Objects.hashCode(this.getClass().getName());
	}
	
	/**
	 * 獲取優先順序
	 */
	@Priority
	public int getPriority() {
		return priority;
	}
	
	public void setPriority(int priority) {
		this.priority = priority;
	}
	
}

六、維護規則容器

首先我們定義一個容器介面,宣告一些需要提供的介面。畢竟面向介面程式設計,方便以後擴充套件容器型別。

/**
 * Java規則類儲存器
 * @author z_hh  
 * @date 2018年12月12日
 */
public interface JavaRuleStorage {
	
	/**
	 * 容器是否包含指定規則
	 * @param javaRule
	 * @return
	 */
	boolean contains(String groupName, BaseRule rule);

	/**
	 * 新增規則到容器
	 * @param javaRule
	 */
	boolean add(String groupName, BaseRule rule);
	
	/**
	 * 批量新增規則到容器的指定組
	 * @param javaRules
	 */
	boolean batchAdd(String groupName, Iterable<? extends BaseRule> rules);
	
	/**
	 * 從容器移除指定規則
	 * @param group
	 */
	boolean remove(String groupName, BaseRule rule);
	
	/**
	 * 從容器移除指定組的規則
	 * @param group
	 */
	boolean remove(String group);
	
	/**
	 * 從容器獲取指定組的所有規則
	 * @param group
	 * @return
	 */
	Collection<BaseRule> listObjByGroup(String group);
}

然後提供一個map實現版。或者使用Spring IOC容器,但我感覺沒有那麼靈活。這裡的Multimap是Google Guava提供的集合類工具,類似於JDK裡面的Map<K, Set<E>>。需要注意的是,新增元素的時候,如果存在相同key和相同value型別的元素,需要先將其移除。

/**
 * Java規則類儲存器Map版
 * @author z_hh  
 * @date 2018年12月12日
 */
public class MapJavaRuleStorage implements JavaRuleStorage {
	
	private final Multimap<String, BaseRule> map = HashMultimap.create();

	@Override
	public boolean contains(String groupName, BaseRule rule) {
		return map.containsEntry(groupName, rule);
	}

	@Override
	public boolean add(String groupName, BaseRule rule) {
		// 如果原來有,就先刪除掉
		if (map.containsEntry(groupName, rule)) {
			map.remove(groupName, rule);
		}
		return map.put(groupName, rule);
	}

	@Override
	public boolean batchAdd(String groupName, Iterable<? extends BaseRule> rules) {
		return map.putAll(groupName, rules);
	}

	@Override
	public boolean remove(String groupName, BaseRule rule) {
		return map.remove(groupName, rule);
	}

	@Override
	public boolean remove(String group) {
		return !map.removeAll(group).isEmpty();
	}

	@Override
	public Collection<BaseRule> listObjByGroup(String group) {
		return map.get(group);
	}

}

七、新增規則到容器、將規則從容器移除

獲取規則例項的工具方法(其中getRuleClass(...)方法上面已提供)。

/**
	 * 從JavaRuleDO獲取規則例項物件
	 * @param javaRule
	 * @throws Exception
	 */
	public static BaseRule getRuleInstance(JavaRuleDO javaRule) throws Exception {
		try {
			BaseRule rule = (BaseRule) getRuleClass(javaRule).newInstance();
			// 設定優先順序
			rule.setPriority(javaRule.getSort());
			return rule;
		} catch (Exception e) {
			log.error("從JavaRuleDO獲取規則例項異常!", e);
			throw e;
		}
	}

新增規則到容器,其中entity為JavaRuleDO例項。

/**
     * 新增規則到容器
     * @param entity
     * @return
     */
    private Result<JavaRuleDO> addRuleToStorage(JavaRuleDO entity) {
		try {
			BaseRule rule = DynamicRuleUtils.getRuleInstance(entity);
			return javaRuleStorage.add(entity.getGroupName(), rule) ? Results.success(entity)
	    			: Results.error("新增規則到容器失敗!");
		} catch (Exception e) {
			log.error("新增規則{}到容器異常!", entity.getName(), e);
			return Results.error("新增規則到容器異常!");
		}
    }

將規則從容器移除,其中entity為JavaRuleDO例項。

String groupName = entity.getGroupName();
try {
	BaseRule rule = DynamicRuleUtils.getRuleInstance(entity);
	if (javaRuleStorage.contains(groupName, rule) && !javaRuleStorage.remove(groupName, rule)) {
		return Results.error("從容器移除規則失敗!");
	}
} catch (Exception e) {
	log.error("從容器移除規則{}異常!", entity.getName(), e);
	return Results.error("從容器移除規則異常!");
}

八、封裝規則使用元件

為了更方便使用規則引擎,我們特意建立了一個DynamicRuleManager,以實現鏈式呼叫。

/**
 * 動態規則管理器
 * @author z_hh  
 * @date 2018年12月12日
 */
@Component("dynamicRuleManager")
public class DynamicRuleManager {
	
	public Builder builder() {
		return new Builder(this);
	}
    
    public class Builder {
    	private Rules rules = new Rules();
    	private Facts facts = new Facts();
    	private RulesEngine engine = new DefaultRulesEngine();
    	private JavaRuleStorage javaRuleStorage;
    	
    	public Builder(DynamicRuleManager dynamicRuleManager) {
    		javaRuleStorage = dynamicRuleManager.javaRuleStorage;
		}
        
    	/**
    	 * 設定引數,該引數為值傳遞,在規則裡面或者執行完之後可以取到
    	 * @param name
    	 * @param value
    	 * @return
    	 */
        public Builder setParameter(String name, Object value) {
        	facts.put(name, value);
            return this;
        }
        
        /**
         * 增加規則組(將指定所屬分組的所有啟用規則新增進來)
         * @param groupName
         * @return
         */
        public Builder addRuleGroup(String groupName) {
        	Collection<BaseRule> rs = javaRuleStorage.listObjByGroup(groupName);
        	rs.stream().forEach(rules::register);
        	return this;
        }
        
        /**
         * 執行規則引擎
         */
        public Builder run() {
        	engine.fire(rules, facts);
        	return this;
        }
        
        /**
         * 獲取指定引數,並轉為指定型別
         * @param pName
         * @param pType
         * @return
         */
        public <T> T getParameter(String pName, Class<T> pType) {
        	return facts.get(pName);
        }
        
    }
    
    @Autowired
    private JavaRuleStorage javaRuleStorage;
    
}
@Configuration
public class RuleDefaultConf {

	@Bean
	@ConditionalOnMissingBean
	public JavaRuleStorage javaRuleStorage() {
		return new MapJavaRuleStorage();
	}
}

九、使用案例

1、建立規則。

@Rule
public class DemoRule1 extends BaseRule {

	@Condition
	public boolean when(@Fact("param1") String param1) {
		System.out.println("我是引數1,value=" + param1);
		return true;
	}
	
	@Action
	public void then(@Fact("param2") String param2) {
		System.out.println("我是引數2,value=" + param2);
	}
}

2、呼叫規則。

Builder builder = dynamicRuleManager.builder()
			.setParameter("param1", "Hello")
			.setParameter("param2", "World")
			.addRuleGroup("testRule")
			.run();
		String param1 = builder.getParameter("param1", String.class);
		String param2 = builder.getParameter("param2", String.class);
		System.out.println(param1 + " " + param2);

3、執行結果。

十、系統啟動時將啟用的規則新增到容器

我們設定了dynamic.rule.target(目標,所屬系統)引數從配置檔案獲取。

/**
 * 應用啟動監聽器
 * @author z_hh  
 * @date 2018年12月10日
 */
@Slf4j
@WebListener
public class AppRunListener implements ServletContextListener {
	
	@Value("${dynamic.rule.target}")
	private String ruleTarget;
	
	/**
	 * 將指定組的javaRule物件裝進容器
	 */
	@Override
	public void contextInitialized(ServletContextEvent event) {
		if (StringUtils2.notEmpty(ruleTarget)) {
			List<JavaRuleDO> javaRules = javaRuleService.createJpaQuery()
				.where("status", Consts.Entity.IS_VALID)
				.where("target", SqlFieldOperatorEnum.IN, Arrays.asList(ruleTarget.split(",")))
				.list();
			javaRules.stream()
				.forEach(javaRule -> {
					try {
						BaseRule rule = DynamicRuleUtils.getRuleInstance(javaRule);
						if (!javaRuleStorage.add(javaRule.getGroupName(), rule)) {
							log.warn("新增規則{}到容器失敗!", javaRule.getName());
							javaRule.setStatus(Consts.Entity.NOT_VALID);
							javaRuleService.save(javaRule);
						}
						log.info("添加了規則{}到容器", javaRule.getFullClassName());
					} catch (Exception e) {
						log.warn("新增規則{}到容器異常!", javaRule.getName());
						javaRule.setStatus(Consts.Entity.NOT_VALID);
						javaRuleService.save(javaRule);
					}
					
				});
		}
	}
	
	@Override
	public void contextDestroyed(ServletContextEvent event) {
		// 
	}
	
	@Autowired
	private JavaRuleService javaRuleService;
	@Autowired
	private JavaRuleStorage javaRuleStorage;
}

十一、其它說明

1、當我們在管理頁面新增或者修改規則時,如果狀態為啟用,後臺應該需要在編譯之後建立規則例項並放進容器;反之,如果狀態為禁用,後臺就判斷容器裡是否有該規則類的例項,有的話需要將其移除。

2、管理頁面的啟用和禁用,對應著這個規則類例項新增到容器和從容器裡面移除的操作。

3、刪除規則時,如果啟用狀態不能刪除,需要先將其禁用。

十二、相關頁面展示

本文內容到此結束了,有什麼問題或者建議,歡迎在評論區進行探討!

博文內容沒有將涉及的程式碼全部展示。完整的程式碼已經打包上傳到我的資源,大家可以去下載參考;還有,部分程式碼涉及到公司的框架,並沒有包含在裡面,可以換一種自己的方式實現的,抱歉。