您的位置:

Spring Boot动态数据源实现详解

一、为什么需要动态数据源

在项目开发中,我们经常需要使用多个数据源,比如主库和从库的读写分离,或者不同的租户使用不同的数据源等。如果每个数据源都需要手动配置,那么会增加很多不必要的工作量,而且代码会变得冗长和难以维护。这时候,动态数据源就可以派上用场。

动态数据源就是可以在程序运行时根据需要动态地切换数据库连接的数据源。它可以减少不必要的配置工作,提高代码的可维护性和可扩展性。

二、如何实现动态数据源

1. 自定义数据源

首先,我们需要定义一个数据源的抽象,具体实现交给具体的数据源来完成。下面是一个简单的抽象示例:

public abstract class AbstractRoutingDataSource extends AbstractDataSource {

    /**
     * 获取当前线程上的数据源路由
     *
     * @return 数据源路由,如果为空,则默认返回默认数据源
     */
    protected abstract Object determineCurrentDataSourceKey();

    @Override
    public Connection getConnection() throws SQLException {
        // 获取数据源
        DataSource dataSource = determineCurrentDataSource();
        // 获取连接
        Connection connection = dataSource.getConnection();
        // 返回连接
        return connection;
    }

    /**
     * 获取当前数据源
     *
     * @return 当前数据源
     */
    private DataSource determineCurrentDataSource() {
        // 获取数据源路由
        Object dataSourceKey = determineCurrentDataSourceKey();
        // 如果数据源路由为空,则返回默认数据源
        if (dataSourceKey == null) {
            return getDefaultDataSource();
        }
        // 否则返回指定的数据源
        DataSource dataSource = getTargetDataSources().get(dataSourceKey);
        if (dataSource == null) {
            throw new IllegalStateException("Cannot find data source with key: " + dataSourceKey);
        }
        return dataSource;
    }

}

在上面的抽象类中,我们定义了一个determineCurrentDataSourceKey()方法来获取当前线程上的数据源路由,以及determineCurrentDataSource()方法来获取当前数据源。具体的数据源实现需要继承这个抽象类,并实现这两个方法。下面是一个示例:

public class DynamicRoutingDataSource extends AbstractRoutingDataSource {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    private final Map<String, DataSource> dataSources = new ConcurrentHashMap<>();

    private DataSource defaultDataSource;

    @Override
    protected Object determineCurrentDataSourceKey() {
        return DataSourceContextHolder.getDataSourceKey();
    }

    @Override
    public void afterPropertiesSet() {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("default", defaultDataSource);
        for (String key : dataSources.keySet()) {
            targetDataSources.put(key, dataSources.get(key));
        }
        setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    /**
     * 添加数据源
     *
     * @param key       数据源标识
     * @param dataSource 数据源
     */
    public void addDataSource(String key, DataSource dataSource) {
        dataSources.put(key, dataSource);
        logger.info("Add data source [{}]", key);
    }

    /**
     * 设置默认数据源
     *
     * @param defaultDataSource 默认数据源
     */
    public void setDefaultDataSource(DataSource defaultDataSource) {
        this.defaultDataSource = defaultDataSource;
    }

}

在上面的示例中,我们继承了抽象类,并实现了determineCurrentDataSourceKey()方法和afterPropertiesSet()方法。我们还可以添加数据源和设置默认数据源。这样一来,我们就可以动态地添加和切换数据源了。

2. 使用AOP切面切换数据源

接下来,我们需要通过AOP实现数据源的切换。这里我们使用@Around注解,实现在访问数据库前或者后切换数据源。

@Aspect
@Component
public class DataSourceAspect {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Around("execution(* com.example.service.*.*(..))")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        // 获取方法名
        String methodName = point.getSignature().getName();

        // 获取目标类
        Class targetClass = point.getTarget().getClass();

        // 获取数据源注解
        DataSource dataSource = targetClass.getAnnotation(DataSource.class);

        Method method = ((MethodSignature) point.getSignature()).getMethod();
        if (method.isAnnotationPresent(DataSource.class)) {
            dataSource = method.getAnnotation(DataSource.class);
            methodName = method.getName();
        }

        // 如果有数据源注解,则切换数据源
        if (dataSource != null) {
            String dataSourceKey = dataSource.value();
            DataSourceContextHolder.setDataSourceKey(dataSourceKey);
            logger.info("Switch data source to [{}] for method [{}]", dataSourceKey, methodName);
        }

        // 执行目标方法
        Object result = point.proceed();

        // 切换回默认数据源
        if (dataSource != null) {
            DataSourceContextHolder.clearDataSourceKey();
            logger.info("Restore data source to default for method [{}]", methodName);
        }

        return result;
    }

}

在上面的代码中,我们使用around()方法来拦截类中使用了@DataSource注解的方法,然后切换数据源。注意,我们使用了DataSourceContextHolder来存放当前数据源路由。

三、示例代码

1. 定义注解

我们需要定义一个@DataSource注解来标记使用哪个数据源:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    String value() default "default";
}

2. 数据源切换

我们使用DynamicRoutingDataSource来实现数据源的添加和切换:

@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.default")
    public DataSource defaultDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.test")
    public DataSource testDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.demo")
    public DataSource demoDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    public DynamicRoutingDataSource dynamicRoutingDataSource() {
        DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
        dataSource.setDefaultDataSource(defaultDataSource());

        dataSource.addDataSource("test", testDataSource());
        dataSource.addDataSource("demo", demoDataSource());

        return dataSource;
    }

    /**
     * 设置动态数据源
     *
     * @param dataSource 动态数据源
     * @return 动态数据源
     */
    @Bean
    public DataSource dataSource(DynamicRoutingDataSource dataSource) {
        return dataSource;
    }

}

注意,我们使用@ConfigurationProperties注解来自动加载数据源配置,然后使用DynamicRoutingDataSource将配置转换为数据源,并添加到数据源路由中。

3. 测试数据源切换

最后,我们来测试一下数据源切换是否生效。首先,我们需要使用@DataSource注解来指定使用哪个数据源:

@Mapper
public interface UserMapper {

    @Select("select id, name from user limit 1")
    @DataSource("test")
    User getTestUser();

    @Select("select id, name from user limit 1")
    @DataSource("demo")
    User getDemoUser();

}

然后,在UserService中调用UserMapper

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public User getTestUser() {
        return userMapper.getTestUser();
    }

    public User getDemoUser() {
        return userMapper.getDemoUser();
    }

}

最后,在测试用例中验证数据源是否正确切换:

@RunWith(SpringRunner.class)
@SpringBootTest
public class DataSourceTest {

    @Autowired
    private UserService userService;

    @Test
    public void testSwitchDataSource() {
        User testUser = userService.getTestUser();
        assertNotNull(testUser);

        User demoUser = userService.getDemoUser();
        assertNotNull(demoUser);
    }

}

如果测试用例可以正常运行,那么就说明数据源切换成功了。

四、总结

通过本文的介绍,我们学习了如何使用Spring Boot动态数据源,使得程序可以动态地切换不同的数据库连接。具体实现需要使用AOP切面和自定义数据源来实现。相信大家已经对动态数据源有了一定的了解,并可以根据需要自己实现动态数据源。