[英]How to hot-reload properties in Java EE and Spring Boot?
許多內部解決方案浮現在腦海中。 就像在數據庫中擁有屬性並每 N 秒輪詢一次。 然后還要檢查 a.properties 文件的時間戳修改並重新加載它。
但我正在查看 Java EE 標准和 spring 引導文檔,但我似乎無法找到一些最好的方法。
我需要我的應用程序讀取屬性文件(或環境變量或數據庫參數),然后能夠重新讀取它們。 生產中使用的最佳實踐是什么?
一個正確的答案至少會解決一個場景(Spring Boot 或 Java EE)並提供關於如何使其在另一個場景中工作的概念線索
經過進一步研究,必須仔細考慮重裝屬性。 例如,在 Spring 中,我們可以毫無問題地重新加載屬性的“當前”值。 但。 在上下文初始化時根據 application.properties 文件中存在的值(例如數據源、連接池、隊列等)初始化資源時,必須特別小心。
注意:
用於 Spring 和 Java EE 的抽象類並不是干凈代碼的最佳示例。 但它易於使用,並且確實滿足了以下基本的初始要求:
對於 Spring Boot
此代碼有助於在不使用 Spring Cloud Config 服務器的情況下熱重載 application.properties 文件(這對於某些用例可能有點過分)
這個抽象類你可以復制和粘貼(SO goodies :D)這是從這個 SO answer 派生的代碼
// imports from java/spring/lombok
public abstract class ReloadableProperties {
@Autowired
protected StandardEnvironment environment;
private long lastModTime = 0L;
private Path configPath = null;
private PropertySource<?> appConfigPropertySource = null;
@PostConstruct
private void stopIfProblemsCreatingContext() {
System.out.println("reloading");
MutablePropertySources propertySources = environment.getPropertySources();
Optional<PropertySource<?>> appConfigPsOp =
StreamSupport.stream(propertySources.spliterator(), false)
.filter(ps -> ps.getName().matches("^.*applicationConfig.*file:.*$"))
.findFirst();
if (!appConfigPsOp.isPresent()) {
// this will stop context initialization
// (i.e. kill the spring boot program before it initializes)
throw new RuntimeException("Unable to find property Source as file");
}
appConfigPropertySource = appConfigPsOp.get();
String filename = appConfigPropertySource.getName();
filename = filename
.replace("applicationConfig: [file:", "")
.replaceAll("\\]$", "");
configPath = Paths.get(filename);
}
@Scheduled(fixedRate=2000)
private void reload() throws IOException {
System.out.println("reloading...");
long currentModTs = Files.getLastModifiedTime(configPath).toMillis();
if (currentModTs > lastModTime) {
lastModTime = currentModTs;
Properties properties = new Properties();
@Cleanup InputStream inputStream = Files.newInputStream(configPath);
properties.load(inputStream);
environment.getPropertySources()
.replace(
appConfigPropertySource.getName(),
new PropertiesPropertySource(
appConfigPropertySource.getName(),
properties
)
);
System.out.println("Reloaded.");
propertiesReloaded();
}
}
protected abstract void propertiesReloaded();
}
然后創建一個 bean 類,允許從使用抽象類的 applicatoin.properties 檢索屬性值
@Component
public class AppProperties extends ReloadableProperties {
public String dynamicProperty() {
return environment.getProperty("dynamic.prop");
}
public String anotherDynamicProperty() {
return environment.getProperty("another.dynamic.prop");
}
@Override
protected void propertiesReloaded() {
// do something after a change in property values was done
}
}
確保將 @EnableScheduling 添加到您的 @SpringBootApplication
@SpringBootApplication
@EnableScheduling
public class MainApp {
public static void main(String[] args) {
SpringApplication.run(MainApp.class, args);
}
}
現在您可以在任何需要的地方自動連接 AppProperties Bean。 只需確保始終調用其中的方法,而不是將其值保存在變量中。 並確保重新配置任何使用可能不同的屬性值初始化的資源或 bean。
目前,我僅使用外部和默認找到的./config/application.properties
文件對此進行了測試。
對於 Java EE
我制作了一個通用的 Java SE 抽象類來完成這項工作。
您可以復制並粘貼此內容:
// imports from java.* and javax.crypto.*
public abstract class ReloadableProperties {
private volatile Properties properties = null;
private volatile String propertiesPassword = null;
private volatile long lastModTimeOfFile = 0L;
private volatile long lastTimeChecked = 0L;
private volatile Path propertyFileAddress;
abstract protected void propertiesUpdated();
public class DynProp {
private final String propertyName;
public DynProp(String propertyName) {
this.propertyName = propertyName;
}
public String val() {
try {
return ReloadableProperties.this.getString(propertyName);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
protected void init(Path path) {
this.propertyFileAddress = path;
initOrReloadIfNeeded();
}
private synchronized void initOrReloadIfNeeded() {
boolean firstTime = lastModTimeOfFile == 0L;
long currentTs = System.currentTimeMillis();
if ((lastTimeChecked + 3000) > currentTs)
return;
try {
File fa = propertyFileAddress.toFile();
long currModTime = fa.lastModified();
if (currModTime > lastModTimeOfFile) {
lastModTimeOfFile = currModTime;
InputStreamReader isr = new InputStreamReader(new FileInputStream(fa), StandardCharsets.UTF_8);
Properties prop = new Properties();
prop.load(isr);
properties = prop;
isr.close();
File passwordFiles = new File(fa.getAbsolutePath() + ".key");
if (passwordFiles.exists()) {
byte[] bytes = Files.readAllBytes(passwordFiles.toPath());
propertiesPassword = new String(bytes,StandardCharsets.US_ASCII);
propertiesPassword = propertiesPassword.trim();
propertiesPassword = propertiesPassword.replaceAll("(\\r|\\n)", "");
}
}
updateProperties();
if (!firstTime)
propertiesUpdated();
} catch (Exception e) {
e.printStackTrace();
}
}
private void updateProperties() {
List<DynProp> dynProps = Arrays.asList(this.getClass().getDeclaredFields())
.stream()
.filter(f -> f.getType().isAssignableFrom(DynProp.class))
.map(f-> fromField(f))
.collect(Collectors.toList());
for (DynProp dp :dynProps) {
if (!properties.containsKey(dp.propertyName)) {
System.out.println("propertyName: "+ dp.propertyName + " does not exist in property file");
}
}
for (Object key : properties.keySet()) {
if (!dynProps.stream().anyMatch(dp->dp.propertyName.equals(key.toString()))) {
System.out.println("property in file is not used in application: "+ key);
}
}
}
private DynProp fromField(Field f) {
try {
return (DynProp) f.get(this);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
protected String getString(String param) throws Exception {
initOrReloadIfNeeded();
String value = properties.getProperty(param);
if (value.startsWith("ENC(")) {
String cipheredText = value
.replace("ENC(", "")
.replaceAll("\\)$", "");
value = decrypt(cipheredText, propertiesPassword);
}
return value;
}
public static String encrypt(String plainText, String key)
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidKeySpecException {
SecureRandom secureRandom = new SecureRandom();
byte[] keyBytes = key.getBytes(StandardCharsets.US_ASCII);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(key.toCharArray(), new byte[]{0,1,2,3,4,5,6,7}, 65536, 128);
SecretKey tmp = factory.generateSecret(spec);
SecretKey secretKey = new SecretKeySpec(tmp.getEncoded(), "AES");
byte[] iv = new byte[12];
secureRandom.nextBytes(iv);
final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv); //128 bit auth tag length
cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
byte[] cipherText = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
ByteBuffer byteBuffer = ByteBuffer.allocate(4 + iv.length + cipherText.length);
byteBuffer.putInt(iv.length);
byteBuffer.put(iv);
byteBuffer.put(cipherText);
byte[] cipherMessage = byteBuffer.array();
String cyphertext = Base64.getEncoder().encodeToString(cipherMessage);
return cyphertext;
}
public static String decrypt(String cypherText, String key)
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidKeySpecException {
byte[] cipherMessage = Base64.getDecoder().decode(cypherText);
ByteBuffer byteBuffer = ByteBuffer.wrap(cipherMessage);
int ivLength = byteBuffer.getInt();
if(ivLength < 12 || ivLength >= 16) { // check input parameter
throw new IllegalArgumentException("invalid iv length");
}
byte[] iv = new byte[ivLength];
byteBuffer.get(iv);
byte[] cipherText = new byte[byteBuffer.remaining()];
byteBuffer.get(cipherText);
byte[] keyBytes = key.getBytes(StandardCharsets.US_ASCII);
final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(key.toCharArray(), new byte[]{0,1,2,3,4,5,6,7}, 65536, 128);
SecretKey tmp = factory.generateSecret(spec);
SecretKey secretKey = new SecretKeySpec(tmp.getEncoded(), "AES");
cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, iv));
byte[] plainText= cipher.doFinal(cipherText);
String plain = new String(plainText, StandardCharsets.UTF_8);
return plain;
}
}
然后你可以這樣使用它:
public class AppProperties extends ReloadableProperties {
public static final AppProperties INSTANCE; static {
INSTANCE = new AppProperties();
INSTANCE.init(Paths.get("application.properties"));
}
@Override
protected void propertiesUpdated() {
// run code every time a property is updated
}
public final DynProp wsUrl = new DynProp("ws.url");
public final DynProp hiddenText = new DynProp("hidden.text");
}
如果您想使用編碼屬性,您可以將它的值包含在 ENC() 中,並且將在屬性文件的相同路徑和名稱中搜索解密密碼,並添加 .key 擴展名。 在本例中,它將在 application.properties.key 文件中查找密碼。
application.properties ->
ws.url=http://some webside
hidden.text=ENC(AAAADCzaasd9g61MI4l5sbCXrFNaQfQrgkxygNmFa3UuB9Y+YzRuBGYj+A==)
aplication.properties.key ->
password aca
對於 Java EE 解決方案的屬性值加密,我咨詢了 Patrick Favre-Bulle 關於Java 和 Android 中使用 AES 對稱加密的優秀文章。 然后在這個關於AES/GCM/NoPadding 的問題中檢查了密碼、塊模式和填充。 最后,我使 AES 位從@erickson 在 SO about AES Password Based Encryption 中的優秀答案中導出。 關於 Spring 中值屬性的加密,我認為它們與Java Simplified Encryption集成
這是否符合最佳實踐可能超出了范圍。 此答案顯示了如何在 Spring Boot 和 Java EE 中擁有可重新加載的屬性。
此功能可以通過使用Spring Cloud Config Server和刷新范圍客戶端來實現。
服務器
服務器(Spring Boot 應用程序)提供存儲在例如 Git 存儲庫中的配置:
@SpringBootApplication
@EnableConfigServer
public class ConfigServer {
public static void main(String[] args) {
SpringApplication.run(ConfigServer.class, args);
}
}
應用程序.yml:
spring:
cloud:
config:
server:
git:
uri: git-repository-url-which-stores-configuration.git
配置文件configuration-client.properties
(在 Git 存儲庫中):
configuration.value=Old
客戶
客戶端(Spring Boot 應用程序)使用@RefreshScope注解從配置服務器讀取配置:
@Component
@RefreshScope
public class Foo {
@Value("${configuration.value}")
private String value;
....
}
引導程序.yml:
spring:
application:
name: configuration-client
cloud:
config:
uri: configuration-server-url
當 Git 存儲庫中的配置發生更改時:
configuration.value=New
通過向/refresh
端點發送POST
請求來重新加載配置變量:
$ curl -X POST http://client-url/actuator/refresh
現在您有了新值New
。
此外,如果Foo
類更改為RestController
並具有相應的端點,則可以通過RESTful API
將該值提供給應用程序的其余部分。
我使用了@David Hofmann 的概念並進行了一些更改,因為並非一切都很好。 首先,在我的情況下,我不需要自動重新加載,我只需調用 REST 控制器來更新屬性。 第二種情況@David Hofmann 的方法對我來說不適用於外部文件。
現在,此代碼可以與來自資源(應用程序內部)和外部位置的application.properties文件一起使用。 我放在 jar 附近的外部文件,我在應用程序啟動時使用這個--spring.config.location=app.properties參數。
@Component
public class PropertyReloader {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private StandardEnvironment environment;
private long lastModTime = 0L;
private PropertySource<?> appConfigPropertySource = null;
private Path configPath;
private static final String PROPERTY_NAME = "app.properties";
@PostConstruct
private void createContext() {
MutablePropertySources propertySources = environment.getPropertySources();
// first of all we check if application started with external file
String property = "applicationConfig: [file:" + PROPERTY_NAME + "]";
PropertySource<?> appConfigPsOp = propertySources.get(property);
configPath = Paths.get(PROPERTY_NAME).toAbsolutePath();
if (appConfigPsOp == null) {
// if not we check properties file from resources folder
property = "class path resource [" + PROPERTY_NAME + "]";
configPath = Paths.get("src/main/resources/" + PROPERTY_NAME).toAbsolutePath();
}
appConfigPsOp = propertySources.get(property);
appConfigPropertySource = appConfigPsOp;
}
// this method I call into REST cintroller for reloading all properties after change
// app.properties file
public void reload() {
try {
long currentModTs = Files.getLastModifiedTime(configPath).toMillis();
if (currentModTs > lastModTime) {
lastModTime = currentModTs;
Properties properties = new Properties();
@Cleanup InputStream inputStream = Files.newInputStream(configPath);
properties.load(inputStream);
String property = appConfigPropertySource.getName();
PropertiesPropertySource updatedProperty = new PropertiesPropertySource(property, properties);
environment.getPropertySources().replace(property, updatedProperty);
logger.info("Configs {} were reloaded", property);
}
} catch (Exception e) {
logger.error("Can't reload config file " + e);
}
}
}
我希望我的方法能幫助某人
對於 spring 引導,這里有一篇關於此主題的非常好的文章,但對於多個屬性文件,它不能完美地工作。 在我的例子中,我有 2 個屬性文件,一個是非敏感文件,另一個包含密碼。 我進行了以下操作:
<dependency>
<groupId>commons-configuration</groupId>
<artifactId>commons-configuration</artifactId>
<version>1.10</version>
</dependency>
擴展 spring 的 PropertySource,以便您可以將可重新加載的版本添加到環境中。
public class ReloadablePropertySource extends PropertySource {
private final PropertiesConfiguration propertiesConfiguration;
public ReloadablePropertySource(String name, String path, ConfigurationListener listener) {
super(StringUtils.hasText(name) ? name : path);
try {
this.propertiesConfiguration = getConfiguration(path, listener);
} catch (Exception e) {
throw new MissingRequiredPropertiesException();
}
}
@Override
public Object getProperty(String s) {
return propertiesConfiguration.getProperty(s);
}
private PropertiesConfiguration getConfiguration(String path, ConfigurationListener listener) throws ConfigurationException {
PropertiesConfiguration configuration = new PropertiesConfiguration(path);
FileChangedReloadingStrategy reloadingStrategy = new FileChangedReloadingStrategy();
reloadingStrategy.setRefreshDelay(5000);
configuration.setReloadingStrategy(reloadingStrategy);
configuration.addConfigurationListener(listener);
return configuration;
}
}
現在將所有屬性文件(現在可重新加載)添加到 spring 的環境中
@Configuration
public class ReloadablePropertySourceConfig {
private final ConfigurableEnvironment env;
@Value("${spring.config.location}")
private String appConfigPath;
@Value("${spring.config.additional-location}")
private String vaultConfigPath;
public ReloadablePropertySourceConfig(ConfigurableEnvironment env) {
this.env = env;
}
@Bean
@ConditionalOnProperty(name = "spring.config.location")
public ReloadablePropertySource getAppConfigReloadablePropertySource(){
ReloadablePropertySource rps = new ReloadablePropertySource("dynamicNonSensitive", appConfigPath, new PropertiesChangeListener());
MutablePropertySources sources = env.getPropertySources();
sources.addFirst(rps);
return rps;
}
@Bean
@ConditionalOnProperty(name = "spring.config.additional-location")
public ReloadablePropertySource getVaultReloadablePropertySource(){
ReloadablePropertySource rps = new ReloadablePropertySource("dynamicVault", vaultConfigPath, new PropertiesChangeListener());
MutablePropertySources sources = env.getPropertySources();
sources.addFirst(rps);
return rps;
}
private static class PropertiesChangeListener implements ConfigurationListener{
@Override
public void configurationChanged(ConfigurationEvent event) {
if (!event.isBeforeUpdate()){
System.out.println("config refreshed!");
}
}
}
}
從文章
我們將新屬性 source 添加為第一項,因為我們希望它用相同的鍵覆蓋任何現有屬性
在我們的例子中,我們有 2 個“可重新加載”的屬性源,它們都將首先被查找。
最后再創建一個 class,我們可以從中訪問環境的屬性
@Component
public class ConfigProperties {
private final Environment environment;
public ConfigProperties(Environment environment) {
this.environment = environment;
}
public String getProperty(String name){
return environment.getProperty(name);
}
}
現在您可以自動裝配ConfigProperties
並始終獲取文件中的最新屬性,而無需重新啟動應用程序。
@RestController
@Slf4j
public class TestController {
@Autowired
private ConfigProperties env;
@GetMapping("/refresh")
public String test2() {
log.info("hit");
String updatedProperty = env.getProperty("test.property");
String password = env.getProperty("db.password");
return updatedProperty + "\n" + password;
}
}
其中test.property
來自第一個文件,而db.password
來自另一個文件。
如果您想實時更改屬性並且不想重新啟動服務器,請按照以下步驟操作:
1)。 應用程序屬性
app.name= xyz
management.endpoints.web.exposure.include=*
2)。 在 pom.xml 中添加以下依賴項
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
3).將 application.properties 放在/target/config
文件夾中。 在/target
文件夾中創建 jar
4).在ApplcationProperties.java下面添加一個類
@Component
@RefreshScope
@ConfigurationProperties(prefix = "app")
public class ApplicationProperties {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
5)。 編寫 Controller.java 並注入 ApplcationProperties
@RestController
public class TestController {
@Autowired
private ApplicationProperties applcationProperties;
@GetMapping("/test")
public String getString() {
return applcationProperties.getName();
}
}
6).運行spring boot應用
從瀏覽器調用localhost:XXXX/test
Output : xyz
7)。 將 application.properties 中的值從 xyz 更改為 abc
8). 使用 postman 向 localhost:XXXX/actuator/refresh 發送 POST 請求
response: ["app.name"]
9). 從瀏覽器調用 localhost:XXXX/find
Output : abc
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.