I have this configuration:
@Configuration
@EnableIntegration
public class SftpConfiguration {
@Autowired
private InterfaceRepository interfaceRepo;
public record SessionFactoryKey(String host, int port, String user) {
}
@Bean
SessionFactoryLocator<LsEntry> sessionFactoryLocator() {
Map<Object, SessionFactory<LsEntry>> factories = interfaceRepo.findAll().stream()
.map(x -> new SimpleEntry<>(new SessionFactoryKey(x.getHostname(), x.getPort(), x.getUsername()),
sessionFactory(x.getHostname(), x.getPort(), x.getUsername(), x.getPassword())))
.collect(Collectors.toMap(Entry::getKey, Entry::getValue, (a, b) -> a));
return new DefaultSessionFactoryLocator<>(factories);
}
@Bean
RemoteFileTemplate<LsEntry> fileTemplateResolver(DelegatingSessionFactory<LsEntry> delegatingSessionFactory) {
return new SftpRemoteFileTemplate(delegatingSessionFactory);
}
@Bean
DelegatingSessionFactory<LsEntry> delegatingSessionFactory(SessionFactoryLocator<LsEntry> sessionFactoryLocator) {
return new DelegatingSessionFactory<>(sessionFactoryLocator);
}
@Bean
RotatingServerAdvice advice(DelegatingSessionFactory<LsEntry> delegatingSessionFactory) {
List<RotationPolicy.KeyDirectory> keyDirectories = interfaceRepo.findAll().stream()
.filter(Interface::isReceivingData)
.map(x -> new RotationPolicy.KeyDirectory(
new SessionFactoryKey(x.getHostname(), x.getPort(), x.getUsername()),
x.getDirectory()))
.toList();
return keyDirectories.isEmpty() ? null : new RotatingServerAdvice(delegatingSessionFactory, keyDirectories);
}
@Bean
PropertiesPersistingMetadataStore store() {
return new PropertiesPersistingMetadataStore();
}
@Bean
public IntegrationFlow flow(ObjectProvider<RotatingServerAdvice> adviceProvider,
DelegatingSessionFactory<LsEntry> delegatingSessionFactory, PropertiesPersistingMetadataStore store) {
RotatingServerAdvice advice = adviceProvider.getIfAvailable();
return advice == null ? null
: IntegrationFlows
.from(Sftp.inboundAdapter(delegatingSessionFactory)
.filter(new SftpPersistentAcceptOnceFileListFilter(store, "rotate_"))
.localDirectory(new File("C:\\tmp\\sftp"))
.localFilenameExpression("#remoteDirectory + T(java.io.File).separator + #root")
.remoteDirectory("."), e -> e.poller(Pollers.fixedDelay(1).advice(advice)))
.channel(MessageChannels.queue("files")).get();
}
private SessionFactory<LsEntry> sessionFactory(String host, int port, String user, String password) {
DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(true);
factory.setHost(host);
factory.setPort(port);
factory.setUser(user);
factory.setPassword(password);
factory.setAllowUnknownKeys(true);
return factory;
}
}
Basically it provides a RemoteFileTemplate
that allows to upload files via SFTP, and an IntegrationFlow
that polls a set of SFTP servers to retrieve files. The configuration is loaded via a database.
I want to reload the beans when the configuration has changed in the database but I can't figure out how.
I think the only chance I have to make it work is to use lazy proxies because the client code has already loaded bean instances which cannot be unloaded. That's why I tried @RefreshScope
from spring cloud, but it didn't work because IntegrationFlow
forbids other scopes than singleton
.
Is there any solution other than closing the application context and run SpringApplication.run
again?
According to your current configuration, only what you need to reload is a configuration for SFTP servers. So, you need to refresh only sessionFactoryLocator
and RotatingServerAdvice
beans.
According to the org.springframework.cloud.context.scope.refresh.RefreshScope
, we need to have standard lifecycle callback on a bean for a proper refreshment. The DefaultSessionFactoryLocator
doesn't have one to clean up its internal Map
, so we probably need to catch a RefreshScopeRefreshedEvent
and call DefaultSessionFactoryLocator.removeSessionFactory()
for a clear state. However if you would just use simple keys for factory entries, then standard @RefreshScope
would be enough: the DefaultSessionFactoryLocator
would be reinitialized and its internal map would be just overridden for the same key, but new values. Not sure why you decided to go some complex SessionFactoryKey
abstraction based on a connection info.
The RotatingServerAdvice
does not need any extra work: just @RefreshScope
should be enough. That RotatingServerAdvice(DelegatingSessionFactory<?> factory, List<RotationPolicy.KeyDirectory> keyDirectories)
constructor overrides all the instance internals.