spring-jdbc 的实现原理

前言

本篇文章将回答以下几个问题

  1. spring-jdbc 的出现是为了解决什么问题
  2. spring-jdbc 如何解决的这些问题
  3. 它的这种技术有何缺陷

首先希望你能带着这些问题来看这篇文章,也希望这篇文章能让你很好的解答这些问题。当然,这篇文章的终极目标是希望你能够借鉴spring-jdbc 的思想来解决我们在工作过程中所面临的问题。
如果你想了解,如何使用spring-jdbc,请绕道......

Dao 模式

为了实现数据和业务的分离,有人提出了Dao模式。Dao模式是数据处理的一种理想模式,(我认为)它带来了两个方面的好处:1、屏蔽数据访问的差异性;2、业务与数据分离。spring-jdbc 在本质上是一种Dao模式的具体实现。(Dao模式的详细介绍
接下下我们用一个简单的例子(未具体实现)来简单介绍一下Dao模式(如下图所示)


从上面的UML图可以知道:

  • 首先定义了一个User的操作接口UserDao,它定义了了获取用户信息、添加用户信息、更改用户信息的行为;
  • 具体行为的由其实现类来实现,我们这里举了两个例子:Batis 实现和Jdbc实现(当然也可以缓存实现或file实现等),它实现具体获取或修改数据的行为;UserDaoFactory 生成具体的实现UserDao实现类(请参考下面代码)。
  • 所以当我们在Service层(UserService)访问数据时,只 需要使用UserDaoFactory 生成一个具体的UserDao实现类就可以了,这样业务层就可以完全操作数据操作的具体实现( 参考下面UserService的具体实现)
public class User {
    private int id;
    private String name;
    private String email;
    private String phone;
 
    public User() {
    }
 
    public int getId() {
        return id;
    }
 
    public String getName() {
        return name;
    }
 
    public String getEmail() {
        return email;
    }
 
    public String getPhone() {
        return phone;
    }
 
    public void setId(int id) {
        this.id = id;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public void setEmail(String email) {
        this.email = email;
    }
 
    public void setPhone(String phone) {
        this.phone = phone;
    }
}
public interface UserDaoInterface {
    public User getUserInfoByName(String name);
    public void putUserInfo(User user);
    public void updateUserInfo(User user);
}
public class UserDaoJdbcAccessImpl implements UserDaoInterface {
    // Jdbc连接数据库等操作,未完成具体实现
    private DataSource dataSource;
    public User getUserInfoByName(String name) {
        dataSource.getC
 
        return new User();
    }
    public void putUserInfo(User user) {
 
    }
    public void updateUserInfo(User user) {
 
    }
}
public class UserDaoBatisAccessImpl implements UserDaoInterface {
    // Batis连接数据库等操作,未完成具体实现
    public User getUserInfoByName(String name) {
        return new User();
    }
    public void putUserInfo(User user) {
 
    }
    public void updateUserInfo(User user) {
 
    }
}
public class UserDaoFacotry {
 
    public static UserDaoInterface getUserDao(int which) {
        switch(which) {
            case 1:
                return new UserDaoJdbcAccessImpl();
            case 2:
                return new UserDaoBatisAccessImpl();
            default:
                return null;
        }
    }
}
public class UserService {
 
    public UserDaoInterface getUserDaoOperation() {
        return UserDaoFacotry.getUserDao(1);
    }
 
    public void getUserInfo() {
 
        User user = this.getUserDaoOperation().getUserInfoByName("xiaoming");
    }
}

但在具体实现DaoImpl时遇到了一个问题,数据库的连接访问会抛出异常,且属于checked exception

public User getUserInfoByName(String name) {
   try {
       Connection connection = dataSource.getConnection();
       User user = ....
       return user;
   } catch (SQLException e) {
        
   } finally {
       connection.close();
   }
}

这是很尴尬的,因为此时我们不知道是要抛给上层业务还是catch之后进行处理。catch之后进行处理,由于屏蔽异常会让客户端难以排查问题,如果直接抛出去也带来更严重的问题(必须更改接口且不同数据库所抛出的异常不一样),如下所示


public User getUserInfoByName(String name) throw SQLException, NamingException ... {
   try {
       Connection connection = dataSource.getConnection();
       User user = ....
       return user;
   } finally {
       connection.close();
   }
}

jdbc 为了解决不同数据库带来的异常差异化,则对异常进行统一转换,并抛出unchecked异常。具体抛出的异常可以在org.springframework.dao中查看

这是很尴尬的,因为此时我们不知道是要抛给上层业务还是catch之后进行处理。catch之后进行处理,由于屏蔽异常会让客户端难以排查问题,如果直接抛出去也带来更严重的问题(必须更改接口且不同数据库所抛出的异常不一样),如下所示

具体异常所代表的含义:
Spring的DAO异常层次

| 异常 | 何时抛出 |
| :-------- :|: --------:|
| CleanupFailureDataAccessException |一项操作成功地执行,但在释放数据库资源时发生异常(例如,关闭一个Connection |
| DataAccessResourceFailureException |数据访问资源彻底失败,例如不能连接数据库 |
| iMac | 10000 元 |
|DataIntegrityViolationException| Insert或Update数据时违反了完整性,例如违反了惟一性限制|
|DataRetrievalFailureException |某些数据不能被检测到,例如不能通过关键字找到一条记录|
|DeadlockLoserDataAccessException| 当前的操作因为死锁而失败|
|IncorrectUpdateSemanticsDataAccessException| Update时发生某些没有预料到的情况,例如更改超过预期的记录数。当这个异常被抛出时,执行着的事务不会被回滚|
|InvalidDataAccessApiusageException 一个数据访问的JAVA| API没有正确使用,例如必须在执行前编译好的查询编译失败了|
|invalidDataAccessResourceUsageException |错误使用数据访问资源,例如用错误的SQL语法访问关系型数据库|
|OptimisticLockingFailureException |乐观锁的失败。这将由ORM工具或用户的DAO实现抛出|
|TypemismatchDataAccessException| Java类型和数据类型不匹配,例如试图把String类型插入到数据库的数值型字段中|
|UncategorizedDataAccessException| 有错误发生,但无法归类到某一更为具体的异常中|

spring-jdbc

我们可以将spring-jdbc 看作Dao 模式的一个最佳实践,它只是使用了template模式,实现了最大化的封装,以减少用户使用的复杂性。spring-jdbc 提供了两种模式的封装,一种是Template,一种是操作对象的模式。操作对象的模式只是提供了面向对象的视觉(template 更像面向过程),其底层的实现仍然是采用Template。
接下来我们将会了解Template 的封装过程。

2.1 Template

还是延用上述例子,如果这里我们需要根据用户名查询用户的完整信息,将采用下面的方式实现查询

public class UserDaoJdbcAccessImpl implements UserDaoInterface {
    // Jdbc连接数据库等操作,未完成具体实现
    private DataSource dataSource;
    public User getUserInfoByName(String name) {
        String sql = "....." + name;
        Connection connection = null;
        try {
            connection = DataSourceUtils.getConnection(dataSource);
            Statement statement = connection.createStatement();
            ResultSet resultSet = statement.executeQuery(sql);
            List<User> userList = Lists.newArrayList();
            while(resultSet.next()) {
                User user = new User();
                user.setId(resultSet.getInt(1));
                user.setName(name);
                user.setEmail(resultSet.getString(3));
                user.setPhone(resultSet.getString(4));
                userList.add(user);
            }
            connection.close();
            connection = null;
            statement.close();
            return userList;
        } catch (Exception e) {
            throw new DaoException(e);
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    log.error(".....");
                }
            }
        }
    }

当我们只需要完成一个操作的项目时,这种方式还可以接受,但当项目中有大量的DAO需要操作时,难免过程中会出现各种问题,如忘记关闭连接等。
其实我们可以发现整个的数据库的操作实现可以分为四个部分:资源管理(数据库的连接关闭等操作)、sql执行(查询、更新等)、结果集的处理(将sql查询结果转化)、异常处理。
那是不是可以将公共部分抽象成一个模板进行使用呢?现在我们来定义一个Jdbc的一个模板

public class JdbcTemplate {
    public final Object execute(StatementCallback callback) {
        Connection connection = null;
        Statement statement = null;
        try {
            connection = getConnetion();
            statement = con.createStatement();
            Object ret = callback.doWithStatement(callback);
            return retValue;
        } catch (SQLException e) {
            DateAccessException ex = translateSqlException(e);
            throw ex;
        } finally {
            closeStatement(statement);
            releaseConnection(connection);
        }
    }
}

Template 定义了关注了操作的所有过程,只需要传递一个callback,就可以帮我们处理各种细节化操作,这些细节化操作包括:获取数据库连接;执行操作;处理异常;资源释放。那我们在使用时就可以简化为

private JdbcTemplate jdbcTemplate;
// Jdbc连接数据库等操作,未完成具体实现
private DataSource dataSource;
public User getUserInfoByName(String name) {
    StatementCallback statementCallback = new StatementCallback() {
        @Override
        public Object doInStatement(Statement stmt) throws SQLException, DataAccessException {
            return null;
        }
    }
     return jdbcTemplate.execute(statementCallback);
     
}

实际上,Template 在封装时远比这个复杂,接下来我们就看一下spring-jdbc 是如何对jdbc进行封装的

JdbcTemplate 实现了JdbcOperations接口和继承了JdbcAccessor。
JdbcOperations 定义了数据库的操作,excute、 query、update 等,它是对行为的一种封装。
JdbcAccessor 封装了对资源的操作以及异常的处理,可以看一下源码,比较短。

public abstract class JdbcAccessor implements InitializingBean {
 
   /** Logger available to subclasses */
   protected final Log logger = LogFactory.getLog(getClass());
 
   private DataSource dataSource;
 
   private SQLExceptionTranslator exceptionTranslator;
 
   private boolean lazyInit = true;
 
 
   /**
    * Set the JDBC DataSource to obtain connections from.
    */
   public void setDataSource(DataSource dataSource) {
      this.dataSource = dataSource;
   }
 
   /**
    * Return the DataSource used by this template.
    */
   public DataSource getDataSource() {
      return this.dataSource;
   }
 
   /**
    * Specify the database product name for the DataSource that this accessor uses.
    * This allows to initialize a SQLErrorCodeSQLExceptionTranslator without
    * obtaining a Connection from the DataSource to get the metadata.
    * @param dbName the database product name that identifies the error codes entry
    * @see SQLErrorCodeSQLExceptionTranslator#setDatabaseProductName
    * @see java.sql.DatabaseMetaData#getDatabaseProductName()
    */
   public void setDatabaseProductName(String dbName) {
      this.exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dbName);
   }
 
   /**
    * Set the exception translator for this instance.
    * <p>If no custom translator is provided, a default
    * {@link SQLErrorCodeSQLExceptionTranslator} is used
    * which examines the SQLException's vendor-specific error code.
    * @see org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator
    * @see org.springframework.jdbc.support.SQLStateSQLExceptionTranslator
    */
   public void setExceptionTranslator(SQLExceptionTranslator exceptionTranslator) {
      this.exceptionTranslator = exceptionTranslator;
   }
 
   /**
    * Return the exception translator for this instance.
    * <p>Creates a default {@link SQLErrorCodeSQLExceptionTranslator}
    * for the specified DataSource if none set, or a
    * {@link SQLStateSQLExceptionTranslator} in case of no DataSource.
    * @see #getDataSource()
    */
   public synchronized SQLExceptionTranslator getExceptionTranslator() {
      if (this.exceptionTranslator == null) {
         DataSource dataSource = getDataSource();
         if (dataSource != null) {
            this.exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
         }
         else {
            this.exceptionTranslator = new SQLStateSQLExceptionTranslator();
         }
      }
      return this.exceptionTranslator;
   }
 
   /**
    * Set whether to lazily initialize the SQLExceptionTranslator for this accessor,
    * on first encounter of a SQLException. Default is "true"; can be switched to
    * "false" for initialization on startup.
    * <p>Early initialization just applies if {@code afterPropertiesSet()} is called.
    * @see #getExceptionTranslator()
    * @see #afterPropertiesSet()
    */
   public void setLazyInit(boolean lazyInit) {
      this.lazyInit = lazyInit;
   }
 
   /**
    * Return whether to lazily initialize the SQLExceptionTranslator for this accessor.
    * @see #getExceptionTranslator()
    */
   public boolean isLazyInit() {
      return this.lazyInit;
   }
 
   /**
    * Eagerly initialize the exception translator, if demanded,
    * creating a default one for the specified DataSource if none set.
    */
   @Override
   public void afterPropertiesSet() {
      if (getDataSource() == null) {
         throw new IllegalArgumentException("Property 'dataSource' is required");
      }
      if (!isLazyInit()) {
         getExceptionTranslator();
      }
   }
 
}

源码有三个参数:datasource、exceptionTranslator(转换各种数据库方案商的不同的数据库异常)、lazyInit(延时加载:是否在applicationContext 初始化时就进行实例化)

在使用的过程中我们可以看到,只需要提供一个statementCallback,就可以实现对Dao 的各种操作。spring-jdbc 为了满足各种场景的需要,为我们提供了四组不同权限的callback

在使用的过程中我们可以看到,只需要提供一个statementCallback,就可以实现对Dao 的各种操作。spring-jdbc 为了满足各种场景的需要,为我们提供了四组不同权限的callback

| callback | 说明 |
| :-------- :|: --------:|
|CallableStatementCallback|面向存储过程|
|ConnectionCallback|面向连接的call,权限最大(但一般情况应该避免使用,造成操作不当)|
|PreparedStatementCallback|包含查询询参数的的callback,可以防止sql 注入|
|StatementCallback|缩小了ConnectionCallback的权限范围,不允许操作数据库的连接|

我们再看一下JdbcTemplate 的封装

public <T> T execute(ConnectionCallback<T> action) throws DataAccessException {
   Assert.notNull(action, "Callback object must not be null");
 
   Connection con = DataSourceUtils.getConnection(getDataSource());
   try {
      Connection conToUse = con;
      if (this.nativeJdbcExtractor != null) {
         // Extract native JDBC Connection, castable to OracleConnection or the like.
         conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
      }
      else {
         // Create close-suppressing Connection proxy, also preparing returned Statements.
         conToUse = createConnectionProxy(con);
      }
      return action.doInConnection(conToUse);
   }
   catch (SQLException ex) {
      // Release Connection early, to avoid potential connection pool deadlock
      // in the case when the exception translator hasn't been initialized yet.
      DataSourceUtils.releaseConnection(con, getDataSource());
      con = null;
      throw getExceptionTranslator().translate("ConnectionCallback", getSql(action), ex);
   }
   finally {
      DataSourceUtils.releaseConnection(con, getDataSource());
   }
}

有两个需要注意的地方

Connection con = DataSourceUtils.getConnection(getDataSource());

这里创建连接使用的是DataSourceUtils,而不是datasource.getConnection,这是由于考虑到了事务处理的因素。

if (this.nativeJdbcExtractor != null) {
         // Extract native JDBC Connection, castable to OracleConnection or the like.
         conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
      }

这里并不一定使用的是jdbc的connection,因为jdbc是一种统一化封装,而忽略了各个sql供应商的差异性。有时间我们需要使用某一数据库的某种特性(比如Oracle sql)时,就可以通过对nativeJdbcExtractor来达到目的。
JdbcTemplate 还有几个演生的template,这里都不再详细介绍。
Ok,关于template 的介绍就到此为止(这里更倾向于介绍各种技术的实现原理,而非如何使用)。

2.2 对象模式

对象模式其实只是把Template 中的操作封装成各个对象,而其本质的实现方式仍然是Template

三、缺陷

spring-jdbc的封装方式得到了广泛认可,但并不代表它是一个友好的的操作数据库的工具。 从上面的介绍过程中,我们可以感受到jdbc 的封装是面向底层的,所以它对于上层的使用方并不那么友好。jdbc 并未能真正的实现业务和数据的完全分离,对callback的定义仍然会穿插在业务当中,所以在实际的业务应用中,已经很少直接使用jdbc。因此spring 也对很多其它的ORM框架进行了支持,如ibatis,hibernate,JDO等等,这些更高级对用户更加友好。接下我会用一系列文章,对这些框架进行介绍

四、总结

我们再来回顾一下最前面提出的三个问题:

  1. spring-jdbc 是为了解决数据和业务分离的问题,使客户端能够更专注于业务层面,而不必关注数据库资源的连接释放及异常处理等逻辑。
  2. spring-jdbc 采用dao模式实现了业务和数据的分离;使用模板模式,实现了逻辑的封装
  3. spring-jdbc 属于面向低层的实现,对用户不太友好。

个人能力有限,有错误之处还请指证.....

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,724评论 6 342
  • 01 这个话题好像已经被说的烂大街了,同样我也知道我的题目特别特别俗。但只有这两个词才能形容我接下来说的事了。 0...
    都乐很可爱阅读 193评论 0 1
  • 球球的游戏规则就是大球吃小球,最终的目的就是生存下来。刚开始我很不解,就一直吃那些小点点有什么好玩的,可当我真的试...
    方默默阅读 534评论 6 5
  • 文/若杉 假日,闲来翻看朋友圈,见友人发了这样一条消息: “最无聊的7天开始了,不知道去哪儿,也没人约,继续在家睡...
    若杉阅读 956评论 11 18