序文
しばらく前に友人とチャットしていました。彼は春の単一プロジェクトを手元に持っていました。データベース構成が変更されるたびに、構成を有効にするためにプロジェクトを再起動する必要がありました。彼は、プロジェクトを再起動せずに構成を有効にする方法があるかどうかを知りたかっただけです。その際、私は「コンフィグレーションセンターを利用できます」と伝えましたが、それは保守プロジェクトなので、追加のコンフィグレーションセンターを導入して運用保守コストが増加するのは避けたいという意味でした。その後、構成ファイルの変更を監視し、ファイルの変更が検出されると、対応する変更操作を実行するプログラムを実装する計画について彼と話し合いました。具体的な処理は以下の通りですが
、この中で一番面倒なのは、友人がSpringプロジェクトなので、Beanの動的リフレッシュをどのように実装するかということですが、今日はSpringプロジェクトでBeanの動的リフレッシュを実装する方法についてお話しましょう。
実装のアイデア
Spring に詳しい友人は、Spring のシングルトン Bean が singletonObjects マップにキャッシュされているため、singletonObjects を変更することで Bean を更新できることを知っているはずです。これは、removeSingleton メソッドと addSingleton メソッドを呼び出すことで実現できますが、この実装の欠点は、Bean のライフサイクルが変更され、AOP などの元の拡張機能の一部が失敗することです。しかし、非常に優れたフレームワークである Spring は、Bean を自分で管理できる拡張ポイントを提供します。この拡張ポイントは、スコープを指定することで Bean を管理する効果を実現します。
実装手順
1. カスタムスコープ
public class RefreshBeanScope implements Scope {
private final Map<String,Object> beanMap = new ConcurrentHashMap<>(256);
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
if(beanMap.containsKey(name)){
return beanMap.get(name);
}
Object bean = objectFactory.getObject();
beanMap.put(name,bean);
return bean;
}
@Override
public Object remove(String name) {
return beanMap.remove(name);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
}
@Override
public Object resolveContextualObject(String key) {
return null;
}
@Override
public String getConversationId() {
return null;
}
}
2. カスタムスコープの登録
public class RefreshBeanScopeDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
beanFactory.registerScope(SCOPE_NAME,new RefreshBeanScope());
}
}
3. カスタムスコープの注釈 (オプション)
@Target({
ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refreshBean")
@Documented
public @interface RefreshBeanScope {
/**
* @see Scope#proxyMode()
* @return proxy mode
*/
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}
4. カスタム スコープ Bean 更新ロジックを作成する
@RequiredArgsConstructor
public class RefreshBeanScopeHolder implements ApplicationContextAware {
private final DefaultListableBeanFactory beanFactory;
private ApplicationContext applicationContext;
public List<String> refreshBean(){
String[] beanDefinitionNames = beanFactory.getBeanDefinitionNames();
List<String> refreshBeanDefinitionNames = new ArrayList<>();
for (String beanDefinitionName : beanDefinitionNames) {
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanDefinitionName);
if(SCOPE_NAME.equals(beanDefinition.getScope())){
beanFactory.destroyScopedBean(beanDefinitionName);
beanFactory.getBean(beanDefinitionName);
refreshBeanDefinitionNames.add(beanDefinitionName);
applicationContext.publishEvent(new RefreshBeanEvent(beanDefinitionName));
}
}
return Collections.unmodifiableList(refreshBeanDefinitionNames);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
上記の手順は、カスタム スコープ管理 Bean を実装するプロセスです。以下では、構成変更による Bean の更新の例を使用して、上記の手順を示します。
例
1. プロジェクトの src/main/rescoures ディレクトリにプロパティ構成ファイル config/config.properties を作成します。
そしてテスト内容を記入してください
test:
name: zhangsan2222
2.config.ymlをSpringにロードします。
public static void setConfig() {
String configLocation = getProjectPath() + "/src/main/resources/config/config.yml";
System.setProperty("spring.config.additional-location",configLocation);
}
public static String getProjectPath() {
String basePath = ConfigFileUtil.class.getResource("").getPath();
return basePath.substring(0, basePath.indexOf("/target"));
}
3. 構成監視の実装
注: これは、 hutool の WatchMonitor または Apache common io のファイル監視を使用して実現できます。
Apache 共通 IO を例に挙げます
a.ビジネス pom ファイルに common-io gav を導入します
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${common-io.version}</version>
</dependency>
b.カスタム ファイル変更リスナー
@Slf4j
public class ConfigPropertyFileAlterationListener extends FileAlterationListenerAdaptor {
private ApplicationContext applicationContext;
public ConfigPropertyFileAlterationListener(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Override
public void onStart(FileAlterationObserver observer) {
super.onStart(observer);
}
@Override
public void onDirectoryCreate(File directory) {
super.onDirectoryCreate(directory);
}
@Override
public void onDirectoryChange(File directory) {
super.onDirectoryChange(directory);
}
@Override
public void onDirectoryDelete(File directory) {
super.onDirectoryDelete(directory);
}
@Override
public void onFileCreate(File file) {
super.onFileCreate(file);
}
@Override
public void onFileChange(File file) {
log.info(">>>>>>>>>>>>>>>>>>>>>>>>> Monitor PropertyFile with path --> {}",file.getName());
refreshConfig(file);
}
@Override
public void onFileDelete(File file) {
super.onFileDelete(file);
}
@Override
public void onStop(FileAlterationObserver observer) {
super.onStop(observer);
}
}
c.ファイルリスナーを開始します
@SneakyThrows
private static void monitorPropertyChange(FileMonitor fileMonitor, File file,ApplicationContext context){
if(fileMonitor.isFileScanEnabled()) {
String ext = "." + FilenameUtils.getExtension(file.getName());
String monitorDir = file.getParent();
//轮询间隔时间
long interval = TimeUnit.SECONDS.toMillis(fileMonitor.getFileScanInterval());
//创建文件观察器
FileAlterationObserver observer = new FileAlterationObserver(
monitorDir, FileFilterUtils.and(
FileFilterUtils.fileFileFilter(),
FileFilterUtils.suffixFileFilter(ext)));
observer.addListener(new ConfigPropertyFileAlterationListener(context));
//创建文件变化监听器
FileAlterationMonitor monitor = new FileAlterationMonitor(interval, observer);
//开始监听
monitor.start();
}
}
4. ファイルの変更を監視し、PropertySource と Bean を更新します。
@SneakyThrows
private void refreshConfig(File file){
ConfigurableEnvironment environment = applicationContext.getBean(ConfigurableEnvironment.class);
MutablePropertySources propertySources = environment.getPropertySources();
PropertySourceLoader propertySourceLoader = new YamlPropertySourceLoader();
List<PropertySource<?>> propertySourceList = propertySourceLoader.load(file.getAbsolutePath(), applicationContext.getResource("file:"+file.getAbsolutePath()));
for (PropertySource<?> propertySource : propertySources) {
if(propertySource.getName().contains(file.getName())){
propertySources.replace(propertySource.getName(),propertySourceList.get(0));
}
}
RefreshBeanScopeHolder refreshBeanScopeHolder = applicationContext.getBean(RefreshBeanScopeHolder.class);
List<String> strings = refreshBeanScopeHolder.refreshBean();
log.info(">>>>>>>>>>>>>>> refresh Bean :{}",strings);
}
5. テスト
a.コントローラーを作成し、コントローラーのスコープをカスタム スコープに設定します。
@RestController
@RequestMapping("test")
@RefreshBeanScope
public class TestController {
@Value("${test.name: }")
private String name;
@GetMapping("print")
public String print(){
return name;
}
}
元の test.name の内容は次のとおりです
test:
name: zhangsan2222
ブラウザ経由でアクセスする
b.現時点ではサーバーを再起動せず、test.name を次のように変更します。
test:
name: zhangsan3333
この時点で、コンソールにログ情報が出力されることがわかりました。
ブラウザ経由で再度アクセスする
内容が変わっていることが分かりました
付録: カスタマイズされたスコープ メソッドのトリガー タイミング
1、scope get方法
// Create bean instance.
if (mbd.isSingleton()) {
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
// Explicitly remove instance from singleton cache: It might have been put there
// eagerly by the creation process, to allow for circular reference resolution.
// Also remove any beans that received a temporary reference to the bean.
destroySingleton(beanName);
throw ex;
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
else if (mbd.isPrototype()) {
// It's a prototype -> create a new instance.
Object prototypeInstance = null;
try {
beforePrototypeCreation(beanName);
prototypeInstance = createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}
else {
String scopeName = mbd.getScope();
final Scope scope = this.scopes.get(scopeName);
if (scope == null) {
throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
}
try {
Object scopedInstance = scope.get(beanName, () -> {
beforePrototypeCreation(beanName);
try {
return createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
});
bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
}
catch (IllegalStateException ex) {
throw new BeanCreationException(beanName,
"Scope '" + scopeName + "' is not active for the current thread; consider " +
"defining a scoped proxy for this bean if you intend to refer to it from a singleton",
ex);
}
}
}
catch (BeansException ex) {
cleanupAfterBeanCreationFailure(beanName);
throw ex;
}
トリガー時間は、getBean が呼び出されたときです。
2、scope remove方法
@Override
public void destroyScopedBean(String beanName) {
RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
if (mbd.isSingleton() || mbd.isPrototype()) {
throw new IllegalArgumentException(
"Bean name '" + beanName + "' does not correspond to an object in a mutable scope");
}
String scopeName = mbd.getScope();
Scope scope = this.scopes.get(scopeName);
if (scope == null) {
throw new IllegalStateException("No Scope SPI registered for scope name '" + scopeName + "'");
}
Object bean = scope.remove(beanName);
if (bean != null) {
destroyBean(beanName, bean, mbd);
}
}
トリガー時は実際に destroyScopedBean メソッドを呼び出します。
要約する
Spring Cloud RefreshScope について調査すると、上記の実装が RefreshScope の大まかなバージョンであることがわかります。
デモリンク
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-bean-refresh