0

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?

Guerric P
  • 30,447
  • 6
  • 48
  • 86
  • here are some approaches how to reload beans in general, maybe this helps. https://stackoverflow.com/questions/51218086/how-to-reinitialize-a-spring-bean – René Winkler May 07 '23 at 20:41

1 Answers1

2

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.

Artem Bilan
  • 113,505
  • 11
  • 91
  • 118
  • Thanks for your complete and clear answer. There's just one thing I don't get: if I have multiple servers to poll, isn't the tuple "hostname", "port", and "user" a good key for the session factory? Are you saying that the only purpose of the locator is to have one session factory per thread? If so, how do I populate the keys before the threads exist? – Guerric P May 08 '23 at 18:33
  • Well, that is also OK, but technically we suggest to have just some logical key which could be accessed from the `RotatingServerAdvice`. But I see your point: you may not know how many servers you have in the config. So, yes, then your `SessionFactoryKey` is good. But you need to clean up your `SessionFactoryLocator` some way. Although since your `RotatingServerAdvice` will never use old keys, it might be OK to not worry about old entries... – Artem Bilan May 08 '23 at 18:56
  • Ok thanks. Last thing, you are saying that the `DefaultSessionFactoryLocator` can't clear its internal `Map` and that I have to call `DefaultSessionFactoryLocator.removeSessionFactory()` in a `RefreshScopeRefreshedEvent` listener. According to me annotating `sessionFactoryLocator` with `@RefreshScope` will call the bean factory to create a fresh one which will be provided by the proxy whose reference is held by the `DelegatingSessionFactory`. Am I misunderstanding? – Guerric P May 08 '23 at 21:42
  • Yes, you are right: I misread that `RefreshScope` Javadoc. Cannot confirm though, since I don't have any code to play with. – Artem Bilan May 08 '23 at 21:56