SpringBoot3使用动态数据源

547 阅读6分钟

需求背景

日前经历了一个项目,客户有不同的数据源,目前有Oracle和MySQL,数据库的表结构一样,所以业务代码没有区别,数据源是动态变化的,用户可以在使用的过程中切换数据源。

期望的效果

目前有两个数据源:Oracle和MySQL的数据源,其中一个表的数据分别如下: Oracle:

image.png

MySQL:

image.png

不同数据源下的表结构相同,但是数据不同。

期望的效果就是用户通过类似下面的界面切换数据源,后续的业务都使用新切换的数据源。

image.png

代码实现

MyBatis Plus有个插件,实现了动态数据源,官方文档地址: github.com/baomidou/dy…

从这里索引,会有一个更详细的文档,不过这个更详细的文档大部分收费,这是我没有想到的。 我们这个需求比较简单,使用这个插件的一小部分功能即可。

Maven引入

Maven中引入最新版的插件


<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
    <version>4.3.1</version>
</dependency>

配置文件修改

动态数据源的插件一般的用法都是多数据源,但是我们不需要多数据源,在同一个时刻,只需要一个数据源,并且这个数据源是用户手动配置的,所以我们期望在程序启动时无数据源,为此配置文件的数据源做如下配置:


spring:
  datasource:
    dynamic:
      primary: master
      strict: false

我们数据源的名字固定为master,需要注意的是strict设置为false。这样程序启动时,不会因为没有数据源就报错,但是会出现这么一条log:

dynamic-datasource initial loaded [0] datasource,Please add your primary datasource or check your configuration

定义数据源连接DTO

/**
 * 数据源连接DTO.
 *
 * @author liupeng
 * @date 2025/3/6
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class DataSourceDTO {
    public static final String DEFAULT_POOL_NAME = "master";

    private String poolName;
    private String driverClassName;
    private String url;
    private String username;
    private String password;
}

连接数据源

定义一个Controller类,来连接数据源。

@RestController
@RequestMapping("/datasource")
@RequiredArgsConstructor
@Tag(name = "连接数据源", description = "连接数据源")
public class DataSourceController {


    /**
     * The Data source.
     */
    final DataSource dataSource;

    /**
     * The Default data source creator.
     */
    final DefaultDataSourceCreator defaultDataSourceCreator;

    private final ModelService modelService;

    /**
     * 连接数据源.
     *
     * @param connRequest the data source dto
     * @return the response entity
     */
    @PostMapping("/connect")
    @Operation(summary = "连接数据源", description = "连接数据源")
    public ResponseEntity<ResultEntity<Boolean>> connect(@Valid @RequestBody DataSourceConnRequest connRequest) {
        DataSourceProperty dataSourceProperty = new DataSourceProperty();
        DataSourceDTO dataSourceDTO = DataSourceConnRequest.toDataSource(connRequest);
        BeanUtils.copyProperties(dataSourceDTO, dataSourceProperty);
        DynamicRoutingDataSource source = (DynamicRoutingDataSource) dataSource;
        try {
            // 如果连接信息不正确,这一步会抛出异常, 数据源创建失败
            DataSource dataSourceLocal = defaultDataSourceCreator.createDataSource(dataSourceProperty);
            source.addDataSource(dataSourceDTO.getPoolName(), dataSourceLocal);
            // 数据源连接之后检索数据看看结果,这个对于数据库连接不上必须的
            modelService.listAllModels();
        } catch (Exception e) {
            return ResponseEntity.ok(ResultEntity.error("连接失败, 请确认数据库连接参数是否正确"));
        }
        return ResponseEntity.ok(ResultEntity.ok(true));
    }

    /**
     * 关闭数据源.
     *
     * @return the response entity
     */
    @PostMapping("/close")
    @Operation(summary = "关闭数据源", description = "关闭数据源")
    public ResponseEntity<ResultEntity<Boolean>> close() {
        DynamicRoutingDataSource source = (DynamicRoutingDataSource) dataSource;
        source.removeDataSource(DataSourceDTO.DEFAULT_POOL_NAME);
        return ResponseEntity.ok(ResultEntity.ok(true));
    }
}

上面的代码也顺便创建了一个关闭数据源的方法,事实证明这个不是必须的。

用到了一个请求类,DataSourceConnRequest,代码如下:

/**
 * 数据库连接请求.
 *
 * @author liupeng
 * @date 2025 /3/6
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(title = "数据库连接请求对象")
public class DataSourceConnRequest {

    /**
     * The constant DB_TYPE_MYSQL.
     */
    public static final String DB_TYPE_MYSQL = "mysql";

    /**
     * The constant DB_TYPE_ORACLE.
     */
    public static final String DB_TYPE_ORACLE = "oracle";

    /**
     * The constant driver_type_thin.
     */
    public static final String DRIVER_TYPE_THIN = "thin";

    /**
     * The constant driver_type_oci.
     */
    public static final String DRIVER_TYPE_OCI = "oci";

    /**
     * The constant driver_type_oci8.
     */
    public static final String DRIVER_TYPE_OCI8 = "oci8";

    public static final String DRIVER_CLASS_NAME_ORACLE = "oracle.jdbc.driver.OracleDriver";

    public static final String DRIVER_CLASS_NAME_MYSQL = "com.mysql.cj.jdbc.Driver";

    @Schema(title = "数据库类型. 支持: MySql, Oracle")
    @NotBlank(message = "必须选择数据库类型")
    private String dbType;

    @Schema(title = "主机")
    @NotBlank(message = "必须选择主机")
    private String host;

    @Schema(title = "端口, 在不填写的情况下, MySql默认3306, Oracle默认1521")
    private String port;

    @Schema(title = "Oracle的情况请输入正确的SID, MySql的场合下输入数据库名称")
    @NotBlank(message = "必须选择MySQL的数据库或者Oracle的SID")
    private String database;

    @Schema(title = "Oracle的情况下适用, 默认为Thin, 支持Thin, OCI, OCI8")
    private String driverType;

    @Schema(title = "用户名")
    @NotBlank(message = "必须选择用户名")
    private String username;

    @Schema(title = "密码")
    @NotBlank(message = "必须输入密码")
    private String password;

    /**
     * To data source data source dto.
     *
     * @param request the request
     * @return the data source dto
     */
    public static DataSourceDTO toDataSource(DataSourceConnRequest request) {
        if (request == null) {
            return null;
        }
        if (!StringUtils.hasText(request.getDbType())) {
            return null;
        }
        if (!DB_TYPE_MYSQL.equalsIgnoreCase(request.getDbType()) && !DB_TYPE_ORACLE.equalsIgnoreCase(request.getDbType())) {
            return null;
        }
        //     private String driverClassName;
        String driverClassName = getDriverClassName(request.getDbType());
        if (!StringUtils.hasText(driverClassName)) {
            return null;
        }
        //    private String url;
        String url = getUrl(request);
        if (!StringUtils.hasText(url)) {
            return null;
        }
        //    private String username;
        String username = request.getUsername();
        //    private String password;
        String password = request.getPassword();
        return DataSourceDTO.builder()
            .poolName(DataSourceDTO.DEFAULT_POOL_NAME)
            .url(url)
            .driverClassName(driverClassName)
            .username(username)
            .password(password)
            .build();
    }

    private static String getUrl(DataSourceConnRequest request) {
        if (!StringUtils.hasText(request.getDatabase())) {
            return "";
        }
        if (DB_TYPE_MYSQL.equalsIgnoreCase(request.getDbType())) {
            String port = request.getPort();
            if (!StringUtils.hasText(port)) {
                port = "3306";
            }
            return "jdbc:mysql://" + request.getHost()
                + ":"
                + port + "/"
                + request.getDatabase()
                + "?useUnicode=true&characterEncoding=UTF-8";
        }
        if (DB_TYPE_ORACLE.equalsIgnoreCase(request.getDbType())) {
            String port = request.getPort();
            if (!StringUtils.hasText(port)) {
                port = "1521";
            }
            String driverType = request.getDriverType();
            if (!StringUtils.hasText(driverType)) {
                driverType = "thin";
            }
            return "jdbc:oracle:"
                + driverType
                + ":@" + request.getHost()
                + ":"
                + port
                + ":"
                + request.getDatabase();
        }
        return "";
    }

    private static String getDriverClassName(@NotBlank(message = "必须选择数据库类型") String dbType) {
        if (DB_TYPE_MYSQL.equalsIgnoreCase(dbType)) {
            return DRIVER_CLASS_NAME_MYSQL;
        }
        if (DB_TYPE_ORACLE.equalsIgnoreCase(dbType)) {
            return DRIVER_CLASS_NAME_ORACLE;
        }
        return "";
    }
}

这个只是检证用的代码,有些地方写得不够好,但是对于理解概念足够清晰了。

访问Model表数据业务逻辑

Controller

@RestController
@RequestMapping("/api/model")
@RequiredArgsConstructor
@Tag(name = "模型接口", description = "模型接口")
public class ModelController {

    private final ModelService modelService;

    /**
     * Register response entity.
     *
     * @param id the id
     * @return the response entity
     */
    @PostMapping("/detail/{id}")
    @Operation(summary = "模型详情", description = "根据ID查看详情")
    @Parameters({
        @Parameter(in = ParameterIn.PATH, name = "id", description = "ID", required = true)
    })
    public ResponseEntity<ResultEntity<ModelDTO>> register(@PathVariable(name = "id") String id) {
        ResultEntity<ModelDTO> detail = ResultEntity.ok(modelService.detail(id));
        return ResponseEntity.ok(detail);
    }

    @PostMapping("/all")
    @Operation(summary = "列出所有模型", description = "列出所有模型")
    @Parameters({
        @Parameter(in = ParameterIn.PATH, name = "id", description = "ID", required = true)
    })
    public ResponseEntity<ResultEntity<List<ModelDTO>>> register() {
        ResultEntity<List<ModelDTO>> models = ResultEntity.ok(modelService.listAllModels());
        return ResponseEntity.ok(models);
    }

}

Service

public interface ModelService extends IService<Model> {


    /**
     * 模型详情.
     *
     * @param id the id
     * @return the model dto
     */
    ModelDTO detail(String id);

    /**
     * 列出所有模型.
     *
     * @return the list
     */
    List<ModelDTO> listAllModels();
}

实现类:

@Service
@Slf4j
@RequiredArgsConstructor
public class ModelServiceImpl extends ServiceImpl<ModelMapper, Model> implements ModelService {

    /**
     * 模型详情.
     *
     * @param id the id
     * @return the model dto
     */
    @Override
    public ModelDTO detail(String id) {
        Model entity = this.baseMapper.selectById(id);
        return ModelConverter.INSTANCE.toDto(entity);
    }

    /**
     * 列出所有模型.
     *
     * @return the list
     */
    @Override
    public List<ModelDTO> listAllModels() {
        List<Model> entities = list();
        return ModelConverter.INSTANCE.toDtoList(entities);
    }
}

异常处理

没有连接数据源的时候如果试图访问数据库,应该报出合理的错误信息。

@ExceptionHandler(DataAccessException.class)
public ResponseEntity<ResultEntity<String>> dataAccessException(DataAccessException exception) {
    log.error("[服务] - [捕获SQL异常]", exception);
    String message = exception.getMessage();
    if (StringUtils.hasText(message) && message.indexOf("CannotFindDataSourceException") > 0) {
        message = "请先连接数据源再访问数据库";
    }
    return ResponseEntity.ok(ResultEntity.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), message));
}

验证结果

程序启动后不连接数据源

程序启动后不连接数据源,这时没有数据源,肯定会抛出异常,访问接口的结果:

image.png

连接Oracle数据源

请求连接Oracle数据源:

image.png

脱敏后的请求JSON对象如下:

{

  "dbType": "oracle",
  "host": "123.x.x.x",
  "port": "",
  "database": "XXXX",
  "driverType": "",
  "username": "XXX",
  "password": "XXX"

}

数据源连接成功后再访问业务接口:

image.png

这时候就成功了, 跟我们文章开头给出的数据源一致。

连接MySQL数据源

我们重新连续MySQL数据源,由于代码中我们使用的键值都是master,会自动冲掉Oracle的数据源连接。

image.png

脱敏后端JSON请求对象:

{
  "dbType": "mysql",
  "host": "localhost",
  "port": "",
  "database": "XXX",
  "driverType": "",
  "username": "XXX",
  "password": "XXX"
}

这时候再访问业务接口:

image.png

返回了MySQL数据源的业务数据。

至此,已经达成了我们期望的目标。

关闭数据源

目前业务上不需要关闭数据源,但是关闭数据源也是有效的。 不过我们的实现有点问题,大家可以试试看,使用本文中的代码关闭数据源会出现什么现象。