Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to hot-reload properties in Java EE and Spring Boot?

Many in-house solutions come to mind. Like having the properties in a database and poll it every N secs. Then also check the timestamp modification for a .properties file and reload it.

But I was looking in Java EE standards and spring boot docs and I can't seem to find some best way of doing it.

I need my application to read a properties file(or env. variables or DB parameters), then be able to re-read them. What is the best practice being used in production?

A correct answer will at least solve one scenario (Spring Boot or Java EE) and provide a conceptual clue on how to make it work on the other

like image 295
David Hofmann Avatar asked Oct 01 '18 15:10

David Hofmann


People also ask

How do I refresh properties in spring boot?

For Reloading properties, spring cloud has introduced @RefreshScope annotation which can be used for refreshing beans. Spring Actuator provides different endpoints for health, metrics. but spring cloud will add extra end point /refresh to reload all the properties.

How reload properties file without restarting server in spring boot?

include=refresh: this is actuator property which will help us to refresh the properties without restarting the server.

How reload properties file in java?

If you want to be updated you need to check the resource from tome to time and reload the properties. It is a problem that you are loading properties from classpath. If you use file system you can check the last modified attribute of file and decide whether to reload it.


2 Answers

After further research, reloading properties must be carefully considered. In Spring, for example, we can reload the 'current' values of properties without much problem. But. Special care must be taken when resources were initialized at the context initialization time based on the values that were present in the application.properties file (e.g. Datasources, connection pools, queues, etc.).

NOTE:

The abstract classes used for Spring and Java EE are not the best example of clean code. But it is easy to use and it does address this basic initial requirements:

  • No usage of external libraries other than Java 8 Classes.
  • Only one file to solve the problem (~160 lines for the Java EE version).
  • Usage of standard Java Properties UTF-8 encoded file available in the File System.
  • Support encrypted properties.

For Spring Boot

This code helps with hot-reloading application.properties file without the usage of a Spring Cloud Config server (which may be overkill for some use cases)

This abstract class you may just copy & paste (SO goodies :D ) It's a code derived from this 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(); } 

Then you make a bean class that allows retrieval of property values from applicatoin.properties that uses the abstract class

@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     } } 

Make sure to add @EnableScheduling to your @SpringBootApplication

@SpringBootApplication @EnableScheduling public class MainApp  {    public static void main(String[] args) {       SpringApplication.run(MainApp.class, args);    } } 

Now you can auto-wire the AppProperties Bean wherever you need it. Just make sure to always call the methods in it instead of saving it's value in a variable. And make sure to re-configure any resource or bean that was initialized with potentially different property values.

For now, I have only tested this with an external-and-default-found ./config/application.properties file.

For Java EE

I made a common Java SE abstract class to do the job.

You may copy & paste this:

// 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;   } } 

Then you can use it this way:

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");  } 

In case you want to use encoded properties you may enclose it's value inside ENC() and a password for decryption will be searched for in the same path and name of the property file with an added .key extension. In this example it will look for the password in the application.properties.key file.

application.properties ->

ws.url=http://some webside hidden.text=ENC(AAAADCzaasd9g61MI4l5sbCXrFNaQfQrgkxygNmFa3UuB9Y+YzRuBGYj+A==) 

aplication.properties.key ->

password aca 

For the encryption of property values for the Java EE solution I consulted Patrick Favre-Bulle excellent article on Symmetric Encryption with AES in Java and Android. Then checked the Cipher, block mode and padding in this SO question about AES/GCM/NoPadding. And finally I made the AES bits be derived from a password from @erickson excellent answer in SO about AES Password Based Encryption. Regarding encryption of value properties in Spring I think they are integrated with Java Simplified Encryption

Wether this qualify as a best practice or not may be out of scope. This answer shows how to have reloadable properties in Spring Boot and Java EE.

like image 187
David Hofmann Avatar answered Sep 22 '22 01:09

David Hofmann


This functionality can be achieved by using a Spring Cloud Config Server and a refresh scope client.

Server

Server (Spring Boot app) serves the configuration stored, for example, in a Git repository:

@SpringBootApplication @EnableConfigServer public class ConfigServer {   public static void main(String[] args) {     SpringApplication.run(ConfigServer.class, args);   } } 

application.yml:

spring:   cloud:     config:       server:         git:           uri: git-repository-url-which-stores-configuration.git 

configuration file configuration-client.properties (in a Git repository):

configuration.value=Old 

Client

Client (Spring Boot app) reads configuration from the configuration server by using @RefreshScope annotation:

@Component @RefreshScope public class Foo {      @Value("${configuration.value}")     private String value;      .... } 

bootstrap.yml:

spring:   application:     name: configuration-client   cloud:     config:       uri: configuration-server-url 

When there is a configuration change in the Git repository:

configuration.value=New 

reload the configuration variable by sending a POST request to the /refresh endpoint:

$ curl -X POST http://client-url/actuator/refresh 

Now you have the new value New.

Additionally Foo class can serve the value to the rest of application via RESTful API if its changed to RestController and has a corresponding endpont.

like image 35
Boris Avatar answered Sep 20 '22 01:09

Boris