《Spring揭秘-下》

前言

此篇博客主要针对的是《Spring揭秘》的下半部分,对应了spring的数据访问部分(包括事务)和Spring MVC的部分。

第十三章

程序无法脱离数据存在。我们的程序中或多或少都会和数据库、文件或者别的系统进行交互。为了屏蔽这些不同系统之间的差异,我们抽象出了一层叫DAO(data access object)的类(接口),我们只需要调用这些类中的方法,就可以进行数据的访问和存储,而不需要在乎实现细节。

理想很美好,但是现实却没那么简单。想一想,如果你的数据库访问出现了问题,需要处理这个异常,那么应该由谁来处理?DAO吗?那客户端就无从得知它的操作有没有成功了呀。那抛给客户端?那客户端就需要根据不同的底层系统来处理不同的异常,这样岂不是又回到了最初的起点?

我们其实需要的就只是一套异常层次的体系,有了这套体系,客户端可以愉快的处理它了。好在spring已经为我们抽象出了这么一套体系:以DataAccessException这个抽象类为统领的一组类。

第十四章

Spring为我们提供了两种JDBC的最佳实践,虽然现在都是用的ORM框架如MyBatis等,但是我觉得理解下还是有必要的。

JDBC是成功的吗?当然,现在几乎只要你通过java访问数据库,必然需要通过JDBC。但是我作为一个使用者,用起来爽吗?不不不,就算一个最最简单的select,我也需要写一大堆的代码,就算有snippet功能,还是觉得烦。其次是所有的异常都是SQLException,而且我们还需要自己去分析这个exception。

在这种情况下,JdbcTemplate诞生了。spring就是以JdbcTemplate作为基石来构建的。在这里首先先需要对设计模式中的模板方法进行一波简单的介绍。其实就是在一个类中用一个final修饰一个方法,然后这个方法是所有子类都需要的,子类只需要把不同的方法改一下就好了。

下面是我抽象的一个模板类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public abstract class Template {
public final void doSomething(){
step01();
step02();
step03();
step04();
}

private void step01(){
System.out.println("步骤1");
}

protected abstract void step02();

private void step03(){
System.out.println("步骤3");
}

private void step04(){
System.out.println("步骤4");
}
}

可以看到doSomething被声明成了final,就是不想子类修改,因为这是所有子类必须遵守的逻辑部分。但是又特别把step02设置成了抽象方法,这样每个子类可以根据自己的需要修改这一个步骤:

1
2
3
4
5
6
public class Impl extends Template{
@Override
protected void step02() {
System.out.println("步骤02");
}
}

这个设计模式对于JDBC访问数据库真的是特别适合,因为访问数据库就那么几步,而且就是照着规矩来的。

但是似乎还有那么一丝丝不方便,就是模板的类是抽象的,我们必须要实现一个子类去实现它,稍微有点麻烦(当然比起没有模板的时候方便了太多了)。所以spring在实现的时候,还加入了Callback接口。现在,如果我们希望访问数据库,可以这样访问了:

1
2
3
4
JdbcTemplate jdbcTemplate = new JdbcTemplate();
final String sql = "select * from xxx;";
StatementCallback callback = statement -> statement.execute(sql);
jdbcTemplate.execute(callback);

当然由于没有配置DataSource,所以显然是无法访问数据库的。下面是JdbcTemplate的详细的继承层次,还算挺简单的:

image-20200806001704498

继承了JdbcAccessor,并且实现了JdbcOperations。

  • JdbcAccessor里面就有一个DataSource的对象,spring对数据库访问全部建立在DataSource之上;还有一个SQLExceptionTranslator的对象,这个就是用来解决之前说的SQLException设计不良,导致乱象丛生的问题的。
  • JdbcOperations则规定了所有的操作。

同时,在JdbcTemplate里面,还有一些面向不同API来分成了几组方法,分别是面向Connectioni,Statement,PreparedStatement和CallableStatement。我们看一个面向Statement的方法好了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Nullable
public <T> T execute(StatementCallback<T> action) throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");
Connection con = DataSourceUtils.getConnection(this.obtainDataSource());
Statement stmt = null;

Object var11;
try {
stmt = con.createStatement();
this.applyStatementSettings(stmt);
T result = action.doInStatement(stmt);
this.handleWarnings(stmt);
var11 = result;
} catch (SQLException var9) {
String sql = getSql(action);
JdbcUtils.closeStatement(stmt);
stmt = null;
DataSourceUtils.releaseConnection(con, this.getDataSource());
con = null;
throw this.translateException("StatementCallback", sql, var9);
} finally {
JdbcUtils.closeStatement(stmt);
DataSourceUtils.releaseConnection(con, this.getDataSource());
}

return var11;
}

可以看到奥秘就在第11行,通过传入的action来做操作,并且返回这个结果。同时如果你仔细观看,会发现第4行也有点奇怪,为什么不是直接从dataSource直接获取连接,而是需要大费周章从DataSourceUtils这个类来获取呢?这个主要是为了将conn绑定到当前线程,然后在事务管理的时候发挥作用。

之前还聊到了我们需要把复杂的SQLException转译成DataAccessException,这个是由SQLExceptionTranslator来做的,具体的实现类有三个,逻辑首先是通过查询自己有没有sql-error-codes.xml文件,如果有就根据该文件来提取,如果当前的类路径下也有这个文件,那么会覆盖掉spring包下面的。所以我们可以在类路径下放置这个文件,来告知如何根据错误代码来转换成DataAccessException。当然实际中我是没有遇到过这种需求。

关于JdbcTemplate对数据进行查询、更新(包括插入和删除),我个人认为不是重点,就直接跳过了。

DataSource

其实DataSource更为通用的名字应该叫ConnectionFactory,因为我们的conn就是根据它来获取的。Spring中的DataSource主要有两种,一种是DriverManagerDataSource,另外一种是SingleConnectionDataSource。听名字也知道了,每次向single去请求conn的时候,返回的都是同一个对象。但是由于这两者都没有缓冲池,所以生产环境中请不要使用。

至于带有缓冲池功能的datasource嘛,现在一堆开源的都是。

基于对象的操作

这边先直接跳过了。我个人觉得现在都是用的ORM框架来进行操作,所以JDBC的最佳实践我了解一种JdbcTemplate就足够了。

第十五章

和之前的问题一样,每个ORM都有它们自己的异常,为了屏蔽这些,spring需要提供一个异常转译机制。同理,spring还需要提供统一的资源管理方式和事务管理等。

iBatis

虽然现在已经是MyBatis了,但是作为前身,我觉得理解一下也是好的。基本上所有的ORM框架都至少需要两个配置文件,一个是全局配置文件,指定好数据源等相关配置;另外一个(多个)用来指定表和对象之间的映射关系。

第十六章

不管是Spring,还是其他的ORM框架,它们都不约而同使用了模板的方式来处理,相关的好处相信你已经了解了。在学了JdbcTemplate之后,相信你也可以根据日常中重复的逻辑,来生成属于自己的template了。

第十七章

事务中的成员

  • Resource Manager:负责存储并管理系统数据资源。如MySQL数据库服务器就是典型的RM。
  • Transaction Processing Moniotr:在分布式事务中协调多个RM进行事务处理。
  • Transaction Manager:是上面的TPM的核心模块,提供多种功能模块。
  • Application:就是应用程序呗。

全局事务(也叫做分布式事务)就是有多个Application和多个RM,然后它们之间通过一个TPM(TM)来协调。TM通过两阶段提交来保证事务的ACID。

局部事务就是一个Application和一个RM,由于RM本身就自带了事务支持,所以其实直接和RM打交道就行了。

第十八章

局部事务支持

编写过JDBC代码的同学一定非常熟悉,只需要设置conn.setAutoCommit(false);就可以自己手动提交事务,然后进行相应的回滚处理了。

分布式事务支持

书中介绍的分布式事务的支持都太老了,这里直接略过。

第十九章

spring通过分析之前的缺点,并对事务管理进行了抽象,得到了Spring事务框架,核心原则就是:事务管理和数据访问相互分离。

我们一般会将事务的管理放在service层,而将数据访问放在dao层。所以可能会出现这种情况:两个dao需要在同一个service中,而且它们的conn必须是同一个,那么我们只需要传递这个conn就可以了,也不是很麻烦。

具体实现的话,可以把conn绑定到当前的线程,这样大家都可以获取这个conn了。而TransactionResourceManager就是封装了这个逻辑,我们实际中只需要向它“索要”conn就行了。

TRM的代码见下:

1
2
3
4
5
6
7
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;

void commit(TransactionStatus var1) throws TransactionException;

void rollback(TransactionStatus var1) throws TransactionException;
}

可以看到相关的还有TransactionDefinition和TransactionStatus这两个接口。TD根据名字可以判断出就是用来定义事务的相关属性的,包括隔离级别、传播行为等。而TS则是用来表示事务的状态的。

TD

这个接口主要包括了:

  • 事务的隔离级别:
1
2
3
4
5
int ISOLATION_DEFAULT = -1;			// 使用数据库默认的
int ISOLATION_READ_UNCOMMITTED = 1;
int ISOLATION_READ_COMMITTED = 2;
int ISOLATION_REPEATABLE_READ = 4;
int ISOLATION_SERIALIZABLE = 8;
  • 事务的传播行为:
1
2
3
4
5
6
7
int PROPAGATION_REQUIRED = 0;
int PROPAGATION_SUPPORTS = 1;
int PROPAGATION_MANDATORY = 2;
int PROPAGATION_REQUIRES_NEW = 3;
int PROPAGATION_NOT_SUPPORTED = 4;
int PROPAGATION_NEVER = 5;
int PROPAGATION_NESTED = 6;
  • 事务的超时时间:int TIMEOUT_DEFAULT = -1;,-1代表使用系统默认的超时时间。
  • 事务是否是只读的:default boolean isReadOnly() {return false;}

上面可能会对事务的传播行为有点陌生。事务的传播行为指的是,整个事务处理过程中,跨越的业务对象,会以什么样的行为来参与事务。

  • REQUIRED:当前存在事务则加入,没有事务则创建一个事务。这个是默认的。
  • SUPPORTS:当前存在事务则加入,没有事务则直接执行。对于一些查询的方法,这个是比较推荐的。
  • MANDATORY:必须要有一个事务,否则就抛出异常。
  • REQUIRES_NEW:不管是否存在事务,都会创建事务。如果之前的事务是存在的,则挂起它。比如我向数据库更新一些不重要的信息,就算这些信息更新失败了,原来的事务还是要不受影响。
  • NOT_SUPPORTED:必须没有事务才能执行。
  • NEVER:永远都不能有事务,有的话就抛出异常。
  • NESTED:如果存在事务,那么就在事务中的嵌套事务中执行;否则创建事务执行。

TS

  • 查询相关的事务状态
  • 标记事务以便进行回滚
  • 创建内部嵌套事务

第二十章

编程式事务管理

我们可以直接使用PlatformTransactionManager来进行代码的编写。只要自己实现一个TD和一个TS,就可以完成整个事务处理流程了。而且这套流程是比较固定的,所以我们可以使用模板的设计模式来优化它,Spring则是抽象出了TransactionTemplate来进行。

1
2
3
4
5
6
7
8
TransactionTemplate tx = new TransactionTemplate();
Object execute = tx.execute(new TransactionCallback<Object>() {
@Override
public Object doInTransaction(TransactionStatus transactionStatus) {
// 各种事务操作
return null;
}
});

声明式事务管理

spring aop可以在这里很好用上。具体实现可以有xml的和注解的方式。使用xml的话可以有以下四种:

  • ProxyFactory+TransactionInterceptor
  • 直接使用TransactionProxyFactoryBean
  • 使用BeanNameAutoProxyCreator
  • 使用spring2.x的声明事务配置

而目前一般都是直接使用注解来进行开发的。

第二十一章

之前提到过用threadlocal来保存conn信息,这样可以在一个线程的各个方法之间进行有效的传递。

从本质上说,threadlocal和synchronized是一点关系也没有,但是它们都能够有效解决线程安全问题,一个是通过变量的私有化,另外一个是通过同步来实现的。

第二十二章

一些MVC框架的发展史,有兴趣的可以看看。

第二十三章

Spring MVC是一个请求驱动的Web框架,其中有一个DispatcherServlet作为Font Controller,它负责接收所有的web请求,然后根据处理逻辑来分发给下一级控制器,也就是我们常说的Controller(更加具体的应该说的是Page Controller)。

我们其实分析一下日常写的一些业务逻辑,不难发现一些共同的特点:

  1. 获取请求的参数
  2. 根据请求的参数来进行相应的处理
  3. 处理完成之后,把数据交给jsp视图

其实spring也算是源于业务了,看看它是怎么做的。首先容器中必然是有一个DispatcherServlet的,这个毋庸置疑。然后又有一个HandlerMapping,来处理URL的匹配。在查找到了之后,会首先发回给DispatcherServlet,然后由它来发送给下一级的controller。controller处理完之后,就会返回一个ModelAndView对象,里面封装了对应的数据和视图的逻辑名称。DispatcherServlet接下来就会依赖ViewResolver来查找对应的View实现类,然后把找到的实现类交给DispatcherServlet。最后DispatcherServlet会把数据封装到View里面,并且发还给用户。