前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[享学Netflix] 二、Apache Commons Configuration事件监听机制及使用ReloadingStrategy实现热更新

[享学Netflix] 二、Apache Commons Configuration事件监听机制及使用ReloadingStrategy实现热更新

作者头像
YourBatman
发布2020-02-21 16:46:31
1.4K0
发布2020-02-21 16:46:31
举报
文章被收录于专栏:BAT的乌托邦BAT的乌托邦

你应该思考:我们不去做的事,哪些是因为克制?哪些是因为懒惰?

代码下载地址:https://github.com/f641385712/netflix-learning

前言

上篇文章 概要性的介绍了Apache Commons Configuration,并且了解了它的核心API以及使用。但它带给我们的功能还不仅于此,比如本文要讲到的事件监听机制和热更新。

事件-监听机制:能在“修改”(增删改)属性值的时候发送对应事件,让感兴趣的监听者执行其对应的逻辑。 热更新这个词各位开发者肯定不陌生,在实际生产中经常会遇到有类似的需求场景。ReloadingStrategy表示重载策略,Commons Configuration通过它来帮助我们实现热更新、热加载的效果,关键时刻不用再重启应用而掉链子。

作为一个有经验的开发人员,对事件-监听机制(同义词:观察者模式)应该是不陌生的,比如Java源生的就有

  • 事件源:java.util.EventObject
  • 监听者(观察者):java.util.Observer / java.util.EventListener
  • 被观察对象:java.util.Observable

除了Java源生的,还有大家更为熟悉的Spring里的事件监听实现:

  • 事件源:org.springframework.context.ApplicationEvent
  • 监听者:org.springframework.context.ApplicationListener
  • 事件广播器:org.springframework.context.event.ApplicationEventMulticaster

正文

AbstractConfiguration派生的所有配置类均允许注册事件侦听器,只要更改配置数据,便会通知事件侦听器,这样使用者就可以根据需要,定制自己关心的事件来实现自我的逻辑。

几乎所有的Configuration子类均直接/间接的从AbstractConfiguration派生出来,而对于上文描述的常见的几个子类全部派生自AbstractConfiguration,因此具有事件-监听的能力。 Apache Commons Configuration有它自己的事件-监听相关API:

  • 事件源:org.apache.commons.configuration.event.ConfigurationEvent / ConfigurationErrorEvent
  • 监听者:ConfigurationListener / ConfigurationErrorListener

EventSource作为基类,用于收集、管理监听“自己”的xxxListener监听器们。


事件-监听

ConfigurationEvent

用于报告配置对象更新的事件类,它继承自Java标注事件对象:java.util.EventObject,代表着一个事件。

代码语言:javascript
复制
public class ConfigurationEvent extends EventObject {
	
	// 事件类型
	private int type;
	// 属性名
	private String propertyName;
	// 属性值
	private Object propertyValue;
	// update之前标志位 默认是false
	// 标记此事件是在源配置更新之前还是之后生成的。
	private boolean beforeUpdate;

	// 请注意:父类还有个最重要的属性:事件对象  它是不能为null的
	protected transient Object  source;
	
	... // 省略构造器,以及所有get方法
}

该事件Event可以存储如下几部分信息:

  1. 一个源对象,通常是被修改的Configuration对象。
  2. 事件的类型。这是一个数值,对应于具体配置类中的常量声明。它描述了到底发生了什么
    1. 有些实现是通过Class类型(比如Spring的事件)来区分同一源上的不同事件,而这里使用的是这么一个int值来区分的
  3. 导致此事件的属性的名称。
  4. 导致此事件的属性的值。
  5. 标记此事件是在源配置更新之前还是之后生成的(配置的修改通常会导致两个事件:执行修改之前的一个事件和执行修改之后的一个事件。这允许事件监听器在正确的时间点做出响应)

ConfigurationErrorEvent

错误事件类型,用于报告同时发生的错误的事件类处理配置属性,它继承自ConfigurationEvent

代码语言:javascript
复制
public class ConfigurationErrorEvent extends ConfigurationEvent {
	
	// 导致发送此事件的错误原因
	private Throwable cause;
	... // 省略构造器及getCause()方法
}

ConfigurationListener

配置观察者的简单事件监听接口。

代码语言:javascript
复制
public interface ConfigurationListener {
	void configurationChanged(ConfigurationEvent event);
}

对配置触发的更新事件感兴趣的对象必须实现ConfigurationListener接口。


ConfigurationErrorListener

不解释。

代码语言:javascript
复制
public interface ConfigurationErrorListener {
	void configurationError(ConfigurationErrorEvent event);
}

EventSource

用于生成配置事件的对象的基类。这个类实现了管理一组事件监听器的功能,可以在事件发生时通知(类似于Spring的事件广播器)。

操作可以并发地添加和删除事件监听器用于导致事件的配置,操作是同步的线程安全的(不同于Spring,它只能同步触发监听者逻辑)。

代码语言:javascript
复制
public class EventSource {

	// 注册的监听器们
	private Collection<ConfigurationListener> listeners;
	private Collection<ConfigurationErrorListener> errorListeners;

	private final Object lockDetailEventsCount = new Object();
	// 详细事件的计数器
	private int detailEvents;

	// 唯一构造器:完成集合的初始化 变为线程安全的集合CopyOnWriteArrayList
    public EventSource() {
        initListeners();
    }
    private void initListeners() {
        listeners = new CopyOnWriteArrayList<ConfigurationListener>();
        errorListeners = new CopyOnWriteArrayList<ConfigurationErrorListener>();
    }

	public void addConfigurationListener(ConfigurationListener l) { ... }
	public boolean removeConfigurationListener(ConfigurationListener l) { ... }
	// 返回的是个视图
	public Collection<ConfigurationListener> getConfigurationListeners() { ... }
    public void clearConfigurationListeners() {
        listeners.clear();
    }
    ... // 省略error的增删改查方法

	// 确定是否应生成详细事件。如果启用,一些方法可以生成多个更新事件。
	// 注意这个方法记录调用次数,例如如果setDetailEvents(false)}被调用三次
	// 您将必须经常调用该方法以启用细节。
	public void setDetailEvents(boolean enable) { ... detailEvents++; ... }
    private boolean checkDetailEvents(int limit) {
        synchronized (lockDetailEventsCount) {
            return detailEvents > limit;
        }
    }

	// 创建事件对象,并将其交付给所有已注册的事件。也就是让所有关心它的监听器执行
	// 注意:这里执行的正常事件(非错误事件)
	protected void fireEvent(int type, String propName, Object propValue, boolean before) {
		if (checkDetailEvents(-1)) {
			Iterator<ConfigurationListener> it = listeners.iterator();
			if (it.hasNext()) { // 这句相当于判空,下面while才是一个个执行
				ConfigurationEvent event = createEvent(type, propName, propValue, before);
				// 把监听器一个个的执行掉
                while (it.hasNext()) {
                    it.next().configurationChanged(event);
                }
			}
		}
	}
	protected void fireError(int type, String propName, Object propValue, Throwable ex) { ... }

	// 子类可复写构造一个ConfigurationEvent 实例的方法
	// 比如根据type、propName来判断等等逻辑
    protected ConfigurationEvent createEvent(int type, String propName, Object propValue, boolean before) {
        return new ConfigurationEvent(this, type, propName, propValue, before);
    }
	...
}

EventSource它提供了对监听器的维护,以及当对应事件到达时会触发注册上来的监听者们。如果你希望被监听,那就继承自此类吧。

AbstractConfiguration就继承自此类,因此它的派生子类们均可以被监听。同时此类也是EventSource唯一继承子类。


AbstractConfiguration

它是org.apache.commons.configuration.Configuration的通用抽象实现,并且继承了EventSource,从而允许其派生出的所有的子类均可被监听。

代码语言:javascript
复制
public abstract class AbstractConfiguration extends EventSource implements Configuration {

	// 它定义了很多事件类型哦,并且还都是public可访问的
	public static final int EVENT_ADD_PROPERTY = 1;
	public static final int EVENT_CLEAR_PROPERTY = 2;
	public static final int EVENT_SET_PROPERTY = 3;
	public static final int EVENT_CLEAR = 4;
	public static final int EVENT_READ_PROPERTY = 5;

	// 可以使用${}引用其它属性值
	protected static final String START_TOKEN = "${";
	protected static final String END_TOKEN = "}";

	// List请用逗号分隔
	private static char defaultListDelimiter = ',';
	
	... // 生路基本属性的赋值、get方法

	// 下面是对Configuration的改变方法,需要发出事件
	
	// 动作前、后都发出了对应的事件
    public void addProperty(String key, Object value) {
        fireEvent(EVENT_ADD_PROPERTY, key, value, true);
        addPropertyValues(key, value, isDelimiterParsingDisabled() ? DISABLED_DELIMITER : getListDelimiter());
        fireEvent(EVENT_ADD_PROPERTY, key, value, false);
    }
    // 抽象方法:直接添加 -> 不触发事件的发送
    protected abstract void addPropertyDirect(String key, Object value);

	// 同样的,前后均发送事件
	public void setProperty(String key, Object value) {
		fireEvent(EVENT_SET_PROPERTY, key, value, true);
		setDetailEvents(false);
        try {
            clearProperty(key);
            addProperty(key, value);
        } finally {
            setDetailEvents(true);
        }
        fireEvent(EVENT_SET_PROPERTY, key, value, false);
	}
	... // 其它的类同
}

AbstractConfiguration主要是对接口Configuration大部分方法的通用实现,并且在一些修改方法的前后均发送了事件,感兴趣的监听器也就会被触发。

另外你会发现fireError()方法并没有被触发过,这是因为AbstractConfiguration实现里并不会产生异常,但子类实现就不一定了,比如:

代码语言:javascript
复制
DatabaseConfiguration:

	public Object getProperty(String key) {
		...
		try {
			...
		} catch (SQLException e) {
			fireError(EVENT_READ_PROPERTY, key, null, e);
		}
		...
	}

JNDIConfiguration:

	public Object getProperty(String key) {
		...
		try {
			...
		} catch (NamingException e)
			fireError(EVENT_READ_PROPERTY, key, null, e);
			return null;
		}
		...
	}

使用示例

事件-监听机制使用起来其实并没有什么难度,它的难度在于使用过多的话理解上会稍微困难点,这点你应该在理解Spring Boot原理上深有体会。

代码语言:javascript
复制
@Test
public void fun4() throws ConfigurationException {
    PropertiesConfiguration configuration = new PropertiesConfiguration("1.properties");
    // 注册一个监听器
    configuration.addConfigurationListener(event -> {
        Object source = event.getSource(); // 事件源
        int type = event.getType();
        if(!event.isBeforeUpdate()){ // 只关心update后的事件,否则会执行两次哦,请务必注意
            System.out.println("事件源:" + source.getClass());
            System.out.println("事件type类型:" + type);

            // 处理你自己的逻辑
        }
    });

    // 增加一个属性,会同步触发监听器去执行
    configuration.addProperty("common.addition","additionOne");
    System.out.println(configuration.getString("common.addition"));
}

运行程序,控制台打印:

代码语言:javascript
复制
事件源:class org.apache.commons.configuration.PropertiesConfiguration
事件type类型:1
additionOne

ReloadingStrategy(热更新)

决定是否重新加载配置的策略接口。

代码语言:javascript
复制
public interface ReloadingStrategy {
	// 设置configuration被这个策略所管理的
	void setConfiguration(FileConfiguration configuration);
	
	// 初始化此策略
	void init();
	// 评估策略:是否需要重新加载配置
	boolean reloadingRequired();
	
	// 通知策略文件已被重新加载(通知)
	void reloadingPerformed();
}

该策略接口的实现类如下:

在这里插入图片描述
在这里插入图片描述

这里面InvariantReloadingStrategy相当于啥都不做:不做Reloading(所以它是默认策略,占位的意思)。所以下面仅只讲述大名鼎鼎的FileChangedReloadingStrategy,获取它才是我们编程中唯一可能会使用的实现类。


FileChangedReloadingStrategy

一种重新加载策略,每次更改基础文件时都将重新加载配置

注意:此重新加载策略不会主动监视配置文件,而是在访问属性时由其关联的配置触发。然后检查配置文件的最后修改日期,如果更改了,则重新加载。

说明:并不是实时的,有delay延迟,并且是你再次访问的时候再去检查是否有变化

为了避免在连续属性查找时永久访问磁盘,可以指定刷新延迟。这将导致在此延迟期间只检查一次配置文件的最后修改日期。此刷新延迟的默认值为5秒。此策略仅适用于File文件Configuration配置实例。

代码语言:javascript
复制
public class FileChangedReloadingStrategy implements ReloadingStrategy {

	private static final String JAR_PROTOCOL = "jar";
	// 默认最多5秒钟才会去访问一次磁盘,这就是最大延迟
	private static final int DEFAULT_REFRESH_DELAY = 5000;
	// 它的实例有形如:PropertiesConfiguration、XMLConfiguration等都是它的子类
	protected FileConfiguration configuration;
	// 最后一次修改的时间戳
	protected long lastModified;
	// minimum delay
	protected long refreshDelay = DEFAULT_REFRESH_DELAY;

	// 标志位:是否正在正在重新加载中
	private boolean reloading;
	
	...
	
	@Override
    public void setConfiguration(FileConfiguration configuration) {
        this.configuration = configuration;
    }
    // 初始化的时候,就touch一下lastModified时间戳
    @Override
    public void init() {
        updateLastModified();
    }
    @Override
    public void reloadingPerformed() {
        updateLastModified();
    }

    // 给lastModified重新赋值
    protected void updateLastModified() {
		// 拿到这个配置关联到的文件,如:configuration.getFile() / configuration.getURL()
        File file = getFile();
        if (file != null) {
            lastModified = file.lastModified();
        }
        reloading = false;
    }

	@Override
    public boolean reloadingRequired() {
    	// 如果没有正在重新加载中,那就进来处理
        if (!reloading) {
            long now = System.currentTimeMillis();
			// 继续判断:两次间隔超过了refreshDelay,才继续执行
            if (now > lastChecked + refreshDelay) {
                lastChecked = now;
				// file.lastModified() > lastModified 证明文件已经改变过啦
                if (hasChanged()) {
                    if (logger.isDebugEnabled())
                        logger.debug("File change detected: " + getName());
                    reloading = true;
                }
            }
        }
        return reloading;
    }
    ...
}

本类做的事情其实非常的少,主要还是strategy.reloadingRequired()这个方法提供一个标志位,在访问的时候是否需要重新加载:

代码语言:javascript
复制
AbstractFileConfiguration:

	protected ReloadingStrategy strategy;
    public void reload() {
        reload(false);
    }
    public boolean reload(boolean checkReload) {
		...
		if (strategy.reloadingRequired()) {
			// 发送事件
			refresh(); 
			strategy.reloadingPerformed();
		}
    }

	// 刷新 --> 重要方法
	public void refresh() throws ConfigurationException {
		... // 发送before事件,事件类型是:EVENT_RELOAD
		try {
			clear(); // 先情况,再加载
			load(); // 重新从文件里加载最新内容
		} finally { ... }
		clear();
		load();
		... // 发送after事件,事件类型是:EVENT_RELOAD
	}

这段代码解释到了,重新加载数据的逻辑:strategy.reloadingRequired()为true时,先clear()本实例的数据,再从文件内重新load()进该实例。对外暴露的方法是:reload()方法,它的调用处均为读方法

代码语言:javascript
复制
AbstractFileConfiguration:

    @Override
    public Object getProperty(String key) {
    	...
    	reload();
    	return super.getProperty(key);
    }
    @Override
    public boolean isEmpty() {
    	reload();
    	return super.isEmpty();
    }
    @Override
    public boolean containsKey(String key) {
    	reload();
    	return super.containsKey(key);
    }
    @Override
    public Iterator<String> getKeys() {
    	reload();
    	...
    }

使用示例

生产环境,有些配置是需要热加载的,不用重启服务器进行更新

下面就通过FileChangedReloadingStrategy来实现热更新,并且注册一个 ConfigurationListener监听RELOAD事件类型来告知被重新加载了(热更新成功)。

代码语言:javascript
复制
@Test
public void fun5() throws ConfigurationException {
    PropertiesConfiguration configuration = new PropertiesConfiguration("1.properties");

    // 监听到配置文件被重新加载了就输出一条日志喽~
    configuration.addConfigurationListener(event -> {
        // 只监听到重新加载事件
        if (event.getType() == PropertiesConfiguration.EVENT_RELOAD) {
            System.out.println("配置文件重载...");
            configuration.getKeys().forEachRemaining(k -> {
                System.out.println("/t " + k + "-->" + configuration.getString(k));
            });
        }
    });

    // 使用文件改变重载策略:让改变文件能热加载
    FileChangedReloadingStrategy reloadingStrategy = new FileChangedReloadingStrategy();
    reloadingStrategy.setRefreshDelay(3000L); // 设置最小事件间隔,单位是毫秒
    configuration.setReloadingStrategy(reloadingStrategy);

    // 使用另外一个线程模拟去get
    otherThreadGet(configuration);

    // hold住main线程,不让程序终止
    while (true) {}
}

private void otherThreadGet(PropertiesConfiguration configuration) {
    new Thread(() -> {
        while (true) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            configuration.getString("commmon.name");
        }
    }).start();

}

运行程序,改变1.properties文件并保存且重新编译,控制打印:

代码语言:javascript
复制
配置文件重载...
/t common.name-->YourBatman
/t common.age-->18
/t common.addr-->China
/t common.count-->4
/t common.out-->1
/t common.fullname-->YourBatman-Bryant
/t java.version-->1.8.123
配置文件重载...
// 注意:common.name这一行是被我动态删除掉了,所以此处木有
/t common.age-->18
/t common.addr-->China
/t common.count-->4
/t common.fullname-->YourBatman-Bryant
/t java.version-->1.8.123

因此案例在本地模拟起来实属不易,有几个坑在此做出说明,相信对你一定有帮助,一定要看哦,否则你自己运行可能出不来效果:

  1. 请hold住main线程,否则它结束了一切玩完
  2. 请使用另外线程去configuration的get操作,否则是无效果的
    1. 1.x只能自己模拟,2.x内部提供了PeriodicReloadingTrigger通过ScheduledExecutorService的方式解决此问题
  3. 修改文件时,请保存,并且并且并且一定要重新编译,否则不会生效的
    1. 当然我这里默认你的文件是在工程内的,若不在工程内就无需重新编译喽

总结

关于Apache Commons Configuration的时间监听机制,以及读者们非常关心,且觉得非常实用的热更新实现就介绍完了,是不是越发觉得这个配置库还是蛮不错的,别犹豫了,用它去管理你的配置文件们吧~

声明

原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 正文
    • 事件-监听
      • ConfigurationEvent
      • ConfigurationListener
      • EventSource
      • 使用示例
    • ReloadingStrategy(热更新)
      • FileChangedReloadingStrategy
      • 使用示例
  • 总结
    • 声明
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档