1. 程式人生 > >基於Groovy實現Spring Bean的動態載入

基於Groovy實現Spring Bean的動態載入

Spring對Groovy有著良好的支援,能把Groovy實現類作為Bean來使用,在前一篇Blog《Spring對Groovy Bean的支援》有詳細的描述 http://my.oschina.net/joshuazhan/blog/137940 。基於Groovy Bean可以實現Bean的動態修改,但還有一個缺陷,即無法動態的載入/解除安裝Bean,本文基於Spring ApplicationContext的層級關係(Hierarchy

1. Spring ApplicationContext的層級關係

在ApplicationContext抽象類AbstractApplicationContext中,Spring引入了Context(後文將ApplicationContext簡稱為Context)的層級關係,通過ApplicationContext的建構函式,即可設定其父Context。

/**
 * Create a new AbstractApplicationContext with the given parent context.
 * @param parent the parent context
 */
public AbstractApplicationContext(ApplicationContext parent) {
this.parent = parent;
this.resourcePatternResolver = getResourcePatternResolver();
}

父Context的作用在於,如果當前Context找不到對應的Bean,就到父Context中獲取,可參考AbstractBeanFactory的doGetBean 方法實現。

/**
 * Return an instance, which may be shared or independent, of the specified bean.
 * ...
 */
protected <T> T doGetBean(
        final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
        throws BeansException {
    ...
    // 如果Bean已建立,直接從快取獲取
    Object sharedInstance = getSingleton(beanName);
    ...
    
    else
{         ...         // Bean未建立         BeanFactory parentBeanFactory = getParentBeanFactory();         if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {             // 沒對應的Bean定義,且有父BeanFactory,則從父BeanFactory中獲取             // 注:ApplicationContext是BeanFactory的子介面             String nameToLookup = originalBeanName(name);             if (args != null) {                 return (T) parentBeanFactory.getBean(nameToLookup, args);             }             else {                 return parentBeanFactory.getBean(nameToLookup, requiredType);             }         }         ...     }     ... }

基於Spring的層級關係,可以這麼實現Bean的動態載入:

  1. 將穩定的Bean和易變的Bean分離配置,在父Context中定義穩定的基礎服務Bean,在子Context中定義動態載入的Bean;
  2. 用Groovy實現動態類,並定義相應的使用規範/介面;
  3. 監視子Context配置檔案的變動(增減規則),通過修改子Context的配置來增減Bean,在變動產生時重新建立子Context。

下面通過一個例項來說明。

2. 基於Spring Bean的動態規則引擎

例子可能舉得不太恰當,主要用於說明動態Bean的實現思路和使用方式,無法顧全方方面面。

2.1. 規則介面 

一般而言,規則引擎的規則可以是一段表示式(JEXL、MVEL),或是固化在類中的程式程式碼片段,前者靈活可配,後者與系統的服務整合得更好(能直接呼叫),為了兼顧這兩個特點,本例使用Groovy Bean來實現規則,介面定義如下。

public interface RuleService {
/**    
 * 執行規則
 * @param param 規則所需的引數
 */
void run(String param);
}

Groovy Bean與能在直接中呼叫其他Bean提供的服務來獲取規則所需的資料,規則的介面可以變得非常簡單,只需傳入一個引數的ID即可,把引數獲取的業務邏輯遷移到更靈活的Groovy Bean中。同時,規則運算的結果也可以直接通過Spring中的其他服務進行持久化(或是其他的操作),無需把結果返回給規則引擎處理。

2.2. 規則引擎

規則引擎主要負責的是提供規則的呼叫介面以及管理規則的宣告週期,對應的實現類為RuleEngine。

2.2.1. 規則的執行方法

引擎從規則Context中查詢規則,如果存在則執行規則。

/**
 * 執行指定規則
 * 
 * @param ruleName
 *            規則名字
 * @param param
 *            規則引數
 */
public void run(String ruleName, String param) {
// 查詢規則
if (!ruleContext.containsBean(ruleName)) {
System.out.println("Rule[" + ruleName + "] not found.");
return;
}

// 如果規則存在,執行規則
RuleService service = ruleContext.getBean(ruleName, RuleService.class);
if (null != service) {
try {
service.run(param);
} catch (Exception e) {
System.out.println("Error occur while runing the Rule["
+ ruleName + "]");
}
}
}

2.2.2. 規則的裝載

規則配置通過Spring的Resource注入,在註解中指定路徑。

// 規則配置的資原始檔
@Value("path/to/rules.xml")
private Resource ruleConfig;
規則配置在引擎初始化時載入,並記錄配置的修改時間,後續依據修改時間判斷規則配置是否有變動。
/**
 * 初始化方法,記錄初始配置的時間,通過註解標記為init方法。
 */
@PostConstruct
public void init() {
try {
lastModified = ruleConfig.lastModified();
} catch (IOException e) {
throw new RuntimeException(e);
}
reload();
System.out.println("Rule engine initialized.");
}
reload函式根據配置生成規則Context,其父Context為Spring的根Context,通過ApplicationContextAware介面引入。
/**
 * 重新裝載規則引擎,建立新的規則Context,並銷燬舊Context。
 */
private synchronized void reload() {
if (!ruleConfig.exists()) {
throw new RuntimeException("Rule config not exist.");
}
ClassPathXmlApplicationContext oldContext = this.ruleContext;

try {
String[] config = { ruleConfig.getURI().toString() };
ClassPathXmlApplicationContext newContext = new ClassPathXmlApplicationContext(
config, parentContext);
this.ruleContext = newContext;
} catch (IOException e) {
throw new RuntimeException(e);
}

// 銷燬舊的規則Context
if (null != oldContext && oldContext.isActive()) {
oldContext.close();
}
}
規則配置的變更由checkUpdate方法檢測,通過Spring的任務機制進行排程,每隔5秒執行一次(前一次執行結束到後一次執行開始的間隔)。
/**
 * 以固定的時間間隔檢查規則的配置的變更,如有變更則進行配置的重載入。
 */
@Scheduled(fixedDelay = 5000)
public void checkUpdate() {
try {
// 比對規則變更時間,有變動時進行重新載入
long currentLastModified = ruleConfig.lastModified();
if (this.lastModified < currentLastModified) {
reload();
this.lastModified = currentLastModified;
System.out.println("\nRule engine updated.");
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}

2.2.3. Spring配置

示例使用註解和元件掃描來簡化配置,可檢視其中的註釋說明。

示例的根Context配置dynamic.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:task="http://www.springframework.org/schema/task" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd">

<context:property-placeholder location="classpath:context.properties" />

<!-- 基礎的服務通過元件掃描載入 -->
<context:component-scan
base-package="me.joshua.demo4j.spring.groovy.dynamic.service" />

<!-- Spring Task的配置,用於定時檢查規則配置檔案的變更 -->
<task:annotation-driven />

</beans>
示例的規則配置rules.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:lang="http://www.springframework.org/schema/lang" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang.xsd">

<context:property-placeholder location="classpath:context.properties" />
<context:annotation-config />

<!-- 配置動態載入的Bean,基於Spring的Resource支援,可從網路或本地檔案獲取Groovy程式碼 -->
<lang:groovy id="member" refresh-check-delay="2000"
script-source="${res.rootPath}/${res.projectPath}/${groovy.script.packagePath}/MemberRule.groovy" />

<!-- 先註釋掉Order的規則,把註釋起止符刪除即可把Order規則動態新增到規則引擎中 -->
<!--
<lang:groovy id="order" refresh-check-delay="2000"
script-source="${res.rootPath}/${res.projectPath}/${groovy.script.packagePath}/OrderRule.groovy" />
 -->
<!-- 展示動態Bean之間可以互相呼叫的能力 -->
<lang:groovy id="proxy" refresh-check-delay="2000"
script-source="${res.rootPath}/${res.projectPath}/${groovy.script.packagePath}/ProxyRule.groovy" />

</beans>
規則的名字即為Groovy Bean的ID,可以在執行時修改Groovy的配置,比如把某個Groovy Bean註釋/反註釋,以檢視Bean的動態載入/解除安裝效果。

示例的執行程式碼為Demo,通過JUnit來執行。依據提示輸入規則的名字和引數檢視規則執行效果,規則的可用引數在規則註釋中有羅列。

規則配置和Groovy Bean的程式碼預設通過Http請求從[email protected]上拉取,但可修改context.properties將其指定為從本地檔案獲取,更便於修改和測試。

另:

  1. 本示例的專案路徑為
  2. 示例指定了1.6的JDK版本,有需要可以在pom中修改jdk.version的值;
  3. 配置檔案在“/src/main/resources”目錄下。

3. 小結

通過Context層級關係和Groovy Bean,能以非常低的成本,實現Bean的動態加/解除安裝,這在業務場景經常變更的應用中極為實用,無需引入諸如OSGI之類的框架,簡化應用自身的複雜度。

4. 參考

Spring動態語言支援

Spring的任務執行與排程