在开发过程中,经常需要修改配置文件来调整应用程序的行为。但是,如果每次修改配置文件都需要重启应用程序,那么将会浪费很多时间。为了解决这个问题,我们可以使用观察者模式来监听配置文件的变化,并借助apache.commons库来实现配置修改准实时生效,而不用重启SpringBoot应用。
目录
观察者模式简介
在观察者模式中,有两个核心角色:主题和观察者。主题是被观察的对象,观察者是观察主题的对象。当主题状态发生变化时,它会通知所有的观察者,让它们自动更新。这种模式中,主题和观察者是松耦合的,它们之间没有直接的依赖关系。这样,当我们需要增加新的观察者时,只需要实现观察者接口即可,不需要修改主题的代码。
实现步骤
1. 添加依赖
首先,在pom.xml文件中添加apache.commons库的依赖:
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.8</version>
</dependency>
2. 主题配置
然后,我们配置主题,使用apache.commons库中的FileAlterationMonitor类来实现对文件的变化监听,检查文件变化的时间间隔使用默认值10秒。
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.filefilter.FileFilterUtils;
import org.apache.commons.io.filefilter.HiddenFileFilter;
import org.apache.commons.io.filefilter.IOFileFilter;
import org.apache.commons.io.monitor.FileAlterationListener;
import org.apache.commons.io.monitor.FileAlterationMonitor;
import org.apache.commons.io.monitor.FileAlterationObserver;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.io.File;
@Slf4j
@Configuration
public class FileMonitorConfig {
private FileAlterationMonitor monitor;
@PostConstruct
public void init() {
monitor = new FileAlterationMonitor();
try {
monitor.start();
} catch (Exception e) {
log.warn("{}", e.getLocalizedMessage());
}
}
public void addFileAlteration(String fileDir, String fileName, FileAlterationListener listener) {
IOFileFilter directories = FileFilterUtils.and(FileFilterUtils.directoryFileFilter(), HiddenFileFilter.VISIBLE);
// FileFilterUtils.suffixFileFilter(fileSuffix) 可监听具有某个文件名后缀的一些文件
// IOFileFilter files = FileFilterUtils.and(FileFilterUtils.fileFileFilter(), FileFilterUtils.suffixFileFilter(fileSuffix));
IOFileFilter files = FileFilterUtils.and(FileFilterUtils.fileFileFilter(), FileFilterUtils.nameFileFilter(fileName));
IOFileFilter filter = FileFilterUtils.or(directories, files);
FileAlterationObserver observer = new FileAlterationObserver(fileDir, filter);
observer.addListener(listener);
try {
log.info("监听{}{}{}文件变化", fileDir, fileDir.endsWith(File.separator) ? "" : File.separatorChar, fileName);
monitor.addObserver(observer);
} catch (Exception e) {
log.warn("{}", e.getLocalizedMessage());
}
}
}
3. 创建观察者
最后,创建配置文件的观察者。在观察者的事件处理接口实现中,解析yml配置文件并更新当前应用环境配置。通过调试在MutablePropertySources mps后设置断点,可以看到mps中有多个PropertySource,有来自本地resources/config目录的,如果使用配置中心,有来自配置中心的,如果配置了spring.config.additional-location,有来自附加配置目录的。
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.monitor.FileAlterationListenerAdaptor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.stereotype.Component;
import org.springframework.util.ResourceUtils;
import javax.annotation.PostConstruct;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Properties;
import java.util.stream.Collectors;
@Slf4j
@Component
public class AppConfigMonitor implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Autowired
ConfigurableEnvironment cfgableEnv;
@Autowired
FileMonitorConfig fileMonitorConfig;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
AppConfigMonitor.applicationContext = applicationContext;
}
public static String getConfigAddLocation() {
return applicationContext.getEnvironment().getProperty("spring.config.additional-location");
}
@PostConstruct
public void initEnv() {
String dynamicConfigDir = null;
String cfgAddDir = getConfigAddLocation();
if (StringUtils.isNotBlank(cfgAddDir)) {
if (!new File(cfgAddDir).exists()) {
log.warn("spring.config.additional-location directory {} does not exists", cfgAddDir);
}
dynamicConfigDir = cfgAddDir;
} else {
ClassPathResource cpr = new ClassPathResource("config");
if (!cpr.exists()) {
return;
}
try {
if (ResourceUtils.URL_PROTOCOL_FILE.equals(cpr.getURL().getProtocol())) {
//本地调试,监听本地resources/config目录的配置文件
dynamicConfigDir = cpr.getFile().getPath();
}
} catch (IOException e) {
e.printStackTrace();
}
}
if (dynamicConfigDir != null) {
String actProf = applicationContext.getEnvironment().getActiveProfiles()[0];
fileMonitorConfig.addFileAlteration(dynamicConfigDir, "application-" + actProf + ".yml", new YamlAlterationListener());
}
}
public class YamlAlterationListener extends FileAlterationListenerAdaptor {
@Override
public void onFileCreate(File file) {
log.info("load {}", file.getAbsolutePath());
updateEnvironment(file);
}
@Override
public void onFileChange(File file) {
log.info("update {}", file.getAbsolutePath());
updateEnvironment(file);
}
@Override
public void onFileDelete(File file) {
log.info("delete {}", file.getAbsolutePath());
MutablePropertySources mps = cfgableEnv.getPropertySources();
mps.stream().filter(p -> isFilePropertySource(p.getName(), file)).forEach(p -> mps.remove(p.getName()));
}
private void updateEnvironment(File file) {
YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
yaml.setResources(new FileSystemResource(file));
Properties props;
try {
props = yaml.getObject();
} catch (Exception e) {
log.error(e.getMessage());
return;
}
MutablePropertySources mps = cfgableEnv.getPropertySources();
List<PropertySource<?>> psList = mps.stream().filter(p -> isFilePropertySource(p.getName(), file)).collect(Collectors.toList());
if (psList.isEmpty()) {
mps.addLast(new PropertiesPropertySource(propertiesPropertySourceName(file.getAbsolutePath()), props));
} else {
psList.stream().forEach(p -> mps.replace(p.getName(), new PropertiesPropertySource(p.getName(), props)));
}
}
// 在MutablePropertySources中,yml配置文件的propertySourceName
private String propertiesPropertySourceName(String filePath) {
return "applicationConfig: [file:" + filePath + "]";
}
// 判断MutablePropertySources中某个PropertySource是否来自文件file
private boolean isFilePropertySource(String propertySourceName, File file) {
if (propertySourceName.contains(file.getName())) {
if (propertySourceName.contains("[file:" + file.getAbsolutePath() + "]")) {
return true;
}
if (file.getAbsolutePath().contains(":\\")) {
String winPath1 = StringUtils.replace(file.getAbsolutePath(), "\\", "/");
if (propertySourceName.contains("[file:" + winPath1 + "]")) {
return true;
}
} else {
String relativePath = "." + StringUtils.replace(file.getAbsolutePath(), System.getProperty("user.home"), "", 1);
if (propertySourceName.contains("[file:" + relativePath + "]")) {
return true;
}
}
}
return false;
}
}
}
也可以为properties配置文件创建观察者,使用以下方式解析:
Properties properties = new Properties();
fileInputStream = new FileInputStream(file);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(fileInputStream, StandardCharsets.UTF_8));
properties.load(bufferedReader);
现在,当我们修改配置文件时,应用程序会自动重新加载配置信息,不需要重启应用程序就可以获取到新的配置值。