在 Spring Context 中重新初始化 Singleton Bean

1、概览

在本教程中,我们将了解如何在运行时重新初始化 Spring Context 中的 Singleton Bean。

默认情况下,Singleton Scope Bean 不会在应用生命周期中重新初始化。不过,有时可能需要重新创建 Bean,例如在需要更新属性时。我们将介绍几种实现此目的的方法。

2、示例

一个小示例。创建一个 Bean,从配置文件中读取属性保存在内存中。如果配置文件中的属性值被修改,那么 Bean 就要重新加载配置。

2.1、Singleton Bean

首先创建 ConfigManager 类:

@Service("ConfigManager")
public class ConfigManager {

    private static final Log LOG = LogFactory.getLog(ConfigManager.class);

    private Map<String, Object> config;

    private final String filePath;

    public ConfigManager(@Value("${config.file.path}") String filePath) {
        this.filePath = filePath;
        initConfigs();
    }

    private void initConfigs() {
        Properties properties = new Properties();
        try {
            properties.load(Files.newInputStream(Paths.get(filePath)));
        } catch (IOException e) {
            LOG.error("Error loading configuration:", e);
        }
        config = new HashMap<>();
        for (Map.Entry<Object, Object> entry : properties.entrySet()) {
            config.put(String.valueOf(entry.getKey()), entry.getValue());
        }
    }

    public Object getConfig(String key) {
        return config.get(key);
    }
}
  • 在构造函数中调用 initConfigs() 方法,可以在 Bean 创建后立即加载文件。
  • initConfigs() 方法会将文件内容转换为名为 configMap
  • getConfig() 方法用于通过 key 值读取属性。

注意,这里使用了基于构造函数的依赖注入。当需要替换 Bean 时,这很重要。

配置文件的路径为 src/main/resources/config.properties,包含一个属性:

property1=value1

2.2、Controller

创建一个 controller 来测试 ConfigManager

@RestController
@RequestMapping("/config")
public class ConfigController {

    @Autowired
    private ConfigManager configManager;

    @GetMapping("/{key}")
    public Object get(@PathVariable String key) {
        return configManager.getConfig(key);
    }
}

运行应用程序,并通过访问 URL http://localhost:8080/config/property1 读取配置。

接下来,我们要更改文件中的属性值,并再次访问同一 URL 读取修改后的值。让我们来看看几种方法。

3、使用 public 方法重写加载属性

如果我们想重新加载属性,而不是重新创建对象本身,只需创建一个再次初始化 map 的公共方法即可。

ConfigManager 中添加一个方法,调用 initConfigs() 方法:

public void reinitializeConfig() {
    initConfigs();
}

然后,当我们要重新加载属性时,就可以调用该方法。让我们在 Controller 中公开另一个调用 reinitializeConfig() 方法的方法:

@GetMapping("/reinitializeConfig")
public void reinitializeConfig() {
    configManager.reinitializeConfig();
}

现在,我们可以通过如下步骤进行测试:

  • 访问 URL http://localhost:8080/config/property1,返回 value1
  • 然后,修改配置文件,把 property1 的值从 value1 改为 value2
  • 然后,访问 URL http://localhost:8080/config/reinitializeConfig 来重新初始化 config map。
  • 再次访问 URL http://localhost:8080/config/property1,就会发现返回的值是 value2

4、重新初始化 Singleton Bean

重新初始化 Bean 的另一种方式是通过在 Context 中重新创建它。重新创建可以通过使用自定义代码调用构造函数或删除 Bean 并让 Context 自动重新初始化来完成。让我们看一下这两种方式。

4.1、替换 context 中的 Bean

我们可以从 Context 中删除 Bean,并用 ConfigManager 的新实例取而代之。让我们在 Controller 中定义另一个方法来实现:

@GetMapping("/reinitializeBean")
public void reinitializeBean() {
    DefaultSingletonBeanRegistry registry = (DefaultSingletonBeanRegistry) applicationContext.getAutowireCapableBeanFactory();
    registry.destroySingleton("ConfigManager");
    registry.registerSingleton("ConfigManager", new ConfigManager(filePath)); 
}

首先,我们从 Application Context 中获取 DefaultSingletonBeanRegistry 的实例。然后,我们调用 destroySingleton() 方法销毁名为 ConfigManager 的 Bean 实例。最后,我们创建一个 ConfigManager 的新实例,并调用 registerSingleton() 方法将其注册到 Factory。

ConfigManager Bean 所依赖的任何依赖都必须通过构造函数传递。

registerSingleton() 方法不仅在 Context 中创建 Bean,还会自动将其注入到依赖的对象中。

调用 /reinitializeBean 端点可更新 Controller 中的 ConfigManager Bean。我们可以使用与上述相同的步骤进行测试。

4.2、在 Context 中销毁 Bean

在前面的方法中,我们需要通过构造函数传递依赖。有时,我们可能不需要创建 Bean 的新实例,或者无法访问所需的依赖。在这种情况下,另一种方法就是在 Context 中销毁 Bean。

当再次请求 Bean 时,Context 将负责再次创建 Bean。在这种情况下,将使用与创建初始 Bean 相同的步骤创建 Bean。

为了演示这一点,让我们创建一个新的 Controller 方法来销毁 Bean,但不会再次创建它:

@GetMapping("/destroyBean")
public void destroyBean() {
    DefaultSingletonBeanRegistry registry = (DefaultSingletonBeanRegistry) applicationContext.getAutowireCapableBeanFactory();
    registry.destroySingleton("ConfigManager");
}

这不会改变 Controller 已经持有的对 Bean 的引用。要访问最新状态,我们需要直接从 Context 中读取。

创建一个新 Controller 来读取配置。该 Controller 将依赖于 Context 中最新的 ConfigManager Bean:

@GetMapping("/context/{key}")
public Object getFromContext(@PathVariable String key) {
    ConfigManager dynamicConfigManager = applicationContext.getBean(ConfigManager.class);
    return dynamicConfigManager.getConfig(key);
}

测试上述方法:

  • 访问 URL http://localhost:8080/config/context/property1,返回 value1
  • 然后,访问 URL http://localhost:8080/config/destroyBean,销毁 ConfigManager
  • 然后,修改配置文件,把 property1 的值从 value1 改为 value2
  • 再次访问 URL http://localhost:8080/config/context/property1,就会发现返回的值是 value2

5、总结

在本文中,我们学习了重新初始化 Singleton Bean 的方法。我们介绍了一种在不重新创建 Bean 的情况下修改其属性的方法,以及强制在 Context 中重新创建 Bean 的方法。


参考:https://www.baeldung.com/spring-reinitialize-singleton-bean