Dav*_*ann 11 spring properties java-ee spring-boot
我想到了许多内部解决方案.就像在数据库中拥有属性并每隔N秒轮询它一样.然后还要检查.properties文件的时间戳修改并重新加载它.
但我正在研究Java EE标准和Spring引导文档,我似乎无法找到一些最好的方法.
我需要我的应用程序来读取属性文件(或环境变量或数据库参数),然后才能重新读取它们.生产中使用的最佳实践是什么?
一个正确的答案至少可以解决一个场景(Spring Boot或Java EE)并提供一个关于如何使其在另一个场景上工作的概念线索
经过进一步研究,必须仔细考虑重载特性。例如,在Spring中,我们可以重新加载属性的“当前”值没有太大问题。但。基于application.properties文件中存在的值(例如,数据源,连接池,队列等)在上下文初始化时初始化资源时,必须格外小心。
注意:
用于Spring和Java EE的抽象类不是干净代码的最佳示例。但是它易于使用,并且确实满足了以下基本初始要求:
对于春季靴
这段代码有助于在不使用Spring Cloud Config服务器的情况下热重载application.properties文件(在某些情况下可能过大)
您可以只复制并粘贴此抽象类(SO goodies:D),这是从该SO答案派生的代码
// 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();
}
Run Code Online (Sandbox Code Playgroud)
然后,创建一个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
}
}
Run Code Online (Sandbox Code Playgroud)
确保将@EnableScheduling添加到您的@SpringBootApplication中
@SpringBootApplication
@EnableScheduling
public class MainApp {
public static void main(String[] args) {
SpringApplication.run(MainApp.class, args);
}
}
Run Code Online (Sandbox Code Playgroud)
现在,您可以在需要的地方自动装配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;
}
}
Run Code Online (Sandbox Code Playgroud)
然后,您可以通过以下方式使用它:
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");
}
Run Code Online (Sandbox Code Playgroud)
如果要使用编码的属性,可以将其值括在ENC()中,然后将在属性文件的相同路径和名称中搜索解密密码,并添加.key扩展名。在此示例中,它将在application.properties.key文件中寻找密码。
application.properties->
ws.url=http://some webside
hidden.text=ENC(AAAADCzaasd9g61MI4l5sbCXrFNaQfQrgkxygNmFa3UuB9Y+YzRuBGYj+A==)
Run Code Online (Sandbox Code Playgroud)
aplication.properties.key->
password aca
Run Code Online (Sandbox Code Playgroud)
对于Java EE解决方案的属性值的加密,我查阅了Patrick Favre-Bulle的出色文章,内容涉及Java和Android中的AES对称加密。然后在关于AES / GCM / NoPadding的 SO问题中检查了密码,块模式和填充。最后,我使AES位源自@erickson的密码中的一个很好的答案,关于AES基于密码的加密。关于Spring中的值属性加密,我认为它们与Java简化加密集成在一起
是否可以将此视为最佳做法,否则可能不在范围之内。该答案显示了如何在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);
}
}
Run Code Online (Sandbox Code Playgroud)
application.yml:
spring:
cloud:
config:
server:
git:
uri: git-repository-url-which-stores-configuration.git
Run Code Online (Sandbox Code Playgroud)
配置文件configuration-client.properties(在Git存储库中):
configuration.value=Old
Run Code Online (Sandbox Code Playgroud)
客户
客户端(Spring Boot应用程序)使用@RefreshScope批注从配置服务器读取配置:
@SpringBootApplication
@EnableConfigServer
public class ConfigServer {
public static void main(String[] args) {
SpringApplication.run(ConfigServer.class, args);
}
}
Run Code Online (Sandbox Code Playgroud)
bootstrap.yml:
spring:
application:
name: configuration-client
cloud:
config:
uri: configuration-server-url
Run Code Online (Sandbox Code Playgroud)
当Git存储库中的配置发生更改时:
configuration.value=New
Run Code Online (Sandbox Code Playgroud)
通过向端点发送POST请求来重新加载配置变量/refresh:
$ curl -X POST http://client-url/actuator/refresh
Run Code Online (Sandbox Code Playgroud)
现在您有了新的价值New。
另外,如果Fooclass RESTful API更改为RestController并具有相应的endpont,则class可以将值提供给其余应用程序。
我使用了@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);
}
}
Run Code Online (Sandbox Code Playgroud)
}
我希望我的方法能帮助某人
| 归档时间: |
|
| 查看次数: |
12299 次 |
| 最近记录: |