先来安利一发Groovy
Groovy是一种基于JVM(Java虚拟机)的敏捷开发语言,它结合了Python、Ruby和Smalltalk的许多强大的特性,Groovy 代码能够与 Java 代码很好地结合,也能用于扩展现有代码。由于其运行在 JVM 上的特性,Groovy 可以使用其他 Java 语言编写的库. Groovy是JVM的一个替代语言(替代是指可以用 Groovy 在Java平台上进行 Java 编程),使用方式基本与使用 Java代码的方式相同,该语言特别适合与Spring的动态语言支持一起使用,设计时充分考虑了Java集成,这使 Groovy 与 Java 代码的互操作很容易 简单来说Groovy是一种脚本语言,有脚本语言的优点 快,并且和Java关联性很强,学习成本低,这么多对我来说就够了。
现实中的一个痛点问题:
我们在针对一些变化性很强的规则或是场景的时候需要对一些代码进行修改,但是每次修改就需要上线,这样对我们的开发来说比较痛苦
方案:
采用zk+groovy的方式来解决普通场景下的这个问题
步骤一: 监听应用节点下的groovy配置项:默认以xxx.xxx.xxx.groovy.groovyname 节点的配置项作为groovy文件 文件名为groovyname,eg:com.src.project.groovy.test 监听zk的应用节点后,在相关的资源文件夹下生成test.groovy文件
步骤二: 刷新当前的SpringIoc相关的groovyBean的上下文,重载groovy文件
action:
@Service
public class ZKGroovyConfigListener extends Observable implements TreeCacheListener, InitializingBean, ApplicationContextAware {
private Logger LOG = LoggerFactory.getLogger(ZKGroovyConfigListener.class);
private TreeCache cache;
private ApplicationContext applicationContext;
private final String GROOVY_CONFIG_PREFIX = "xxx.xxx.groovy.";
@Autowired
@Qualifier(value = "zookeeperConfigProperties")
private ZookeeperConfigProperties properties;
@Autowired
@Qualifier(value = "curator")
private CuratorFramework curator;
@Override
public void childEvent(CuratorFramework client, TreeCacheEvent event) throws Exception {
//监听节点的变化
if (event.getType().equals(TreeCacheEvent.Type.NODE_ADDED)
|| event.getType().equals(TreeCacheEvent.Type.NODE_UPDATED) || event.getType().equals(TreeCacheEvent.Type.NODE_REMOVED)) {
String path = event.getData().getPath();
String appName = applicationContext.getEnvironment().getProperty("application.name");
int appNameIndex = path.indexOf("/" + appName + "/");
if (appNameIndex > 0) {
String key = PathKeyUtil.sanitizeKey(path.substring(appNameIndex + appName.length() + 1));
if (StringUtils.isNotBlank(key) && key.startsWith(GROOVY_CONFIG_PREFIX)) {
String newVal = new String(event.getData().getData(), Charset.forName("UTF-8"));
EventNode node = buildNode(key.substring(GROOVY_CONFIG_PREFIX.length()), newVal, event.getType());
setChanged();
notifyObservers(node);
}
}
}
}
public void initTreeListener() throws Exception {
this.cache = TreeCache.newBuilder(this.curator, "/" + this.properties.getRoot() + "/" + applicationContext.getEnvironment().getProperty("application.name")).build();
this.cache.getListenable().addListener(this);
this.cache.start();
}
@Override
public void afterPropertiesSet() throws Exception {
try {
initTreeListener();
} catch (Exception e) {
LOG.error("ZKGroovyConfigListener.initTreeListener error!! errorMsg:{}", e.getMessage(), e);
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
private EventNode buildNode(String groovyName, String content, TreeCacheEvent.Type type) {
EventNode eventNode = new EventNode();
eventNode.setType(type);
eventNode.setGroovyFileName(groovyName);
eventNode.setGroovyContent(content);
return eventNode;
}
@Data
public static class EventNode {
private TreeCacheEvent.Type type;
private String groovyFileName;
private String groovyContent;
}
}
此类用作监听相关节点的变化
@Service
public class GroovyLoaderObserver implements Observer, InitializingBean, ApplicationContextAware {
private static final String DEFAULT_GROOVY_FILE_PATH_PREFIX = "/script/groovy";
@Resource
ZKGroovyConfigListener listener;
private ApplicationContext applicationContext;
@Override
public void update(Observable o, Object arg) {
ZKGroovyConfigListener.EventNode eventNode = (ZKGroovyConfigListener.EventNode) arg;
if (Optional.ofNullable(eventNode).isPresent()) {
StringBuilder pathsb = new StringBuilder(this.getClass().getClassLoader().getResource(DEFAULT_GROOVY_FILE_PATH_PREFIX).getPath());
pathsb.setLength(pathsb.length() - 1);
String filePath = pathsb.append(File.separator).append(eventNode.getGroovyFileName()).append(".groovy").toString();
String finalFilePath = filePath.replace("/", File.separator).substring(1);
File file = new File(finalFilePath);
String content = TreeCacheEvent.Type.NODE_REMOVED == eventNode.getType() ? "" : eventNode.getGroovyContent();
try {
Files.write(content, file, Charsets.UTF_8);
} catch (IOException e) {
e.printStackTrace();
}
//refresh SpringContent
refreshSpringContentDueToGroovy(file);
}
}
private void refreshSpringContentDueToGroovy(File file) {
if (Optional.ofNullable(this.applicationContext).isPresent()) {
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) ((AnnotationConfigEmbeddedWebApplicationContext) this.applicationContext).getBeanFactory();
GenericBeanDefinition bd = new GenericBeanDefinition();
bd.setBeanClassName("org.springframework.scripting.groovy.GroovyScriptFactory");
bd.setAttribute("org.springframework.scripting.support.ScriptFactoryPostProcessor.language", "groovy");
bd.setAttribute("org.springframework.scripting.support.ScriptFactoryPostProcessor.refreshCheckDelay", 10000);
bd.getConstructorArgumentValues().addIndexedArgumentValue(0, "classpath:" + DEFAULT_GROOVY_FILE_PATH_PREFIX + "/" + file.getName());
beanFactory.registerBeanDefinition(file.getName().replace(".groovy", ""), bd);
ScriptFactoryPostProcessor processor = applicationContext.getBean(ScriptFactoryPostProcessor.class);
try {
processor.postProcessBeforeInstantiation(Class.forName(bd.getBeanClassName()), file.getName().replace(".groovy", ""));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
@Override
public void afterPropertiesSet() throws Exception {
listener.addObserver(this);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
此类用作更新本地的groovy文件,并刷新相关Groovy Bean的IOC容器
配置文件
<bean class="org.springframework.scripting.support.ScriptFactoryPostProcessor"/>
这样就ok了
zk负责动态修改/新增相关的groovy脚本,zklistener监听节点的变化,更新本地的脚本,observer刷新当前的IOC容器中Bean的部分,完成脚本与Spring的结合。
对于一些动态内容的修改,我们可以直接修改groovy脚本就可以啦。