[SPRING] Java EE 및 스프링 부트에서 등록 정보를 핫 로딩하는 방법은 무엇입니까?
SPRINGJava EE 및 스프링 부트에서 등록 정보를 핫 로딩하는 방법은 무엇입니까?
많은 사내 솔루션이 떠오릅니다. 데이터베이스에 속성을 가지고 매 N 초마다 폴링하는 것과 같습니다. 그런 다음 .properties 파일에 대한 시간 소인 수정을 점검하고 다시로드하십시오.
하지만 Java EE 표준 및 스프링 부트 문서를보고 있었고 최선의 방법을 찾지 못하는 것 같습니다.
내 응용 프로그램이 속성 파일 (또는 환경 변수 또는 DB 매개 변수)을 읽은 다음 다시 읽을 수 있어야합니다. 프로덕션 환경에서 사용되는 가장 좋은 방법은 무엇입니까?
정답은 적어도 하나의 시나리오 (Spring Boot 또는 Java EE)를 해결하고 다른 시나리오에서 작동하게하는 방법에 대한 개념적 단서를 제공합니다
해결법
-
==============================
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.이 기능은 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를 통해 나머지 응용 프로그램에 값을 제공 할 수 있습니다.
from https://stackoverflow.com/questions/52594764/how-to-hot-reload-properties-in-java-ee-and-spring-boot by cc-by-sa and MIT license
'SPRING' 카테고리의 다른 글
[SPRING] HTTP 요청 압축 (0) | 2019.01.06 |
---|---|
[SPRING] 외부 파일의 속성을 hibernate.cfg.xml에 포함시키는 방법? (0) | 2019.01.06 |
[SPRING] 스프링 MVC 매핑 문제 (0) | 2019.01.06 |
[SPRING] 필터를 가능하게하기 위해 Spring JPA와 Hibernate를 사용하여 Session에 접근 (0) | 2019.01.06 |
[SPRING] ClassReader에서 ArrayIndexOutOfBoundsException으로 인해 ApplicationContext를로드하지 못했습니다. (0) | 2019.01.06 |