一、在介绍mybatis的使用之前,先接续上一篇JPA的使用,进行两者的相关简单对比。
1、 mybatis有个优势是,如果接收结果集中的属性没有找到相应的返回数据库字段,不会报错,将赋一个空值,而JPA会报错。
2、 mybatis可以单独的通过@Restult进行结果集中数据库字段与类对象属性的映射;
3、 mybatis不能像JPA一样,在接收对象里面再写一个对象属性来接收其他表的数据;
4、 mybatis的缺点则是返回结果集是数据库的原生字段,接收的结果集属性必须是直接对应数据库原生字段,当然可以为驼峰命名、或者sql返回别名以及map字段映射的方式。
5、 mybatis接受数据的类所有属性都必须是基础数据或者是基础数据的包装类,不能是自定义的对象
6、 如果要让insert ignore into 属性生效,那需要为不能重复的字段添加索引的唯一性约束(unique)。
7、 @Resource是JDK原生的注解,默认是根据bean的类名来识别的,@Autowired是spring框架的注解,默认是通过类型来识别的。mybatis在注入mapper的时候要用@Resource进行引入。当然也可以再mapper上在加一个service的注解,然后就可以通过@Autowired进行类型注入。
二、配置双数据源或多数据源,一般有两种方式。一种是动态设置数据源,一种是分包的方式写死数据源和mapper文件。
1 双数据源设置后,默认配置的数据源会报错,因为会存在多个同类型的bean,程序无法识别,需要单独对数据源进行配置注解别名。
2 在新的数据源配置文件的注解上,必须要配置改数据源扫描的Mapper文件,否则mapper文件也不知道用那个数据源,会报错
3 在新的数据源配置文件的注解上,必须要配置sqlSessionFactory的bean名字和sqlTemplete的bean名字。不然也会报错,识别不了
4 注解中引用的sqlSessionFactory和sqlTemplete,只需要引用其中一个就可以了,当然引用两个也没有问题,日志会有个重复引用的告警信息。
5 多个框架同时使用的时候也要注意,DataSocurce这个Bean被实例化话了多个后,如果对数据源进行别名的注解,也会造成无法识别。比如我下面的例子中实际上同时进入了hibernate和mybatis的依赖,可以根据需要进行调用。
6 Springboot2.2.5以后的类,数据源改用了建造者模式,无法直接通过注解导入属性后映射到数据源对象,需要手动一个一个设置导入的属性。也可以直接返回一个hirika池的数据源后者druid池的数据源。这连各需要通过new 实例化对象的数据源可以直接导入注解配置的属性。
7 mybatis双数据源配置完成后,分别写对应的mapper文件,在数据源配置文件中mapperscan需要应用的mapper。然后再在应用中注入mapper进行业务调用
8 mybatis双数据配置后,会造成原来的mybatis的配置也失效了。这时候需要重新继承org.apache.ibatis.session.Configuration 并且导入原有的yml文件的配置。然后将这个配置注入到新的数据源配置文件中的sqlSessionFactory中,具体为sqlSessionFactoryBean.SetConfiguration方法。
三、 采用AOP的方式来创建动态数据源
1 注意,新创建的子线程并不能调用到主线程注入的bean,直接调用为报null错误。需要先集成thread内,在thread类进行注入,或者通过全局静态类的方式在线程内获取IOC容器中的bean。然后再run方法进行调用,这个错误是在跑多数据源切换时,new 了多个线程来模拟并发的时候发生的。 2 动态的数据源可以通过继承AbstractRoutingDataSource抽象类,通过determineCurrentLookupKey()返回当前要调用的数据源的key来实现动态的数据源调用。 3 继承AbstractRoutingDataSource类,需要注入两个关键的bean,一个是默认的数据源,一个是可选数据源的集合,这个集合需要通过HashMap的数据结构来保存。注入的Bean可以通过构造函数方式进行注入,因为注入后需要SET给原来抽象类的相应属性 4 动态的数据源写好了,因为使用了自定义的数据源配置,因此和上面双数据源配置一样,这个数据源还需要注入sessionFactory和事务的bean。当然factory和tempelte的bean可以二选一在数据源配置类文件上面进行MapperScan注解引用。@MapperScan(basePackages = "com.ywcai.demo.doubleDs.aop", sqlSessionFactoryRef = "aopSqlSessionFactory", sqlSessionTemplateRef = "aopSqlSessionTemplate")。当然两个都指定了也没错,只是会有个告警信息。其他就是在这个配置中需要注入你上面第3步写好的动态数据源。另外还有些关于mybatis的全局配置,也需要单独应用后,在工厂的bean中进行设置。 5 新建一个工厂,然后设置数据源和mybatis的全局配置即可。 aopSqlSessionFactoryBean.setDataSource(aopRoutingDataSource); aopSqlSessionFactoryBean.setConfiguration(myBatisGlobalConfig); 6 接下来就是写一个调度数据源的类,这个也可以注解为一个bean,本例子bean类名为DBConetxtHolder。全局只要1个该类进行调度,但bean可以针对不同调用的线程保存独立的局部变量,以及保存所有线程调用数据源的总次数,进而可以动态的对数据源进行负载均衡。而要在这个bean里面保存个线程的局部变量,则需要利用ThreadLocal,为每个线程在ThreadLocal中保存一个自己的不变的枚举值。 7 通过AOP将Mapper作为切面,对每个包含get的方法前面切入数据源切换方法。既setSlave方法===>>>将DBConetxtHolder中的ThreadLocalcontextHolder = new ThreadLocal<>()中的contextHolder变量设置为为DBTypeEnum.Slave。每个线程对这个contextHolder 的操作都将单独被保存,不会被其他并发的现成所影响。 8 其他的就是AOP的常规操作注解及切入方式。将aop包里面的mapper作为切入点。入参规则 ,第一个*号代表匹配所有返回结果类型。第二个*号代表匹配所有该包下的所有类。第三个*号代表匹配所有方法,()号里面的两个点代表所有参数。第三星号和第二个型号之间2个点则表示匹配这个包根目录及子目录所有的包和类 9 mapper接口文件上的注解,如果只进行mapper注解,bean是默认通过名字进行注册的,需要再注解@service或者@Commpanet等,按照type进行装配。然后再调用的类中使用@Autowired才能注入获取的到引用。 10 springboot 2.2.5默认使用了hikariPool的连接池,如果多线程同时调用,会涉及到数据库池的调用,因此必须完成池的配置。需要单独导入hikariPool的配置进行初始化,不然报错。 11 目前采用的方式是直接初始化为hikari作为数据源进行初始化 12 另外在做多线程测试时候,一定要注意阻塞线程,不然子线程启动完成后,主线程就直接退出虚拟机了。 13 测试的时候可以用countdownlatch来阻塞主线程,监测所有子线线程是否执行完。也可以对阻塞每个子线程,让子线程顺序执行。也可以通过CompletionService的子类ExecutorCompletionService提交相应的callable。 ExecutorCompletionService则可以通过take().get()方法取出来已经执行完成的线程。而不是按照启动顺序去取,造成不必要的阻塞,这样先执行完的可以先返回结果。
四、下面将结合AOP方式设置双数据源并进行hikari池配置,以及如何在本地测试类中进行方法调用来对mybatis的使用进行介绍。
1、先看下依赖的引入,主要是仍然是下面的依赖。
mysql
mysql-connector-java
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.1.2
2、yml文件的主要配置,因为本次是自定义配置了数据源,因此大部分的数据操作相关配置都需要重新进行导入和处理。
#配置通过mybatis的驼峰命名,但是本次示例是自定义了数据源,因此这个配置需要单独导入。
mybatis:
configuration:
map-underscore-to-camel-case: true
#下面通过配置双数据的方式来访问进行主从访问。
#hikari数据库连接池配置.springboot2.0后默认使用了hikari连接池
#本次代码示例在注入数据源时直接将数据源配置为了hikari的数据源
ywcai:
master:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: Jdsj2019!
jdbc-url: jdbc:mysql://47.108.130.221:3306/ywcai?characterEncoding=utf-8&serverTimezone=UTC
#这里需要注意,hikari对应的数据库连接地址的属性是jdbcUrl,因此对应的配置属性是jdbc-url而不是用url
maximum-pool-size: 20 #默认是-1 既没有创建池。
auto-commit: true
minimum-idle: 5
pool-name: masterPools
slave:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: Jdsj2019!
jdbc-url: jdbc:mysql://47.108.130.221:3306/ywcai?characterEncoding=utf-8&serverTimezone=UTC
maximum-pool-size: 20 #默认是-1 既没有创建池。
auto-commit: true
minimum-idle: 5
pool-name: slavePools
3、主数据源、重数据源的配置文件 主数据源配置类
package com.ywcai.demo.doubleDs;
@Slf4j
@Configuration
@MapperScan(basePackageClasses = {MasterMapper.class}
, sqlSessionTemplateRef = "masterSqlSessionTemplate")
public class MasterDsConfig {
//这里将mybatis的配置单独导入后分别注入到master和salve的数据源配置中。
@Autowired
MyBatisGlobalConfig myBatisGlobalConfig;
@Bean(name = "masterDataSource")
@Qualifier(value = "masterDataSource")
@ConfigurationProperties(prefix = "ywcai.master.datasource")
public HikariDataSource dataSource() {
return new HikariDataSource();
}
//这是通过属性注入的方式
@Bean(name = "masterSqlSessionFactory")
@Qualifier(value = "masterSqlSessionFactory")
public SqlSessionFactory masterSqlSessionFactory(@Qualifier("masterDataSource") DataSource masterDataSource)
throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(masterDataSource);
//导入了原来的mybatis的配置
bean.setConfiguration(myBatisGlobalConfig);
//使用注解方式,这里就不用注入本地xml映射文件了。类上面已经导入了mapper包或者类
// bean.setMapperLocations(
// // 设置mybatis的xml所在位置
// new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/test01/*.xml"));
return bean.getObject();
}
@Bean(name = "masterSqlSessionTemplate")
@Qualifier(value = "masterSqlSessionTemplate")
public SqlSessionTemplate masterSqlSessionTemplate(@Qualifier("masterSqlSessionFactory")
SqlSessionFactory masterSqlSessionFactory) {
return new SqlSessionTemplate(masterSqlSessionFactory);
}
@Bean(name = "masterTransactionManager")
@Qualifier(value = "masterTransactionManager")
public DataSourceTransactionManager transactionManager
(@Qualifier("masterDataSource") DataSource masterDataSource) {
return new DataSourceTransactionManager(masterDataSource);
}
}
Slave数据源的配置类
package com.ywcai.demo.doubleDs;
@Slf4j
@Configuration
@MapperScan(basePackageClasses = {SlaveMapper.class}
, sqlSessionFactoryRef = "slaveSqlSessionFactory")
public class SlaveDsConfig {
//导入mybatis的配置后,注入配置
@Autowired
MyBatisGlobalConfig myBatisGlobalConfig;
@Bean(name = "slaveDataSource")
@Qualifier(value = "slaveDataSource")
@ConfigurationProperties(prefix = "ywcai.slave.datasource")
public HikariDataSource dataSource() {
return new HikariDataSource();
}
//这是通过属性注入的方式
@Bean(name = "slaveSqlSessionFactory")
@Qualifier(value = "slaveSqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("slaveDataSource") DataSource slaveDataSource)
throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(slaveDataSource);
bean.setConfiguration(myBatisGlobalConfig);
//使用注解方式,这里就不用注入本地xml映射文件了。类上面已经导入了mapper包或者类
// bean.setMapperLocations(
// // 设置mybatis的xml所在位置
// new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/test01/*.xml"));
return bean.getObject();
}
@Bean(name = "slaveSqlSessionTemplate")
@Qualifier(value = "slaveSqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate
(@Qualifier("slaveSqlSessionFactory") SqlSessionFactory slaveSqlSessionFactory) {
return new SqlSessionTemplate(slaveSqlSessionFactory);
}
@Bean(name = "slaveTransactionManager")
@Qualifier(value = "slaveTransactionManager")
public DataSourceTransactionManager transactionManager
(@Qualifier("slaveDataSource") DataSource slaveDataSource) {
return new DataSourceTransactionManager(slaveDataSource);
}
}
单独配置一个自动实例化的MyBatisGlobalConfig.class
package com.ywcai.demo.doubleDs;
//只需要继承父类,导入配置,实例化bean即可
@Configuration
@ConfigurationProperties("mybatis.configuration")
public class MyBatisGlobalConfig extends org.apache.ibatis.session.Configuration {
/**
* @描述 采用自定义配置后,还是需要把yml文件里面的关于batis开头的配置导入过来。
* @创建人 jimi
* @参数
* @返回值
* @创建时间 2020/3/15
*/
}
动态数据源配置
package com.ywcai.demo.doubleDs.aop;
//动态数据源的配置,需要继承AbstractRoutingDataSource类,并重写determineCurrentLookupKey()
//这里需要实例化动态数据源
@Configuration
@Slf4j
public class AopRoutingDataSource extends AbstractRoutingDataSource {
@Autowired
DBContextHolder dbContextHolder;
@Override
protected Object determineCurrentLookupKey() {
return dbContextHolder.get();
}
@Autowired
public AopRoutingDataSource(
@Qualifier(value = "masterDataSource") HikariDataSource masterDataSource,
@Qualifier(value = "slaveDataSource") HikariDataSource slaveDataSource) {
setDefaultTargetDataSource(masterDataSource);
log.info("masterDataSource {}", masterDataSource.getHikariPoolMXBean());
log.info("slaveDataSource {}", slaveDataSource);
Map
通过固定的两个静态方法来指定当前线程的数据源,利用threadLocal方法存储当前线程内的枚举值,保证数据源切换不被其他并发线程所影响。DBContextHolder.class
package com.ywcai.demo.doubleDs.aop;
@Service
@Slf4j
public class DBContextHolder {
private ThreadLocal contextHolder = new ThreadLocal<>();
//这个用来计算当前累加的访问数据库的数量,保证数据的有序增长。在Aop中进行自增加。
private AtomicInteger counter = new AtomicInteger(-1);
public void set(DBTypeEnum type) {
contextHolder.set(type);
}
public DBTypeEnum get() {
return contextHolder.get();
}
//如果是修改数据,则只会在主库做操作
public void setMaster() {
set(DBTypeEnum.MASTER);
}
//如果是查询数据,则是在主库查一次、从库查一次,做负载均衡
public void setSlave() {
//因为初始值为-1,因此这里先自增,然后再取2的模,将先再主库查询
int a = counter.incrementAndGet();
if (a % 2 == 0) {
log.info("=============select master ============{}", a);
set(DBTypeEnum.MASTER);
} else {
log.info("=============select slave ============{}", a);
set(DBTypeEnum.SLAVE);
}
}
}
标记是那个数据源的枚举类
package com.ywcai.demo.doubleDs.aop;
public enum DBTypeEnum {
MASTER, SLAVE;
}
测试的Mapper接口
package com.ywcai.demo.doubleDs.aop;
@Service
@Mapper
public interface AopMapper {
@Select("SELECT * FROM user ")
List getAllUserInfo();
@Delete("delete from roles where roles.user_id!=#{userId}")
int deleteRole(@Param(value = "userId") long userId);
}
对应的映射结果集的实体类
TestRole.class
package com.ywcai.demo.doubleDs.aop;
import lombok.Data;
@Data
public class TestRole {
long id;
String roleName;
long userId;
}
TestUser.class
package com.ywcai.demo.doubleDs.aop;
import lombok.Data;
@Data
public class TestUser {
long id;
String username;
String password;
}
AOP切面处理DBAspect.class
package com.ywcai.demo.doubleDs.aop;
@Aspect
@Component
@Slf4j
public class DBAspect {
@Autowired
DBContextHolder dbContextHolder;
//将aop包里面的mapper作为切入点
//入参规则 ,第一个*号代表匹配所有返回结果类型
//第二个*号代表匹配所有改包的所有类
//第三个*号代表匹配所有方法,()号里面的两个点代表所有参数
@Pointcut(value =
"(execution(public * com.ywcai.demo.doubleDs.aop.AopMapper.get*(..)))||
(execution(public * com.ywcai.demo.doubleDs.aop.AopMapper.select*(..)))")
public void setSlave() {
}
//JoinPoint表示切入的具体方法信息,包括了方法名和形参。在切入点之前需要做的事
//这里为了测试效果,可以将主数据源设置为错误的库路径,从库数据源路径设置为正确。
最终get相关的方法正常运行,其他方法报错。
@Before("setSlave()")
public void beforeSlave(JoinPoint joinPoint) {
//如果方法名字包含了select或者get的,默认是读数据,走SLAVE,其他的方法则走主数据源
//走SLAVE的方法,这里做的DEMO 也会进行选择,根据调用方法的次数,单数走SLAVE,双数走MASTER,次数从0开始。
dbContextHolder.setSlave();
}
}
调用测试的测试类,里面有几个不同的测试方法
package com.ywcai.demo.doubleDs.aop;
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
class AopDsTest {
@Autowired
AopMapper aopMapper;
@Test
void testAopGet() {
// CountDownLatch,可以用于等待所有线程结束,进行结果的同步.
ExecutorService exec = Executors.newFixedThreadPool(5);
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
AopThread aopThread = new AopThread(countDownLatch);
exec.submit(aopThread);
}
//这里注意,多线程,一定要在方法结束的时候阻塞线程,否则jvm退出会导致线程无法运行完。
//因此用了countDownlatch来检测线程执行结束的情况
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("exe complete !");
}
//返回多个线程执行后返回的数据并对list长度进行加法处理
@Test
void testAopGet3() {
LinkedBlockingDeque linkedBlockingDeque = new LinkedBlockingDeque(100);
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
Callable callable = new CallableTask();
//可以直接执行后返回future的接口
// Future future = executorService.submit(callable);
//也可以将callable包装为FutureTask,然后提交执行,没有本质区别
FutureTask futureTask = new FutureTask(callable);
executorService.submit(futureTask);
// futureTask.run();
try {
linkedBlockingDeque.putLast(futureTask);
} catch (InterruptedException e) {
e.printStackTrace();
}
//需要有个集合来把数据存一下,然后再主线程去遍历。不然这里使用 get会被阻塞
}
//这里注意,多线程,一定要在方法结束的时候阻塞线程,否则jvm退出会导致线程无法运行完。
//这里遍历出结果,遍历结果的get方法时,会阻塞子线程,知道子线程执行完成返回结果
//这里自定义了一个双端队列来获取当前处理完成的结果,避免被某一个耗时线程所阻塞
while (linkedBlockingDeque.size() > 0) {
try {
log.info("this result {}", ((FutureTask) linkedBlockingDeque.pollFirst()).get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
//通过ExecutorCompletionService直接将完成的现成结果包装到了双端队列。然后可以take().get()已完成的结果
@Test
void testAopGet4() {
ExecutorService executorService = Executors.newFixedThreadPool(10);
CompletionService completionService = new ExecutorCompletionService(executorService);
for (int i = 0; i < 10; i++) {
Callable callable = new CallableTask();
completionService.submit(callable);
//需要有个集合来把数据存一下,然后再主线程去遍历。不然这里使用 get会被阻塞
}
//这里可以直接通过completionService打印出callable的结果。
for (int i = 0; i < 10; i++) {
try {
log.info("the list size is {}", completionService.take().get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
//测试一个delete方法,只会用master数据源操作。
@Test
void testAopDelete() {
log.info("aopMapper delete role info {}", aopMapper.deleteRole(11l));
}
}
另外测试方法中还需辅助的执行任务,对应不同的测试方法
Callable返回值的辅助线程
package com.ywcai.demo.doubleDs.aop;
@Slf4j
public class CallableTask implements Callable {
@Override
public Integer call() {
AopMapper aopMapper = GetBeanUtil.getBean(AopMapper.class);
int i = aopMapper.getAllUserInfo().size();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("Thread ID is {}", Thread.currentThread().getId());
return i;
}
}
通过countdowncatch阻塞主线程的辅助线程
AopThread.class
package com.ywcai.demo.doubleDs.aop;
@Slf4j
public class AopThread extends Thread {
AopMapper aopMapper;
CountDownLatch countDownLatch;
public AopThread(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
aopMapper = GetBeanUtil.getBean(AopMapper.class);
}
@Override
public void run() {
super.run();
try {
log.info("userinfo : {}", aopMapper.getAllUserInfo());
} catch (Exception e) {
log.error("AopThread run err : {}", e);
} finally {
countDownLatch.countDown();
}
}
}
最后附一个测试结果截图。