Search code examples
javaspringspring-integration

Reconfigure Spring Integration beans at runtime


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?


Solution

  • 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.