我看了这本书的结构, 先用了全书八分之三的篇幅来讲解IOC+AOP这两个框架的核心内容, 中间穿插了比如Spring EL之类的小知识点, 然后又花了四分之一的篇幅来讲解DAO, 之后又会穿插一个Spring的异步任务处理.

上边的准备工作都结束了之后, 才进入SpringMVC, 回想一下自己原来看Spring入门视频, 上来就用MVC写一个增删改查, 略过了多少重要的内容.

现在就来仔细的看一下DAO吧, 数据库这块以及Java 的ORM一直是一个弱项, 因为平时用的不多, 这次就来好好看一下.

  1. Spring的DAO
  2. DAO异常体系
  3. 统一的数据访问模板
  4. 数据源对象
  5. 通用流程
  6. 数据库事务理论
  7. ThraedLocal类

Spring的DAO

DAO 就是 数据存取对象 Data Access Object的简称, 是面向对象语言用于屏蔽底层具体数据库操作的抽象概念.

Java的DAO经典的实现有JDBC, 此外还有完全ORM的Hibernate, MyBatis等框架, DAO的核心是定义好数据对象和存取对象的接口, 然后就可以平滑的在各个技术之间切换.

Spring 对于 DAO技术的支持在于想统一的方式整合底层的不同技术, 所以首先就要提供统一的异常体系, 然后还需要提供统一的事务管理体系. 在这两个体系的管理之下, 将具体数据库的操作交给具体框架.

DAO异常体系

Spring 将原本的检查型异常基本上都改成了运行时异常, 这就让检查异常泛滥的情况得到了改变.

Spring的DAO支持都在 org.springframework.dao包中. 所有异常都继承于DataAccessException, DataAccessException继承NestedRuntimeException.

NestedRuntimeException中封装了原来的异常, 可以通过其方法查看源异常, 这样所有的异常就都被纳入到Spring DAO的体系中.

异常类有很多, 用到的时候可以查看异常类对应的内容, 以try catch 这些具体的运行时异常.

Spring 还很牛逼的针对不同的持久化技术编写了异常转换器. 支持Hibernate 3+, JPA和JDO等, 未来主要就是用Hibernate了.

统一的数据访问模板

为了简化开发, Spring提供了不同持久化技术的模板:

  1. org.springframework.jdbc.core.JdbcTemplate, 对应JDBC
  2. org.springframework.orm.hibernateX.HibernateTemplate, 对应Hibernate
  3. org.springframework.orm.jap.JpaTemplate, 对应JPA
  4. org.springframework.orm.jdo.JdoTemplate, 对应JDO

从模板名字可以发现, 除了JDBC之外的剩下三个, 都是ORM模型.

这些模板类如果要使用的话, 需要在Spring中定义一个数据源对象, 然后创建一个模板对象, 不过Spring已经在org.springframework.dao.support中编写好了这些模板的支持类.

在创建这些模板类对象的时候, 模板类会在其中的afterPropertiesSet()方法中检查是否已经有数据源对象. 所以就来看一下数据源对象.

数据源对象

数据源的规定不是Spring规定的, 而是JDBC的标准, 也就是DataSource对象. 在模板类初始化的时候, 会到容器里寻找是不是有DataSource的实现类, 如果没有就会报错.

所以需要配置一个数据源, Spring使用的数据源一般是Apache的DBCP, 或者是C3PO数据源, 需要在XML文件中使用SQL连接来配置成一个Bean:

<bean class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close" p:defaultAutoCommit="true"
      p:driverClassName="com.mysql.cj.jdbc.Driver"
      p:url="jdbc:mysql://localhost:3306/sia5"
      p:username="root"
      p:password="****"
/>

这样就配置好一个DataSource, 之后才能实例化模板对象. C3PO也是类似, 有很多配置, 需要的时候可以具体查看书里.

Spring自己也提供了一个DriveManagerDataSource类, 也实现了DataSource接口. 不过没有采用连接池机制, 每次创建新连接, 可以简单测试的时候使用.

通用流程

上边已经提了, 不过这里还是再总结一下, 在Spring中使用数据库的方法

  1. 需要有数据源对象, 实现DataSource接口, 然后将其设置为容器中的一个Bean
  2. 在有了数据源对象的基础上, 去创建对应的模板支持类的对象, 也需要设置为容器中的一个Bean, 这个DaoSupport类型会在创建的时候自动去容器中寻找DataSource, 如果配置不正确, 就会报错
  3. 使用创建的DaoSupport对象操作数据库

一个简单的例子如下(还没用上模板, 用的原生JDBC), 配置好XML文件:

<?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:util="http://www.springframework.org/schema/util"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">

    <util:properties id="properties" location="config.properties"/>
    <context:property-placeholder properties-ref="properties"/>
    <context:component-scan base-package="cc.conyli" />

    <bean class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close" p:defaultAutoCommit="true" id="source"
          p:driverClassName="com.mysql.cj.jdbc.Driver"
          p:url="jdbc:mysql://localhost:3306/sia5"
          p:username="root"
          p:password="*******"
    />
</beans>

然后就可以操作数据库了:

import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class Main {

    public static void main(String[] args) throws SQLException {
        //创建容器
        Resource res = new FileSystemResource("D:\\Coding\\Java\\practice\\src\\main\\java\\spconfig.xml");
        DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
        reader.loadBeanDefinitions(res);

        //从容器中获取组装好的DataSource对象
        DataSource source = (DataSource) factory.getBean("source");
        System.out.println(source);
        Connection connection = source.getConnection();
        Statement statement = connection.createStatement();
        String sql = "SELECT * FROM sia5.course";
        ResultSet rs = statement.executeQuery(sql);
        while (rs.next()) {
            System.out.println(rs.getString(2));
        }
    }
}

数据库事务理论

对于普通开发来说, 数据库理论确实是一个弱项, 这次在开始事务学习之前, 先要了解一下基本的数据库理论.

一个数据库的事务, 有如下ACID要求:

  1. Atomic 原子性, 多条数据操作是不可分割的整体, 要么全部成功, 要么全部失败
  2. Consistency 一致性, 数据库最终的状态和其要达到的业务规则是一样的
  3. Isolation 隔离性, 并发的时候不同的事务有不同的数据空间, 隔离性做的越好, 越不能并发(等于线性操作了). 实际中有不同的隔离级别
  4. Durability 持久性, 事务提交成功后, 就要实际的写入数据库, 不能发生事务提交成功结果还没有写入.

这中间一致性是最终目标, 其他目的都是为了完成一致性采取的手段.

数据并发有五种问题:

  1. 脏读 dirty read, 指在事务B提交事务的过程中, 事务A读了数据, 然后事务B回滚了, 结果事务A读到的数据就是脏数据, 没有任何用处
  2. 不可重复读 unrepeatable read, 指事务A两次读取之间, 事务B提交修改的数据, 结果造成事务A两次读取不一致, 由于这个是修改问题, 所以可以对行级数据加锁, 不管哪个事务用到这个数据, 都加锁直到结束修改或者读取, 这中间不能被其他程序读取和修改.
  3. 幻象读, 也叫幻读 phantom read, 指事务A两次读取之间, 事务B新建了数据, 在事务A的查询范围之内, 结果造成事务A两次读取不一致. 这就要锁住表, 不允许新增和删除内容.
  4. 第一类丢失更新, 即A事务在撤销的时候, 把B事务正常提交的数据给覆盖了, 造成B事务的更新丢失
  5. 第二类丢失更新, A事务覆盖B事务已经提交的数据, 导致B事务的更新丢失, 这两类丢失更新主要在于一个是撤销导致另外一个事务丢失, 一个是正常操作导致丢失.

解决并发的问题, 肯定要提到锁. 数据库的锁针对锁住的对象不同, 有锁住表和锁住行之分. 根据是否允许并发, 分为共享锁定和独占锁定. 共享锁定会防止独占锁定, 但会开放给其他的共享锁定. 而独占锁定会防止共享和其他的独占锁定.

一般为了更改数据, 数据库都会在要更改的行上加上行独占锁定, INSERT, UPDATE, DELETE 和SELECT FOR UPDATE在内部都会使用行独占锁定.

书上下边的几种共享锁定看的有点懵, 这个需要专门找东西补一下, 初看的感觉就是这并不是四种, 而是还有交叉.

直接使用锁很麻烦, ANSI/ISO SQL 92 规定了4个等级的事务隔离, 只要确认了事务隔离级别, 数据库就会分析SQL语句并且自动加锁.

只要记住, 第一类丢失更新都是不允许的. 最低的隔离层次 READ UNCOMMITED 允许剩下四个问题. 第二层READ COMMITED在第一层的基础上不允许脏读.

第三层REPEATABLE READ 从名字就可以看出来, 在第二层基础上不允许不可重复读, 也不允许第二类丢失更新.

最高级别 SERIALIZABLE 从名字就可以看出来, 完全隔离成了序列操作, 所以不允许全部的并发问题出现.

并非所有的数据库都支持全部的隔离级别以及事务, 可以通过连接对象查看, 试着操作一下:

public static void main(String[] args) throws SQLException {
    Resource res = new FileSystemResource("D:\\Coding\\Java\\practice\\src\\main\\java\\spconfig.xml");
    DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
    XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
    reader.loadBeanDefinitions(res);
    DataSource source = (DataSource) factory.getBean("source");
    System.out.println(source);
    Connection connection = source.getConnection();

    //是否支持事务
    DatabaseMetaData metaData = connection.getMetaData();
    System.out.println(metaData.supportsTransactions());
    //四种隔离级别是否支持
    System.out.println(metaData.supportsTransactionIsolationLevel(Connection.TRANSACTION_READ_UNCOMMITTED));
    System.out.println(metaData.supportsTransactionIsolationLevel(Connection.TRANSACTION_READ_COMMITTED));
    System.out.println(metaData.supportsTransactionIsolationLevel(Connection.TRANSACTION_REPEATABLE_READ));
    System.out.println(metaData.supportsTransactionIsolationLevel(Connection.TRANSACTION_SERIALIZABLE));

    //设置事务级别
    connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
}

JDBC对事务的支持在以前只能有两个操作, 提交和回滚, 在JDBC 3.0=Java 1.4之后, 还添加了一个保存点SavePoint, 可以回滚到特定保存点, 然后再提交.

ThraedLocal类

ThreadLocal可以将一个本来多线程不安全的方法改成安全的, 只要将方法所需要的变量包裹在一个ThreadLocal类中即可. 这样不同的线程执行同一个方法, 每个变量都是自己对应的.

当然, 如果保存进去的是同一个引用, 那还是会有问题. 所以一般都保存新创建的变量或者是具体的值.

ThreadLocal支持泛型, 方法如下:

  1. void set(T value), 设置当前线程的局部变量值
  2. public T get(), 获取当前线程对应的局部变量值
  3. public void remove(), 删除当前线程对应的局部变量值. 注意即使不删除, 在线程执行完毕之后, 操作系统也会释放掉线程的资源
  4. protected T initialValue(), 返回默认值, 这个方法是为了覆盖而存在的. 一般在第一次调用get()和set()的时候才执行, 而且仅执行一次, 如果想生成一个特定的初始值, 就用这个方法.

有一个简单的例子, 我看了一下, 其核心思想就是, 如果一个类中有一个需要进行多线程操作的方法, 那么就在类中设置一个静态变量为ThreadLocal对象, 每次要操作数据的时候, 都使用对象的set和get方法即可.

package cc.conyli.threads;

public class SequeceNumber {

    private static ThreadLocal<Integer> seqNum = ThreadLocal.withInitial(() -> 0);

    public int getNextNum() {
        seqNum.set(seqNum.get() + 1);
        return seqNum.get();
    }

    public static void main(String[] args) {

        SequeceNumber seqNum = new SequeceNumber();

        TestClient testClient1  = new TestClient(seqNum);
        TestClient testClient2  = new TestClient(seqNum);
        TestClient testClient3  = new TestClient(seqNum);
        TestClient testClient4  = new TestClient(seqNum);

        testClient1.start();
        testClient2.start();
        testClient3.start();
        testClient4.start();
    }

    private static class TestClient extends Thread {

        private SequeceNumber sequeceNumber;

        public TestClient(SequeceNumber sequeceNumber) {

            this.sequeceNumber = sequeceNumber;
        }

        public void run() {
            for (int i = 0; i < 3; i++) {
                System.out.println("thread[" + Thread.currentThread().getName() + "] sn[" + sequeceNumber.getNextNum() + "]");
            }
        }
    }
}

使用这个方法来改造一个线程不安全的方法的时候, 只要将使用到的变量放入到ThreadLocal对象中, 然后在传参数和返回结果的时候, 都使用ThreadLocal变量, 这样就可以了, 如果方法内部也使用了共享变量, 也如此改造即可.