基于Groovy实现Spring Bean的动态加载

元气小坏坏 提交于 2019-11-30 03:54:04

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的动态加载/卸载效果。

2.3. 示例运行说明

示例的运行代码为Demo,通过JUnit来执行。依据提示输入规则的名字和参数查看规则执行效果,规则的可用参数在规则注释中有罗列。

规则配置和Groovy Bean的代码默认通过Http请求从Git@OSC上拉取,但可修改context.properties将其指定为从本地文件获取,更便于修改和测试。

另:

  1. 代码托管在Git@OSC上,欢迎下载运行,http://git.oschina.net/joshuazhan/demo4j.git
  2. 本示例的项目路径为“demo4j/spring/groovy/dynamic”,需要使用Maven来生成工程,“mvn eclipse:eclipse”;
  3. 示例指定了1.6的JDK版本,有需要可以在pom中修改jdk.version的值;
  4. 配置文件在“/src/main/resources”目录下。

3. 小结

通过Context层级关系和Groovy Bean,能以非常低的成本,实现Bean的动态加/卸载,这在业务场景经常变更的应用中极为实用,无需引入诸如OSGI之类的框架,简化应用自身的复杂度。

4. 参考

Spring动态语言支持
http://static.springsource.org/spring/docs/3.2.x/spring-framework-reference/html/dynamic-language.html

Spring的任务执行与调度

http://static.springsource.org/spring/docs/3.2.x/spring-framework-reference/html/scheduling.html

Blog已搬家到BAE,欢迎来访问http://home4j.duapp.com/index.php/2013/07/24/on-demand-spring-bean-config-based-on-groovy.html

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!