복붙노트

[SPRING] Java EE 및 스프링 부트에서 등록 정보를 핫 로딩하는 방법은 무엇입니까?

SPRING

Java EE 및 스프링 부트에서 등록 정보를 핫 로딩하는 방법은 무엇입니까?

많은 사내 솔루션이 떠오릅니다. 데이터베이스에 속성을 가지고 매 N 초마다 폴링하는 것과 같습니다. 그런 다음 .properties 파일에 대한 시간 소인 수정을 점검하고 다시로드하십시오.

하지만 Java EE 표준 및 스프링 부트 문서를보고 있었고 최선의 방법을 찾지 못하는 것 같습니다.

내 응용 프로그램이 속성 파일 (또는 환경 변수 또는 DB 매개 변수)을 읽은 다음 다시 읽을 수 있어야합니다. 프로덕션 환경에서 사용되는 가장 좋은 방법은 무엇입니까?

정답은 적어도 하나의 시나리오 (Spring Boot 또는 Java EE)를 해결하고 다른 시나리오에서 작동하게하는 방법에 대한 개념적 단서를 제공합니다

해결법

  1. ==============================

    1.추가 연구가 끝나면 재 로딩 특성을 신중하게 고려해야합니다. 예를 들어 Spring에서는 많은 문제없이 속성의 '현재'값을 다시로드 할 수 있습니다. 그러나. application.properties 파일에있는 값 (예 : 데이터 소스, 연결 풀, 대기열 등)을 기반으로 컨텍스트 초기화 시간에 자원을 초기화하면 특별한주의를 기울여야합니다.

    추가 연구가 끝나면 재 로딩 특성을 신중하게 고려해야합니다. 예를 들어 Spring에서는 많은 문제없이 속성의 '현재'값을 다시로드 할 수 있습니다. 그러나. application.properties 파일에있는 값 (예 : 데이터 소스, 연결 풀, 대기열 등)을 기반으로 컨텍스트 초기화 시간에 자원을 초기화하면 특별한주의를 기울여야합니다.

    노트:

    Spring과 Java EE에 사용되는 추상 클래스는 깨끗한 코드의 좋은 예가 아닙니다. 그러나 사용하기 쉽고 기본적인 기본 요구 사항을 해결합니다.

    봄 부팅 용

    이 코드는 Spring Cloud Config 서버를 사용하지 않고 application.properties 파일을 핫 로딩하는 데 유용합니다 (일부 유스 케이스의 경우 잔인 할 수 있음)

    이 추상 클래스를 복사하여 붙여 넣기 만하면됩니다 (SO : 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();
    }
    

    그런 다음 abstract 클래스를 사용하는 applicatoin.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
        }
    }
    

    @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==)
    

    application.properties.key ->

    password aca
    

    Java EE 솔루션의 속성 값을 암호화하기 위해 Java 및 Android에서 AES를 사용한 대칭 암호화에 대한 Patrick Favre-Bulle 우수 기사를 참조했습니다. 그런 다음 Cipher, 블록 모드 및 패딩을 점검하여 AES / GCM / NoPadding에 관한이 질문에 답하십시오. 그리고 마지막으로 AES 비트를 AER 암호 기반 암호화에 대한 @erickson 우수 답변의 암호에서 파생 시켰습니다. Spring에서 값 속성의 암호화에 관해서는 Java Simple Encryption

    이 날씨는 모범 사례로 간주되거나 범위를 벗어날 수 있습니다. 이 답변은 Spring Boot와 Java EE에서 재로드 가능한 속성을 갖는 방법을 보여줍니다.

  2. ==============================

    2.이 기능은 Spring Cloud Config Server 및 새로 고침 범위 클라이언트를 사용하여 수행 할 수 있습니다.

    이 기능은 Spring Cloud Config Server 및 새로 고침 범위 클라이언트를 사용하여 수행 할 수 있습니다.

    섬기는 사람

    Server (Spring Boot app)는 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
    

    고객

    클라이언트 (스프링 부트 응용 프로그램)는 @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
    

    / refresh 엔드 포인트로 POST 요청을 보내 구성 변수를 다시로드하십시오.

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

    이제 새로운 가치 New가 있습니다.

    또한 Foo 클래스는 RestController로 변경되고 해당 endpont가 있으면 RESTful API를 통해 나머지 응용 프로그램에 값을 제공 할 수 있습니다.

  3. from https://stackoverflow.com/questions/52594764/how-to-hot-reload-properties-in-java-ee-and-spring-boot by cc-by-sa and MIT license