Java Spring Сопоставьте конкретные Bean

Я хочу заново создать (новый объект) конкретный bean в Runtime (без перезапуска сервера) при некоторых изменениях БД. Вот как это выглядит -

@Component
public class TestClass {

    @Autowired 
    private MyShop myShop; //to be refreshed at runtime bean

    @PostConstruct //DB listeners
    public void initializeListener() throws Exception {
        //...
        // code to get listeners config
        //...

        myShop.setListenersConfig(listenersConfig);
        myShop.initialize();
    }

    public void restartListeners() {
        myShop.shutdownListeners();
        initializeListener();
    }
}

Этот код не работает как myShop объект создается Spring как Singleton, и его контекст не обновляется, если сервер не перезапускается. Как обновить (создать новый объект) myShop?

Одним из плохих способов я могу создать новый myShop объект внутри restartListeners(), но это не кажется мне правильным.

Ответ 1

В DefaultListableBeanFactory у вас есть общедоступный метод destroySingleton ( "beanName" ), чтобы вы могли играть с ним, но вы должны знать, что если ваш autwired ваш bean, он сохранит тот же экземпляр объекта, который был автоопределен в на первом месте вы можете попробовать что-то вроде этого:

@RestController
public class MyRestController  {

        @Autowired
        SampleBean sampleBean;

        @Autowired
        ApplicationContext context;
        @Autowired
        DefaultListableBeanFactory beanFactory;

        @RequestMapping(value = "/ ")
        @ResponseBody
        public String showBean() throws Exception {

            SampleBean contextBean = (SampleBean) context.getBean("sampleBean");

            beanFactory.destroySingleton("sampleBean");

            return "Compare beans    " + sampleBean + "==" 

    + contextBean;

    //while sampleBean stays the same contextBean gets recreated in the context
            }

    }

Это не очень, но показывает, как вы можете приблизиться к нему. Если вы имели дело с контроллером, а не с классом компонентов, у вас может быть инъекция в аргументе метода, и он также будет работать, потому что bean не будет воссоздаваться до тех пор, пока он не понадобится внутри метода, по крайней мере, так, как он выглядит. Интересным вопросом будет тот, у кого еще есть ссылка на старый bean помимо объекта, в который он был автоматически включен, поскольку он был удален из контекста, интересно, существует ли он или существует ли мусор, если он выпущен в контроллер выше, если некоторые другие объекты в контексте ссылались на него, это может вызвать проблемы.

Ответ 2

У нас один и тот же вариант использования. Как уже упоминалось, одна из основных проблем с воссозданием bean во время выполнения - это то, как обновлять ссылки, которые уже были введены. Это представляет собой основную проблему.

Чтобы обойти эту проблему, я использовал Javas AtomicReference < > class. Вместо того, чтобы напрямую вводить bean, Ive завернул его как AtomicReference, а затем ввел его. Поскольку объект, обернутый AtomicReference, может быть reset безопасным потоком, я могу использовать его для изменения базового объекта при обнаружении изменения базы данных. Ниже приведен пример конфигурации/использования этого шаблона:

@Configuration
public class KafkaConfiguration {

    private static final String KAFKA_SERVER_LIST = "kafka.server.list";
    private static AtomicReference<String> 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<String, String> kafkaProducerFactory() {

        Map<String, Object> 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<String, String>> kafkaTemplate() {

        KafkaTemplate<String, String> template = new KafkaTemplate<>(kafkaProducerFactory());
        AtomicReference<KafkaTemplate<String, String>> ref = new AtomicReference<>(template);
        return ref;
    }
}

Затем я вставляю bean, если необходимо, например

public MyClass1 {

    @Resource 
    AtomicReference<KafkaTemplate<String, String>> kafkaTemplate;
    ...
}

public MyClass2 {

    @Resource 
    AtomicReference<KafkaTemplate<String, String>> kafkaTemplate;
    ...
}

В отдельном классе я запускаю поток планировщика, который запускается при запуске контекста приложения. Класс выглядит примерно так:

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<String, String>> kafkaTemplate = 
                    (AtomicReference<KafkaTemplate<String, String>>) 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 {

        }
    }
}

Я все еще на заборе, если это то, что я буду защищать, поскольку у него есть легкий запах. Но при ограниченном и тщательном использовании он обеспечивает альтернативный подход к заявленному прецеденту. Имейте в виду, что с точки зрения Kafka этот пример кода оставит старого продюсера открытым. На самом деле нужно было бы правильно сделать вызов flush() на старого продюсера, чтобы закрыть его. Но это не то, что должен продемонстрировать пример.