programing

Java EE 및 Spring Boot에서 속성을 핫 새로고침하는 방법은 무엇입니까?

codeshow 2023. 3. 13. 20:41
반응형

Java EE 및 Spring Boot에서 속성을 핫 새로고침하는 방법은 무엇입니까?

많은 사내 솔루션이 떠오릅니다.예를 들어 데이터베이스에 속성을 저장하고 N초마다 폴링합니다.그런 다음 .properties 파일의 타임스탬프 수정도 체크하고 새로고침합니다.

그러나 Java EE 표준과 스프링 부트 문서를 찾아봤는데 최선의 방법을 찾을 수 없는 것 같습니다.

응용 프로그램에서 속성 파일(또는 변수 또는 DB 매개 변수)을 읽고 다시 읽을 수 있어야 합니다.생산에 사용되는 베스트 프랙티스는 무엇입니까?

정답을 맞히면 적어도 하나의 시나리오(Spring Boot 또는 Java EE)가 해결되고 다른 시나리오에서 작동하는 방법에 대한 개념적인 단서가 제공됩니다.

추가 조사 후 특성 재로드에 대해 신중하게 검토해야 합니다.예를 들어 봄에는 큰 문제 없이 속성의 '현재' 값을 다시 로드할 수 있습니다.단, 컨텍스트 초기화 시 application.properties 파일에 있는 값(데이터 소스, 연결 풀, 큐 등)에 따라 리소스를 초기화할 때는 특히 주의해야 합니다.

주의:

Spring 및 Java EE에 사용되는 추상 클래스는 클린 코드의 좋은 예가 아닙니다.단, 사용하기 쉽고 다음과 같은 기본적인 초기 요건을 충족합니다.

  • Java 8 Class 이외에는 외부 라이브러리를 사용하지 않습니다.
  • 문제를 해결할 수 있는 파일은 1개뿐입니다(Java EE 버전의 경우 최대 160줄).
  • 파일 시스템에서 사용할 수 있는 표준 Java Properties UTF-8 인코딩 파일 사용.
  • 암호화된 속성을 지원합니다.

스프링 부트용

이 코드는 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();
}

그런 다음 추상 클래스를 사용하는 applicationatoin.properties에서 속성 값을 검색할 수 있는 bean 클래스를 만듭니다.

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

@SpringBootApplication에 @EnableScheduling을 추가해야 합니다.

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

이제 AppProperties Bean을 필요한 장소에서 자동 연결할 수 있습니다.변수에 값을 저장하는 대신 항상 메서드를 호출해야 합니다.또한 잠재적으로 다른 속성 값으로 초기화된 리소스 또는 빈을 다시 구성해야 합니다.

../config/application.propertiesfilename을 클릭합니다.

Java EE의 경우

저는 그 일을 하기 위해 공통 자바 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에서 비밀번호를 검색합니다.키 파일

application.properties ->

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

aplication.properties.키 ->

password aca

Java EE 솔루션의 속성값 암호화에 대해서는 Patrick Favre-Bulle에게 Java와 Android의 Symmetric Encryption with AES에 관한 훌륭한 기사를 참조했습니다.다음으로 AES/GCM/NoPadding에 관한 SO 질문에서 암호, 블록 모드 및 패딩을 체크합니다.마지막으로 AES 비트를 SO about AES Password Based Encryption의 @erickson excellent answer에서 얻은 패스워드에서 추출했습니다.봄의 가치 속성 암호화는 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);
  }
}

application.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;

    ....
}

bootstrap.yml:

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

Git 저장소에 구성 변경이 있는 경우:

configuration.value=New

하여 "Configuration 변수"를 전송합니다.POST /refresh★★★★★★★★★★★★★★★★★★:

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

새로운 되었습니다.New.

, ★★★★★Foo할 수 .RESTful API로 되었을 경우RestController대응하는 엔드 폰트가 있습니다.

저는 @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);
    }
}

}

내 접근 방식이 누군가와 함께 하는 데 도움이 되길 바란다.

@Boris에서 언급했듯이 패치형 솔루션을 피하기 위해서는 Spring Cloud Config를 사용하는 것이 좋습니다.설정을 최소한으로 유지하기 위해 네이티브 타입(파일 타입)을 사용한 Config Server 접근 방식을 제안합니다.

액튜에이터 엔드포인트를 수동으로 호출하지 않고 자동 설정 리프레시를 지원하기 위해 디렉토리 리스너를 생성하여 파일 변경을 검출하고 리프레시 스코프 이벤트를 디스패치했습니다.

개념 실증 보고서(GIT)

스프링 부트에는 이 주제에 대한 매우 유용한 기사가 있지만 여러 속성 파일의 경우 완벽하게 작동하지 않습니다.제 경우, 2개의 속성 파일이 있었습니다.하나는 중요하지 않은 파일이고 다른 하나는 패스워드가 포함되어 있습니다.저는 다음과 같이 진행했습니다.

<dependency>
    <groupId>commons-configuration</groupId>
    <artifactId>commons-configuration</artifactId>
    <version>1.10</version>
</dependency>

새로고침 가능한 버전을 환경에 추가할 수 있도록 스프링의 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;
    }
}

이제 모든 속성 파일(이제 새로고침 가능)을 스프링 환경 내에 추가합니다.

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

기사에서

동일한 키를 가진 기존 속성을 재정의하도록 새 속성 소스를 첫 번째 항목으로 추가했습니다.

우리의 경우, 2개의 "재로드 가능한" 속성 소스가 있으며, 둘 다 먼저 조회됩니다.

마지막으로 환경 속성에 액세스할 수 있는 클래스를 하나 더 만듭니다.

@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) Application.properties

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폴더

4) Aplation Properties 아래에 클래스를 추가합니다.자바

@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를 작성하여 Aplation Properties를 주입합니다.

@RestController
public class TestController {

@Autowired
private ApplicationProperties applcationProperties;

@GetMapping("/test")
public String getString() {
    return applcationProperties.getName();
}
}

6)스프링 부트 어플리케이션을 실행합니다.

불러localhost:XXXX/test브라우저에서

Output : xyz

7) application.properties의 값을 xyz에서abc로 변경합니다.

8) 우체부를 사용하여 POST 요청을 localhost:XXXX/actuator/refresh로 전송합니다.

response: ["app.name"]

9) localhost: XXXX/브라우저에서 검색하기

Output : abc

언급URL : https://stackoverflow.com/questions/52594764/how-to-hot-reload-properties-in-java-ee-and-spring-boot

반응형