1.环境参数
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.1</version>
<relativePath/>
</parent>
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.53</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
项目使用mybaits的多数据源dynamic datasource包,采用yml配置的形式,配合@DS("xxx")注解实现多数据源切换(因为数据源是确定的,故没有采用数据源配置到数据库动态加载的形式)
yml配置如下:
spring:
datasource:
dynamic:
primary: master
strict: false
datasource:
master:
url: jdbc:mysql://xxx.xxx.xxx.xxx:3306/db1?characterEncoding=UTF-8&useServerPrepStmts=false&rewriteBatchedStatements=true&autoReconnect=true&failOverReadOnly=false&serverTimezone=Asia/Shanghai
username: xxxx
password: xxxxxxx
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
connection-test-query: SELECT 1
slave:
url: jdbc:mysql://xxx.xxx.xxx.xxx:3306/db2?characterEncoding=UTF-8&useServerPrepStmts=false&rewriteBatchedStatements=true&autoReconnect=true&failOverReadOnly=false&serverTimezone=Asia/Shanghai
username: xxxxxx
password: xxxxxxx
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
connection-test-query: select 1 from dual
业务需求是增加两个数据源,是同一个mysql数据源下的两个数据库。但是这个mysql数据库需要使用ssh转发的形式才能访问,这里采用JSCH这个包针对指定端口进行转发,其他端口不转发。
1.新建SSHConfiguration类
@Component
@Slf4j
public class SSHConfiguration {
// 需要ssh连接到的机器地址和账号密码
private final static String SSH_REMOTE_SERVER = "xxx.xxx.xxx.xxx";
private final static int SSH_REMOTE_PORT = 22;
private final static String SSH_USER = "xxxx";
private final static String SSH_PASSWORD = "xxxxxx";
// 需要最终代理到的数据库地址(直连无法访问,必须ssh连上目标机器后才能访问)
private final static String MYSQL_REMOTE_SERVER = "xxx.xxx.xxx.xxx";
private final static int REMOTE_PORT = 3306;
// 需要代理的端口,项目启动后,走该端口的请求都会代理到ssh机器上
public static int PROXY_PORT = 0;
// ssh连接会话
private Session sesion;
// 连接ssh机器
public SSHConnection() throws Throwable {
JSch jsch = new JSch();
sesion = jsch.getSession(SSH_USER, SSH_REMOTE_SERVER, SSH_REMOTE_PORT);
sesion.setPassword(SSH_PASSWORD);
Properties config = new Properties();
// 设置为no即为无脑接受ssh机器的key,其实是一种不安全的做法,根据实际情况做选择
config.put("StrictHostKeyChecking", "no");
sesion.setConfig(config);
sesion.connect();
// 切记这里要这样写,端口写0目的是让运行环境的机器自主分配代理的端口,如果这里写死可能会引发端口占用的问题,让机器自己选择空闲的端口是更好的选择,坏处就是数据库连接地址无法配置到yml文件里,因为接口是动态的
PROXY_PORT = sesion.setPortForwardingL(0, MYSQL_REMOTE_SERVER, REMOTE_PORT);
// 像下面这种写法就可能会导致端口绑定错误
// session.setPortForwardingL("localhost",REMOTE_PORT,MYSQL_REMOTE_SERVER,REMOTE_PORT);
log.info("SSH代理连接成功!");
}
public void closesSH() {
sesion.disconnect();
}
}
2.配置动态数据库
使用mybatis-plus自带的加载动态数据源的方式,参考文档地址:https://www.kancloud.cn/tracy5546/dynamic-datasource/2268595 (需要付费)
我这里动态添加数据源的类实现了CommandLineRunner接口,为的是springboot启动成功后就加载这两个数据源,然后数据源具体代理的端口都是根据SSHConfiguration连接成功后分配的端口。
代码如下:
@Slf4j
@Component
public class DynamicAddDS implements CommandLineRunner {
@Autowired
private DataSource dataSource;
// private final DataSourceCreator dataSourceCreator; //3.3.1及以下版本使用这个通用
@Autowired
private HikariDataSourceCreator hikariDataSourceCreator;
void addSshDs1() {
DataSourceProperty dataSourceProperty = new DataSourceProperty();
dataSourceProperty.setUrl("jdbc:mysql://127.0.0.1:" + SSHConnection.PROXY_PORT + "/db1?characterEncoding=UTF-8&useServerPrepStmts=false&rewriteBatchedStatements=true&autoReconnect=true&failOverReadOnly=false&serverTimezone=Asia/Shanghai");
dataSourceProperty.setUsername("xxxx");
dataSourceProperty.setPassword("xxxxxx");
dataSourceProperty.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSourceProperty.setLazy(true);//3.4.0版本以下如果有此属性,需手动设置,不然会空指针。
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
DataSource dataSource = hikariDataSourceCreator.createDataSource(dataSourceProperty);
log.info("设置proxyds1数据源=" + dataSourceProperty.getUrl());
// 注意下面方法的第一个形参(String ds),这个名称就是@DS("xxx")注解的名称,后面即可用@DS("proxyds1")去控制需要以哪个数据源去查询
ds.addDataSource("proxyds1", dataSource);
}
void addSshDs2() {
DataSourceProperty dataSourceProperty = new DataSourceProperty();
dataSourceProperty.setUrl("jdbc:mysql://127.0.0.1:" + SSHConnection.PROXY_PORT + "/db2?characterEncoding=UTF-8&useServerPrepStmts=false&rewriteBatchedStatements=true&autoReconnect=true&failOverReadOnly=false&serverTimezone=Asia/Shanghai");
dataSourceProperty.setUsername("xxxx");
dataSourceProperty.setPassword("xxxxxx");
dataSourceProperty.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSourceProperty.setLazy(true);//3.4.0版本以下如果有此属性,需手动设置,不然会空指针。
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
DataSource dataSource = hikariDataSourceCreator.createDataSource(dataSourceProperty);
log.info("设置proxyds2数据源=" + dataSourceProperty.getUrl());
ds.addDataSource("proxyds2", dataSource);
}
@Override
public void run(String... args) throws Exception {
addSshDs1();
addSshDs2();
}
}
这样基本完成了主要配置,随便写一个查询Mapper接口
@DS("proxyds1")
@Mapper
public interface TestDao extends BaseMapper<Map<String,Object>> {
/**
* 随便写的测试方法
*
* @return {@link List}<{@link Map}<{@link String},{@link String}>>
*/
@Select("select count(1) from sys_user")
int coutAll();
}
这样就可以在service注入这个Mapper进行查询了
3.总结
这个方案适合原有的多数据源都配置在yml文件中的项目,因为如果全部改造成动态数据源且数据源参数写入数据中的话,工程量稍稍有点大了。
代理的端口一定要给JSCH自动分配,不然容易出现本机运行正常,部署到服务器出现错误,这种错误就是需要代理的端口在该机器上已被占用了 ``` com.jcraft.jsch.JSchException: PortForwardingL: local port 127.0.0.1:3306 cannot be bound. at com.jcraft.jsch.PortWatcher.
也就是要使用下面方法的返回值去动态的配置代理端口
int proxyPort = session.setPortForwardingL(0,host,port)
而不能使用下面这种方式
session.setPortForwardingL(resource_host,resource_port,host,port)
还有要注意如下方法中的第一个形参名称,后续@DS("xxx")注解全靠这个参数作为标识了
ds.addDataSource("proxyds2", dataSource);
示例代码:https://github.com/ong-ing/case_analysis_demo/tree/master/ssh_dynamic_ds