1414import org .springframework .util .StringUtils ;
1515
1616import javax .annotation .PostConstruct ;
17+ import java .io .File ;
1718import java .io .IOException ;
1819import java .nio .file .*;
1920import java .util .Arrays ;
2021import java .util .HashMap ;
2122import java .util .List ;
2223import java .util .Map ;
2324import java .util .concurrent .Executors ;
25+ import java .util .concurrent .TimeUnit ;
26+ import java .util .stream .Stream ;
2427
2528/**
2629 * Enhance PropertySource when spring.config.location is specified, it will start directory-watch,
3336@ ConditionalOnProperty ("spring.config.location" )
3437public class DynamicConfigPropertiesWatcher implements DisposableBean {
3538
39+ private static final long SYMBOL_LINK_POLLING_INTERVAL = 5000 ;
40+ private static final long NORMAL_FILE_POLLING_INTERVAL = 90000 ;
3641 private static final String FILE_COLON_SYMBOL = "file:" ;
42+ private static final String SYMBOL_LINK_DIR = "..data" ;
3743 private static final String FILE_SOURCE_CONFIGURATION_PATTERN = "^.*Config\\ sresource.*file.*$" ;
3844 private static final String FILE_SOURCE_CONFIGURATION_PATTERN_LEGACY = "^.+Config:\\ s\\ [file:.*$" ;
3945 private static final Map <String , PropertySourceMeta > PROPERTY_SOURCE_META_MAP = new HashMap <>(8 );
@@ -43,6 +49,7 @@ public class DynamicConfigPropertiesWatcher implements DisposableBean {
4349 private final List <PropertySourceLoader > propertyLoaders ;
4450
4551 private WatchService watchService ;
52+ private long symbolicLinkModifiedTime = 0 ;
4653
4754 @ Value ("${spring.config.location:}" )
4855 private String configLocation ;
@@ -108,13 +115,15 @@ private void startWatchDir() {
108115 log .info ("start watching configuration directory: {}" , configLocation );
109116 watchService = FileSystems .getDefault ().newWatchService ();
110117 Paths .get (configLocation ).register (watchService , StandardWatchEventKinds .ENTRY_MODIFY , StandardWatchEventKinds .ENTRY_CREATE );
118+ checkChangesWithPeriod ();
111119 WatchKey key ;
112120 while ((key = watchService .take ()) != null ) {
113121 // avoid receiving two ENTRY_MODIFY events: file modified and timestamp updated
114122 Thread .sleep (50 );
115123 for (WatchEvent <?> event : key .pollEvents ()) {
116124 Path path = (Path ) event .context ();
117- reloadChangedFile (path );
125+ reloadChangedFile (path , false );
126+
118127 }
119128 key .reset ();
120129 }
@@ -126,7 +135,55 @@ private void startWatchDir() {
126135 }
127136 }
128137
129- private void reloadChangedFile (Path path ) {
138+ @ SuppressWarnings ("AlibabaThreadPoolCreation" )
139+ private void checkChangesWithPeriod () throws IOException {
140+ Path symLinkPath = Paths .get (configLocation , SYMBOL_LINK_DIR );
141+ boolean hasDotDataLinkFile = new File (configLocation , SYMBOL_LINK_DIR ).exists ();
142+ if (hasDotDataLinkFile ) {
143+ log .info ("ConfigMap/Secret mode detected, will polling symbolic link instead." );
144+ symbolicLinkModifiedTime = Files .getLastModifiedTime (symLinkPath , LinkOption .NOFOLLOW_LINKS ).toMillis ();
145+ Executors .newSingleThreadScheduledExecutor (r -> new Thread (r , "config-watcher-polling" )).scheduleWithFixedDelay (
146+ this ::checkSymbolicLink , SYMBOL_LINK_POLLING_INTERVAL , SYMBOL_LINK_POLLING_INTERVAL , TimeUnit .MILLISECONDS );
147+ } else {
148+ // longer check for all config files, make up mechanism if WatchService doesn't work
149+ Executors .newSingleThreadScheduledExecutor (r -> new Thread (r , "config-watcher-polling" )).scheduleWithFixedDelay (
150+ this ::reloadAllConfigFiles , NORMAL_FILE_POLLING_INTERVAL , NORMAL_FILE_POLLING_INTERVAL , TimeUnit .MILLISECONDS );
151+ }
152+ }
153+
154+ private void checkSymbolicLink () {
155+ try {
156+ Path symLinkPath = Paths .get (configLocation , SYMBOL_LINK_DIR );
157+ long tmp = Files .getLastModifiedTime (symLinkPath , LinkOption .NOFOLLOW_LINKS ).toMillis ();
158+ if (tmp != symbolicLinkModifiedTime ) {
159+ reloadAllConfigFiles (true );
160+ symbolicLinkModifiedTime = tmp ;
161+ }
162+ } catch (IOException ex ) {
163+ log .warn ("could not check symbolic link of config dir: {}" , ex .getMessage ());
164+ }
165+ }
166+
167+ private void reloadAllConfigFiles () {
168+ reloadAllConfigFiles (false );
169+ }
170+
171+ private void reloadAllConfigFiles (boolean forceReload ) {
172+ try (Stream <Path > paths = Files .walk (Paths .get (configLocation ))) {
173+ paths .forEach ((path ) -> {
174+ if (!Files .isDirectory (path )) {
175+ reloadChangedFile (path , forceReload );
176+ }
177+ });
178+ } catch (IOException e ) {
179+ log .warn ("can not walk through config directory: {}" , e .getMessage ());
180+ }
181+ }
182+
183+ private void reloadChangedFile (Path path , boolean forceReload ) {
184+ if (SYMBOL_LINK_DIR .equals (path .toString ())) {
185+ return ;
186+ }
130187 Path absPath = null ;
131188 try {
132189 absPath = Paths .get (configLocation , path .toString ());
@@ -143,7 +200,7 @@ private void reloadChangedFile(Path path) {
143200 }
144201 long currentModTs = Files .getLastModifiedTime (absPath ).toMillis ();
145202 long mdt = propertySourceMeta .getLastModifyTime ();
146- if (mdt != currentModTs ) {
203+ if (forceReload || mdt != currentModTs ) {
147204 doReloadConfigFile (propertySourceMeta , absPathStr , currentModTs );
148205 }
149206 } catch (Exception ex ) {
0 commit comments