基于spring的groovy动态加载框架
1 groovy loader背景
由于groovy动态语言的特性,使用方式与java一致,同时又特别适合与Spring的动态语言支持一起使用,所以基于java的groovy脚本的动态加载的使用场景还是比较多的
2 简介
动态加载指定目录下的groovy脚本,并将其注册为groovy bean,放置于ApplicationContext容器中,并使用命名空间进行分类区分(一个namespace对应于一个ApplicationContext)。同时能够动态感知到groovy脚本的新增、修改以及删除事件,并自动重新加载。
3 原理
- 使用spring配置文件来管理注册groovy bean:每一个spring配置文件作为一个ApplicationContext,管理一个namespace下的groovy bean
- spring配置文件使用标签
<lang:groovy>
,通过指定script-source来加载指定路径下的groovy脚本,通过refresh-check-delay属性来定时动态加载每个groovy bean - 通过扫描监听指定路径下spring配置文件的变更,来接受groovy脚本的新增、删除事件
3.1 监听器listener
由于我们需要动态获取groovy脚本的变更,包含更新、新增、删除。接收到groovy脚本变更后需要触发用户自定义事件,所以我们先提供一个待用户实现的监听器接口,用于接受groovy变更事件,并执行相应代码
- GroovyRefreshedEvent
public class GroovyRefreshedEvent {
private ApplicationContext ancestorContext;
private Map<String, FileSystemXmlApplicationContext> namespacedContextMap;
public GroovyRefreshedEvent(ApplicationContext ancestorContext, Map<String, FileSystemXmlApplicationContext> contextMap) {
this.ancestorContext = ancestorContext;
this.namespacedContextMap = contextMap;
}
...
}
上述提到过,指定目录下的groovy脚本会被注册为groovy bean,并使用Applocation容器进行管理,同时还引入了namespace的概念,每个namespace分别对应一个Application容器,允许namespace不同但是类名相关的groovy bean存在。于是监听器的Event对象被设计成包含一个父容器以及namespace为key,对应Applocation容易为value的Map。
- GroovyRefreshedListener
public interface GroovyRefreshedListener { void groovyRefreshed(GroovyRefreshedEvent event); }
这个需要用户来实现
3.2 触发器trigger
由于需要动态去发现groovy脚本的变更,需要通过定时轮训使用触发器去判断groovy脚本是否完成了变更。上述提到过,框架通过<lang:Groovy>
来定时加载groovy脚本,通过扫描监听指定路径下spring配置文件的变更。通过这样的方式来接受groovy脚本的新增、删除事件。同样触发器也允许用户自定义,框架提供了一个默认的触发器
- GroovyRefreshedListener
public interface GroovyRefreshTrigger { boolean isTriggered(Map<String, Long> resourcesLastModifiedMap, String groovyResourcesDir); }
- ResourceModifiedTrigger
public class ResourceModifiedTrigger implements GroovyRefreshTrigger { public boolean isTriggered(Map<String, Long> resourcesLastModifiedMap, String groovyResourcesDir) { if (StringUtils.isBlank(groovyResourcesDir)) { groovyResourcesDir = this.getClass().getClassLoader().getResource("").getPath() + "/spring/groovy"; } File groovyFileDir = new File(groovyResourcesDir); List<File> groovyFileList = NamespacedGroovyLoader.getResourceListFromDir(groovyFileDir); for (File file : groovyFileList) { //新增 if (!resourcesLastModifiedMap.containsKey(file.getName())) { return true; } else { //修改 if (resourcesLastModifiedMap.get(file.getName()) != file.lastModified()) { return true; } } } //删除 if (resourcesLastModifiedMap.size() != groovyFileList.size()) { return true; } return false; } }
默认的触发器就是通过spring配置文件上一次修改的时间,去判断文件是否被修改
3.3 groovy loader
初始化时会将指定目录下的groovy脚本动态加载成bean,并根据namespace注册到不同的Application容器中。注:以spring配置文件的名称作为namespace
private void initLoadResources() {
if (MapUtils.isNotEmpty(this.namespacedContext)) {
toDestoryContext = new ArrayList<FileSystemXmlApplicationContext>(this.namespacedContext.values());
}
this.namespacedContext = new HashMap<String, FileSystemXmlApplicationContext>();
this.resourcesLastModifiedMap = new ConcurrentHashMap<String, Long>();
//定位资源文件路径
if (StringUtils.isBlank(groovyResourcesDir)) {
groovyResourcesDir = this.getClass().getClassLoader().getResource("").getPath() + "/spring/groovy";
}
File groovyFileDir = new File(groovyResourcesDir);
List<File> groovyFileList = getResourceListFromDir(groovyFileDir);//获取指定目录下所有spring配置文件
for (File file : groovyFileList) {
FileSystemXmlApplicationContext context = new FileSystemXmlApplicationContext(new String[] {file.toURI().toString()}, true, parentContext);
this.namespacedContext.put(file.getName().replace("xml", ""), context);
this.resourcesLastModifiedMap.put(file.getName(), file.lastModified());
}
//触发监听器事件
listener.groovyRefreshed(new GroovyRefreshedEvent(parentContext, this.namespacedContext));
if (CollectionUtils.isNotEmpty(toDestoryContext)) {
for (FileSystemXmlApplicationContext fileSystemXmlApplicationContext : toDestoryContext) {
fileSystemXmlApplicationContext.close();
}
}
}
3.4 spring配置文件
<?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:annotation-config />
<lang:groovy id="test" refresh-check-delay="2000" proxy-target-class="true"
script-source="classpath:groovy/one/Test.groovy" />
</beans>
4 使用
<bean id="listener" class="org.cuner.groovy.loader.test.TestListener"/><!--需要实现org.cuner.groovy.loader.listener.GroovyRefreshedListener -->
<bean id="groovyLoader" class="org.cuner.groovy.loader.NamespacedGroovyLoader">
<property name="groovyResourcesDir" value=""/><!--指定spring groovy配置文件目录,若不设置或者为空则默认为classpath下/spring/groovy目录-->
<property name="listener" ref="listener"/>
<property name="trigger" ref="trigger"/>
</bean>
5 源码&Demo
代码托管在Github上,并附有使用demo,欢迎下载运行: https://github.com/Cuner/groovy-loader
6 版本升级 groovy-loader-v2
v1版本存在的缺陷在于:由于使用了<lang:groovy>
标签,spring定时去重加载groovy bean,即使这个bean没有被修改,由此会产生一些性能消耗问题。为了解决这点,v2实现了groovy脚本的加载以及groovy bean的注入。同时由定时扫描spring配置改为扫描目录下的所有groovy脚本来实现触发器。
- GroovyScriptsModifiedTrigger
public boolean isTriggered(Map<String, Long> lastScriptsModified, String baseDir) { if (StringUtils.isBlank(baseDir)) { baseDir = this.getClass().getClassLoader().getResource("").getPath() + "/groovy"; } List<File> fileList = getFileList(new File(baseDir)); for (File file : fileList) { if (lastScriptsModified.get(file.getPath()) == null) { return true; } if (file.lastModified() != lastScriptsModified.get(file.getPath())) { return true; } } if (fileList.size() != lastScriptsModified.size()) { return true; } return false; }
具体实现基本一致,区别在于外部传参baseDir由spring配置文件路径改为groovy脚本根目录路径。接下来我们看看是如何实现groovy脚本的加载以及groovy bean的注入。
- NamespacedGroovyLoader
/**
* 递归查找并加载namespace下的所有groovy文件
* @param file
* @param namespace
*/
private void scanGroovyFiles(File file, String namespace) throws Exception {
if (!file.exists()) {
return;
}
if (file.isFile() && file.getName().endsWith(".groovy")) {
ApplicationContext context = getOrCreateContext(namespace);
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getAutowireCapableBeanFactory();
String scriptLocation = file.toURI().toString();
if (scriptNotExists(context, scriptLocation)) {
throw new IllegalArgumentException("script not exists : " + scriptLocation);
}
scriptLastModifiedMap.put(file.getPath(), file.lastModified());
String className = StringUtils.removeEnd(scriptLocation.substring(scriptLocation.indexOf(baseDir) + baseDir.length() + 1).replace("/", "."), ".groovy");
// 只有GroovyBean声明的类才实例化
DSLScriptSource scriptSource = new DSLScriptSource(new ResourceScriptSource(context.getResource(scriptLocation)), className);
Class scriptClass = groovyClassLoader.parseClass(scriptSource.getScriptAsString(), scriptSource.suggestedClassName());
// Tell Groovy we don't need any meta
// information about these classes
GroovySystem.getMetaClassRegistry().removeMetaClass(scriptClass);
groovyClassLoader.clearCache();
// Create script factory bean definition.
GroovyScriptFactory groovyScriptFactory = new GroovyScriptFactory(scriptLocation);
groovyScriptFactory.setBeanFactory(beanFactory);
groovyScriptFactory.setBeanClassLoader(urlClassLoader);
Object bean = groovyScriptFactory.getScriptedObject(scriptSource);
if (bean == null) {
//只有静态方法的groovy脚本(没有类声明)
return;
}
// Tell Groovy we don't need any meta
// information about these classes
GroovySystem.getMetaClassRegistry().removeMetaClass(bean.getClass());
groovyScriptFactory.getGroovyClassLoader().clearCache();
String beanName = StringUtils.removeEnd(file.getName(), ".groovy").toLowerCase();
if (beanFactory.containsBean(beanName)) {
beanFactory.destroySingleton(beanName); //移除单例bean
removeInjectCache(context, bean); //移除注入缓存 否则Caused by: java.lang.IllegalArgumentException: object is not an instance of declaring class
}
beanFactory.registerSingleton(beanName, bean); //注册单例bean
beanFactory.autowireBean(bean); //自动注入
} else if (file.isDirectory()) {
File[] subFiles = file.listFiles();
for (File subFile : subFiles) {
scanGroovyFiles(subFile, namespace);
}
}
}
groovy文件夹下第一级文件夹名称拼接namespacePrefix作为namespace,每个namespace分配一个ApplicationContext容器,每个namespace对应的bean都由各自的beanfactory加载注入,避免同名类加载导致的错误。同时groovy脚本更新后产生新的groovy bean之后,还需要移除之前的groovy bean,值得额外注意的是需要移除注入缓存,否则会报错:Caused by: java.lang.IllegalArgumentException: object is not an instance of declaring class 注意:由于我们不需要groovy的metaclass信息,这里对metaClass进行清除来进行优化
最后,附上代码:https://github.com/Cuner/groovy-loader-v2