Spring Boot 实现UI触发INI文件更新时动态切换@Primary数据源
Got it, let's tackle this dynamic datasource switch problem with a straightforward approach that fits your needs—no overcomplicated routing datasource or external actuator endpoints. Here's how you can make it work with minimal changes to your existing code:
Core Idea
Since Spring manages beans as singletons by default, we need to manually destroy the old datasource-related beans, create new ones with the updated INI config, and register them back into the application context as the new @Primary beans. We'll trigger this process directly from your UI button click event.
Step 1: Refactor Config Class for Reusable Bean Creation
First, let's extract the bean creation logic in your JpaConfig into reusable methods so we can call them when switching datasources:
@Configuration @EnableTransactionManagement @EnableJpaRepositories( entityManagerFactoryRef = "smsEntityManagerFactory", transactionManagerRef = "smsTransactionManager", basePackages = { "com.conceptualsystems.sms.db.repository" }) public class JpaConfig { // Keep the original @Bean method for initial creation @Bean(name="SMSX") @Primary public DataSource getDataSource() { return createDataSource(); } @Bean(name = "smsEntityManagerFactory") @Primary public LocalContainerEntityManagerFactoryBean smsEntityManagerFactory( EntityManagerFactoryBuilder builder, @Qualifier("SMSX") DataSource dataSource) { return createEntityManagerFactory(builder, dataSource); } @Bean(name = "smsTransactionManager") @Primary public PlatformTransactionManager smsTransactionManager( @Qualifier("smsEntityManagerFactory") EntityManagerFactory entityManagerFactory) { return createTransactionManager(entityManagerFactory); } // Reusable method to create DataSource with latest INI settings public DataSource createDataSource() { Logger logger = LoggerFactory.getLogger(this.getClass()); logger.error("DATABASE INITIALIZING: createDataSource() called!"); DataSourceBuilder builder = DataSourceBuilder.create(); builder.driverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); IniFile iniFile = IniSettings.getInstance().getIniFile(); builder.username(iniFile.getDbUser()); builder.password(iniFile.getDbPassword()); String host = iniFile.getDbPath(); String db = iniFile.getDbName(); String connectionString = "jdbc:sqlserver://" + host + ";databaseName=" + db; logger.info("Connecting [" + connectionString +"] as [" + iniFile.getDbUser() + ":" + iniFile.getDbPassword() + "]"); builder.url(connectionString); return builder.build(); } // Reusable method to create EntityManagerFactory public LocalContainerEntityManagerFactoryBean createEntityManagerFactory(EntityManagerFactoryBuilder builder, DataSource dataSource) { return builder .dataSource(dataSource) .persistenceUnit("smsEntityManagerFactory") .packages("com.conceptualsystems.sms.db.entity") .build(); } // Reusable method to create TransactionManager public PlatformTransactionManager createTransactionManager(EntityManagerFactory entityManagerFactory) { return new JpaTransactionManager(entityManagerFactory); } }
Step 2: Create a Datasource Switcher Service
Next, create a service class that handles destroying old beans and creating/registering new ones. This service will be called from your UI button click:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.stereotype.Service; import javax.sql.DataSource; import java.sql.SQLException; @Service public class DataSourceSwitcher { @Autowired private ApplicationContext applicationContext; @Autowired private JpaConfig jpaConfig; @Autowired private EntityManagerFactoryBuilder entityManagerFactoryBuilder; public void switchDataSource() throws SQLException { // Cast to ConfigurableApplicationContext to access bean factory ConfigurableApplicationContext ctx = (ConfigurableApplicationContext) applicationContext; DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) ctx.getBeanFactory(); // 1. Destroy old beans (in reverse order of dependency) destroyOldBeans(beanFactory); // 2. Create new beans with updated INI settings DataSource newDataSource = jpaConfig.createDataSource(); LocalContainerEntityManagerFactoryBean newEmfBean = jpaConfig.createEntityManagerFactory(entityManagerFactoryBuilder, newDataSource); // Initialize the EntityManagerFactory bean newEmfBean.afterPropertiesSet(); EntityManagerFactory newEmf = newEmfBean.getObject(); PlatformTransactionManager newTxManager = jpaConfig.createTransactionManager(newEmf); // 3. Register new beans as primary registerBeanAsPrimary(beanFactory, "SMSX", newDataSource, DataSource.class); registerBeanAsPrimary(beanFactory, "smsEntityManagerFactory", newEmf, EntityManagerFactory.class); registerBeanAsPrimary(beanFactory, "smsTransactionManager", newTxManager, PlatformTransactionManager.class); // Optional: Close old datasource to release connections DataSource oldDataSource = beanFactory.getBean("SMSX", DataSource.class); if (oldDataSource != null) { oldDataSource.close(); } } private void destroyOldBeans(DefaultListableBeanFactory beanFactory) { // Destroy transaction manager first, then entity manager factory, then datasource if (beanFactory.containsBean("smsTransactionManager")) { beanFactory.destroyBean("smsTransactionManager"); } if (beanFactory.containsBean("smsEntityManagerFactory")) { beanFactory.destroyBean("smsEntityManagerFactory"); } if (beanFactory.containsBean("SMSX")) { beanFactory.destroyBean("SMSX"); } } private <T> void registerBeanAsPrimary(DefaultListableBeanFactory beanFactory, String beanName, T beanInstance, Class<T> beanClass) { // Register the new bean instance beanFactory.registerSingleton(beanName, beanInstance); // Mark it as primary BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName); beanDefinition.setPrimary(true); } }
Step 3: Trigger Switch from UI Button
In your UI button's action listener, inject or get the DataSourceSwitcher bean from the application context, then call the switch method. For example:
// Inside your UI button's action performed method try { // Load the new INI file first (your existing logic to read new INI path from UI) IniFile newIniFile = new IniFile(newIniPathFromUI); newIniFile.load(); IniSettings.getInstance().setIniFile(newIniFile); // Get the DataSourceSwitcher from Spring context DataSourceSwitcher switcher = mApplicationContext.getBean(DataSourceSwitcher.class); switcher.switchDataSource(); // Show success message to user JOptionPane.showMessageDialog(null, "Datasource switched successfully!"); } catch (Exception e) { // Handle error, show message to user JOptionPane.showMessageDialog(null, "Failed to switch datasource: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); e.printStackTrace(); }
Key Notes & Considerations
- Transaction Safety: Make sure no database transactions are in progress when switching datasources. You might want to add a check for active transactions or pause operations temporarily.
- Connection Pool Cleanup: Calling
oldDataSource.close()ensures the connection pool is properly shut down and releases resources. - Error Handling: Add robust error handling in the switch method—if creating the new datasource fails, you might want to restore the old beans or notify the user clearly.
- JPA Repository Refresh: Since we're replacing the EntityManagerFactory, existing repository instances should automatically use the new one because they depend on the
@PrimaryEntityManagerFactory. If you run into issues, you might need to refresh the repository beans, but in most cases, this isn't necessary.
This approach keeps things simple, leverages your existing configuration logic, and lets you trigger the switch directly from your UI without external endpoints or complex routing.
内容的提问来源于stack exchange,提问作者moonlightcheese




