[TOC] # db-spring-boot-starter 前面项目中,咱们使用了 [01.db-core模块](01.db-core%E6%A8%A1%E5%9D%97.md)为整个项目提供通用的数据库处理,现在我们将采用springboot 标准starter的做法,重构项目基础组件,利用org.springframework.boot.autoconfigure,完成对象的基本装配。同时他具有以下功能: * 集成druid数据源 * 集成mybatis-plus * 动态数据源切换 * pagehelper分页处理 * Guava ## db-spring-boot-starter代码分析 * 工具类 ![](https://img.kancloud.cn/66/39/663991e94fed078fe0728f7a522e2a0a_1283x581.png) * AOP切换数据源类 ![](https://img.kancloud.cn/c5/f3/c5f3ac49fb355d8406a42407111f708b_1530x611.png) * 动态数据源定义core log ![](https://img.kancloud.cn/77/7e/777ede181c9cf389d069dc5636086b1b_1626x640.png) * 多数据源自动装配定义 ![](https://img.kancloud.cn/07/87/078711f35a963869ee0b8eeb4816b632_1834x645.png) ### druid配置 ``` initial-size: 1 max-active: 20 min-idle: 1 # 配置获取连接等待超时的时间 max-wait: 60000 #打开PSCache,并且指定每个连接上PSCache的大小 pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 validation-query: SELECT 'x' test-on-borrow: false test-on-return: false test-while-idle: true #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 time-between-eviction-runs-millis: 60000 #配置一个连接在池中最小生存的时间,单位是毫秒 min-evictable-idle-time-millis: 300000 ``` validationQuery和testWhileIdle这两个参数一起用,用来不间断检测是否有失效的链接,避免高并发的出现失效链接; 数据库连接池在初始化的时候会创建initialSize个连接,当有数据库操作时,会从池中取出一个连接。如果当前池中正在使用的连接数等于maxActive,则会等待一段时间,等待其他操作释放掉某一个连接,如果这个等待时间超过了maxWait,则会报错;如果当前正在使用的连接数没有达到maxActive,则判断当前是否空闲连接,如果有则直接使用空闲连接,如果没有则新建立一个连接。在连接使用完毕后,不是将其物理连接关闭,而是将其放入池中等待其他操作复用。 同时连接池内部有机制判断,如果当前的总的连接数少于miniIdle,则会建立新的空闲连接,以保证连接数得到miniIdle。如果当前连接池中某个连接在空闲了timeBetweenEvictionRunsMillis时间后仍然没有使用,则被物理性的关闭掉。有些数据库连接的时候有超时限制(mysql连接在8小时后断开),或者由于网络中断等原因,连接池的连接会出现失效的情况,这时候设置一个testWhileIdle参数为true,可以保证连接池内部定时检测连接的可用性,不可用的连接会被抛弃或者重建,最大情况的保证从连接池中得到的Connection对象是可用的。当然,为了保证绝对的可用性,你也可以使用testOnBorrow为true(即在获取Connection对象时检测其可用性),不过这样会影响性能。 ### 动态数据源详解 * [15.动态数据源配置](18.%E5%8A%A8%E6%80%81%E6%95%B0%E6%8D%AE%E6%BA%90%E9%85%8D%E7%BD%AE.md) ## db-spring-boot-starter 如何使用 > user-center代码 * user-center pom文件使用 ![](https://img.kancloud.cn/3f/6f/3f6f3f642f446d680a5af9300efc405e_1530x400.png) * user-center application.yml ![](https://img.kancloud.cn/40/a5/40a57e25f8c3cd6521c05798abf79b77_1846x617.png) * 编写dao xml代码 ![](https://img.kancloud.cn/50/2c/502c2953cf4547194c91a530b38cd903_1850x738.png) ## spring事务 ![](https://img.kancloud.cn/91/4b/914b4231d49e92b47243d3e629dacaef_2178x277.png) spring事务抽象 * PlatformTransactionManager 事务管理接口,事务的开启,提交,回滚 * TransactionDefinition 事务的属性,传播属性等 * TransactionStatus 事务的运行状态 ``` @Test // select @@GLOBAL.tx_isolation ,@@tx_isolation @Transactional( propagation = Propagation.REQUIRED ,isolation=Isolation.DEFAULT) public void testSaveException2() { Map dmo = Maps.newHashMap(); dmo.put("id", 3); dmo.put("name", "3"); testDao.save(dmo); Throwables.throwIfUnchecked(new RuntimeException("模拟业务出错")); } @Test public void testSaveException3() { DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); TransactionStatus status = txManager.getTransaction(definition) ; try{ Map dmo = Maps.newHashMap(); dmo.put("id", 5); dmo.put("name", "5"); testDao.save(dmo); Throwables.throwIfUnchecked(new RuntimeException("模拟业务出错")); txManager.commit(status); }catch (Exception e) { txManager.rollback(status); } } ``` ### aop与传播机制问题 ![](https://img.kancloud.cn/74/2b/742bfad2ab8772ccc979d001877254d8_1565x785.png) ### @Transactional try catch 后手动回退事务 ``` TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() ``` ### transactionTemplate 异常后回退事务 ``` transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION\_RE QUIRED); transactionTemplate.execute(item->{ try { compareService.insert2(); } catch (Exception e) { item.setRollbackOnly(); } return Boolean.FALSE; }); return "hello" ; } ``` ### 多线程与事务 ``` @Transactional( rollbackFor = Exception.class) public String delete(List<String> strList){ try{ ConnectionHolder connHolder1 = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource) ; // com.mysql.cj.jdbc.ConnectionImpl@214f85e6 IntStream.range(0, (strList.size() + 100 - 1)/100) .mapToObj(i -> strList.subList(i * 100, Math.min(strList.size(), (i+1) * 100))).parallel() .forEach(batch -> { ConnectionHolder connHolder2 = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource) ; if(connHolder1 == connHolder2) { System.out.println(1); } tableMapper.update("1"); } ); System.out.println(1/0); } catch (Exception e){ TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } return "OK"; } ``` ### CompletableFuture.runAsync 与事务问题 ![](https://img.kancloud.cn/a5/f6/a5f6456fb6bfad7a902c38964e75e9a0_1023x641.png) 需要下沉到另外一个类 ![](https://img.kancloud.cn/ab/75/ab75e8f0c13966e55365c9cb82b27fab_1070x682.png) ### 源码解析 ~~~ public static Connection doGetConnection(DataSource dataSource) throws SQLException { Assert.notNull(dataSource, "No DataSource specified"); //TransactionSynchronizationManager重点!!!有没有很熟悉的感觉?? //还记得我们前面Spring事务源码的分析吗?@Transaction会创建Connection,并放入ThreadLocal中 //这里从ThreadLocal中获取ConnectionHolder ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource); if (conHolder == null || !conHolder.hasConnection() && !conHolder.isSynchronizedWithTransaction()) { logger.debug("Fetching JDBC Connection from DataSource"); //如果没有使用@Transaction,那调用Mapper接口方法时,也是通过Spring的方法获取Connection Connection con = fetchConnection(dataSource); if (TransactionSynchronizationManager.isSynchronizationActive()) { logger.debug("Registering transaction synchronization for JDBC Connection"); ConnectionHolder holderToUse = conHolder; if (conHolder == null) { holderToUse = new ConnectionHolder(con); } else { conHolder.setConnection(con); } holderToUse.requested(); TransactionSynchronizationManager.registerSynchronization(new DataSourceUtils.ConnectionSynchronization(holderToUse, dataSource)); holderToUse.setSynchronizedWithTransaction(true); if (holderToUse != conHolder) { //将获取到的ConnectionHolder放入ThreadLocal中,那么当前线程调用下一个接口,下一个接口使用了Spring事务,那Spring事务也可以直接取到Mybatis创建的Connection //通过ThreadLocal保证了同一线程中Spring事务使用的Connection和Mapper代理类使用的Connection是同一个 TransactionSynchronizationManager.bindResource(dataSource, holderToUse); } } return con; } else { conHolder.requested(); if (!conHolder.hasConnection()) { logger.debug("Fetching resumed JDBC Connection from DataSource"); conHolder.setConnection(fetchConnection(dataSource)); } //所以如果我们业务代码使用了@Transaction注解,在Spring中就已经通过dataSource创建了一个Connection并放入ThreadLocal中 //那么当Mapper代理对象调用方法时,通过SqlSession的SpringManagedTransaction获取连接时,就直接获取到了当前线程中Spring事务创建的Connection并返回 return conHolder.getConnection(); } } 我们看到直接从ThreadLocal中取出来的conn,而spring自己的事务也是操作的这个ThreadLocal中的conn来进行事务的开启和回滚,由此我们知道了在同一线程中Spring事务中的Connection和Mybaits中Mapper代理对象中操作数据库的Connection是同一个,当取出来的conn为空时候,调用org.springframework.jdbc.datasource.DataSourceUtils#fetchConnection获取,然后把从数据源取出来的连接返回 private static Connection fetchConnection(DataSource dataSource) throws SQLException { //从数据源取出来conn Connection con = dataSource.getConnection(); if (con == null) { throw new IllegalStateException("DataSource returned null from getConnection(): " + dataSource); } return con; } ~~~ ## mybatis-plus 介绍 * MyBatis 是一款优秀的持久层框架,其目的是想当做互联网的篱笆墙,围绕着数据库提供持久化服务的一个框架,支持自定义 SQL、存储过程及高级映射。 * MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作,还可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Ordinary Java Object,普通 Java 对象)为数据库中的记录。 * [MyBatis-Plus](https://github.com/baomidou/mybatis-plus)(简称 MP)是一个[MyBatis](http://www.mybatis.org/mybatis-3/)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。 ### 使用mybatis编写Dao ![](https://img.kancloud.cn/50/2c/502c2953cf4547194c91a530b38cd903_1850x738.png) ## db-spring-boot-starter自动装配原理解析 咱们想想,在不同项目中,咱们的项目是如何装配这些对象的吗?下面咱们需要揭密。 * db-spring-boot-starter 中定义了spring.factories文件 ![](https://img.kancloud.cn/7b/65/7b65c4f503da46d6cf96d56d516f9684_1554x443.png) * DataSourceAutoConfig 中@Import(DataSourceAOP.class) ![](https://img.kancloud.cn/c4/88/c48802a1d13011b1baf97f7c7b698fb7_1055x298.png) 那么这些文件是如何完成加载到spring容器的呢? 此时,咱们必须回到user-center,阅读源码 * @SpringBootApplication ![](https://img.kancloud.cn/9b/cf/9bcf14c2f60672903bd0f633c4f868a2_1545x621.png) * @EnableAutoConfiguration ![](https://img.kancloud.cn/3a/27/3a2745529f91f89fccc11b06c74879b4_1088x677.png) * AutoConfigurationImportSelector ![](https://img.kancloud.cn/08/b7/08b727562ad33adfd71fdf4bed7810f8_1007x485.png) 阅读到这里,我们了解到,user-center在启动时,由于@SpringBootApplication是复合注解,包含@EnableAutoConfiguration,这个类中@import了核心处理类AutoConfigurationImportSelector,这个类的核心就是将classpath中搜索所有META-INF/spring.factories配置文件,并且将其中org.springframework.boot.autoconfigure.EnableAutoConfiguration key对应的配置项加载到spring容器 ## SPI机制的使用 ``` SPI的全名为Service Provider Interface,简单的总结下java spi机制的思想。我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块的方案,xml解析模块、jdbc模块的方案等。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 java spi就是提供这样的一个机制:为某个接口寻找服务实现的机制 ``` ### JDK SPI 在 JDBC 中的应用 JDK 中只定义了一个 java.sql.Driver 接口,具体的实现是由不同数据库厂商来提供的。这里以 MySQL 提供的 JDBC 实现包为例进行分析。 在 mysql-connector-java-*.jar 包中的 META-INF/services 目录下,有一个 java.sql.Driver 文件中只有一行内容,如下所示: ``` com.mysql.cj.jdbc.Driver ``` 在使用 mysql-connector-java-*.jar 包连接 MySQL 数据库的时候,我们会用到如下语句创建数据库连接: ``` String url = "jdbc:mysql://59.110.164.254:3306/user-center?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false "; Connection conn = DriverManager.getConnection(url, username, pwd); ``` DriverManager 是 JDK 提供的数据库驱动管理器,其中的代码片段,如下所示: ``` static {     loadInitialDrivers();     println("JDBC DriverManager initialized"); } ``` 在调用 getConnection() 方法的时候,DriverManager 类会被 Java 虚拟机加载、解析并触发 static 代码块的执行,在 loadInitialDrivers() 方法中通过 JDK SPI 扫描 Classpath 下  java.sql.Driver 接口实现类并实例化,核心实现如下所示: ``` private static void loadInitialDrivers() {     String drivers = System.getProperty("jdbc.drivers")     // 使用 JDK SPI机制加载所有 java.sql.Driver实现类     ServiceLoader<Driver> loadedDrivers =             ServiceLoader.load(Driver.class);     Iterator<Driver> driversIterator = loadedDrivers.iterator();     while(driversIterator.hasNext()) {         driversIterator.next();     }     String[] driversList = drivers.split(":");     for (String aDriver : driversList) { // 初始化Driver实现类         Class.forName(aDriver, true,             ClassLoader.getSystemClassLoader());     } } ``` 在 MySQL 提供的 com.mysql.cj.jdbc.Driver 实现类中,同样有一段 static 静态代码块,这段代码会创建一个 com.mysql.cj.jdbc.Driver 对象并注册到 DriverManager.registeredDrivers 集合中( CopyOnWriteArrayList 类型),如下所示: ``` static {    java.sql.DriverManager.registerDriver(new Driver()); } ``` 在 getConnection() 方法中,DriverManager 从该 registeredDrivers 集合中获取对应的 Driver 对象创建 Connection,核心实现如下所示: ``` private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {     // 省略 try/catch代码块以及权限处理逻辑     for(DriverInfo aDriver : registeredDrivers) {         Connection con = aDriver.driver.connect(url, info);         return con;     } } ``` ### SpringBoot中的类SPI扩展机制 在springboot的自动装配过程中,最终会加载META-INF/spring.factories文件,而加载的过程是由SpringFactoriesLoader加载的。从CLASSPATH下的每个Jar包中搜寻所有META-INF/spring.factories配置文件,然后将解析properties文件,找到指定名称的配置后返回。需要注意的是,其实这里不仅仅是会去ClassPath路径下查找,会扫描所有路径下的Jar包,只不过这个文件只会在Classpath下的jar包中。 ## Guava Guava 还提供了很多实用工具,如 Lists、Maps、Sets,接下来我们分别来看下这些常用工具的使用和原理。 * List<泛型> list = Lists.newArrayList(); * Map<String,String> hashMap = Maps.newHashMap(); 这种写法其实就是一种简单的工厂模式 ``` // 可以预估 list 的大小为 20 List<String> list = Lists.newArrayListWithCapacity(20); List<String> list = Lists.newArrayListWithExpectedSize(20); Map<String,String> hashMap = Maps.newHashMap(); Map<String,String> linkedHashMap = Maps.newLinkedHashMap(); Map<String,String> withExpectedSizeHashMap = Maps.newHashMapWithExpectedSize(20); ``` Guava 还提供了提供了一些异常处理的静态方法 ``` Throwables.throwIfUnchecked(new RuntimeException("模拟业务出错")); ``` ## 总结回顾 db-spring-boot-starter构建原理 * 1.ImportSelector 该接口的方法的返回值都会被纳入到spring容器管理中 * 2.SpringFactoriesLoader 该类可以从classpath中搜索所有META-INF/spring.factories配置文件,并读取配置 db-spring-boot-starter如何使用 * 1.使用mybatis构建dao文件 * 2.配置数据源 * 3.配置xml路径