Java Spring Recreate specific Bean

后端 未结 2 1410
南笙
南笙 2020-12-31 04:42

I want to re-create (new Object) a specific bean at Runtime (no restarting the server) upon some DB changes. This is how it looks -

@Component
public class          


        
2条回答
  •  悲哀的现实
    2020-12-31 05:27

    We have the same use-case. As already mentioned one of the main issues with re-creating a bean during runtime is how to updating the references that have already been injected. This presents the main challenge.

    To work around this issue I’ve used Java’s AtomicReference<> class. Instead of injecting the bean directly, I’ve wrapped it as an AtomicReference and then inject that. Because the object wrapped by the AtomicReference can be reset in a thread safe manner, I am able to use this to change the underlying object when a database change is detected. Below is an example config / usage of this pattern:

    @Configuration
    public class KafkaConfiguration {
    
        private static final String KAFKA_SERVER_LIST = "kafka.server.list";
        private static AtomicReference serverList;
    
        @Resource
        MyService myService;
    
        @PostConstruct
        public void init() {
            serverList = new AtomicReference<>(myService.getPropertyValue(KAFKA_SERVER_LIST));
        }
    
        // Just a helper method to check if the value for the server list has changed
        // Not a big fan of the static usage but needed a way to compare the old / new values
        public static boolean isRefreshNeeded() {
    
            MyService service = Registry.getApplicationContext().getBean("myService", MyService.class);    
            String newServerList = service.getPropertyValue(KAFKA_SERVER_LIST);
    
            // Arguably serverList does not need to be Atomic for this usage as this is executed
            // on a single thread
            if (!StringUtils.equals(serverList.get(), newServerList)) {
                serverList.set(newServerList);
                return true;
            }
    
            return false;
        }
    
        public ProducerFactory kafkaProducerFactory() {
    
            Map configProps = new HashMap<>();
            configProps.put(ProducerConfig.CLIENT_ID_CONFIG, "...");
    
            // Here we are pulling the value for the serverList that has been set
            // see the init() and isRefreshNeeded() methods above
            configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, serverList.get());
    
            configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
            configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
            return new DefaultKafkaProducerFactory<>(configProps);
        }
    
        @Bean
        @Lazy
        public AtomicReference> kafkaTemplate() {
    
            KafkaTemplate template = new KafkaTemplate<>(kafkaProducerFactory());
            AtomicReference> ref = new AtomicReference<>(template);
            return ref;
        }
    }
    

    I then inject the bean where needed, e.g.

    public MyClass1 {
    
        @Resource 
        AtomicReference> kafkaTemplate;
        ...
    }
    
    public MyClass2 {
    
        @Resource 
        AtomicReference> kafkaTemplate;
        ...
    }
    

    In a separate class I run a scheduler thread that is started when the application context is started. The class looks something like this:

    class Manager implements Runnable {
    
        private ScheduledExecutorService scheduler;
    
        public void start() {
            scheduler = Executors.newSingleThreadScheduledExecutor();
            scheduler.scheduleAtFixedRate(this, 0, 120, TimeUnit.SECONDS);
        }
    
        public void stop() {
            scheduler.shutdownNow();
        }
    
        @Override
        public void run() {
    
            try {
                if (KafkaConfiguration.isRefreshNeeded()) {
    
                    AtomicReference> kafkaTemplate = 
                        (AtomicReference>) Registry.getApplicationContext().getBean("kafkaTemplate");
    
                    // Get new instance here.  This will have the new value for the server list
                    // that was "refreshed"
                    KafkaConfiguration config = new KafkaConfiguration();
    
                    // The set here replaces the wrapped objet in a thread safe manner with the new bean
                    // and thus all injected instances now use the newly created object
                    kafkaTemplate.set(config.kafkaTemplate().get());
                }
    
            } catch (Exception e){
    
            } finally {
    
            }
        }
    }
    

    I am still on the fence if this is something I would advocate doing as it does have a slight smell to it. But in limited and careful usage it does provide an alternate approach to the stated use-case. Please be aware that from a Kafka standpoint this code example will leave the old producer open. In reality one would need to properly do a flush() call on the old producer to close it. But that's not what the example is meant to demonstrate.

提交回复
热议问题