save/update功能的实现

前言

以上篇文章Spring连接数据库的实现为基础开始分析Spring中JDBC的支持,首先寻找整个功能的切入点,在例子中我们可以看到所有的数据库的操作都封装在了UserServiceImpl中,而UserServiceImpl中的所有数据库的操作又以其内部属性jdbcTemplate为基础。这个jdbcTemplate可以作为源码分析的切入点,来一起看看它是如何实现又是如何初始化的?

sava/update功能的实现

在UserServiceImpl中jdbcTemplate的初始化是从setDataSource函数开始的,DataSource实例通过参数注入,DataSource的创建过程是引入第三方的连接池。DataSource是整个数据库操作的基础,里面封装了整个数据库的连接信息。我们首先以保存实体类为例进行代码跟踪。

public void save(User user) {
        jdbcTemplate.update("insert into USER(name ,age,sex) values (?,?,?)",new Object[]{user.getName(),user.getAge(),user.getSex()},
                new int[]{Types.VARCHAR,Types.INTEGER,Types.VARCHAR});
    }

对于保存一个实体类来讲,在操作中我们只需要提供SQL语句,以及语句中对应的参数和参数类型,其他的操作便可以交给Spring来完成了,这些工作到底包括什么呢?进入jdbcTemplate的update方法:

public int update(String sql, Object[] args, int[] argTypes) throws DataAccessException {
        return this.update(sql, this.newArgTypePreparedStatementSetter(args, argTypes));
    }
public int update(String sql, PreparedStatementSetter pss) throws DataAccessException {
        return this.update((PreparedStatementCreator)(new JdbcTemplate.SimplePreparedStatementCreator(sql)), (PreparedStatementSetter)pss);
    }

进入update之后,Spring并不是急于进入核心处理操作,而是先做足准备工作,使用ArgTypePreparedStatementSetter对参数与参数类型进行封装,同时又使用SimplePreparedStatementCreator对SQL语句进行封装。

经过了数据封装后便可以进入核心的数据处理代码了:

protected int update(PreparedStatementCreator psc, final PreparedStatementSetter pss) throws DataAccessException {
        this.logger.debug("Executing prepared SQL update");
        return (Integer)this.execute(psc, new PreparedStatementCallback<Integer>() {
            public Integer doInPreparedStatement(PreparedStatement ps) throws SQLException {
                Integer var3;
                try {
                    if (pss != null) {
                        pss.setValues(ps);
                    }

                    int rows = ps.executeUpdate();
                    if (JdbcTemplate.this.logger.isDebugEnabled()) {
                        JdbcTemplate.this.logger.debug("SQL update affected " + rows + " rows");
                    }

                    var3 = rows;
                } finally {
                    if (pss instanceof ParameterDisposer) {
                        ((ParameterDisposer)pss).cleanupParameters();
                    }

                }

                return var3;
            }
        });
    }
pss.setValues(ps);:设置prepareStatement所需的全部参数。

其中execute方法时最基础的操作。那么接下来看看这个方法。

基础方法execute

execute作为数据库操作的核心入口,将大多数数据库操作相同的步骤统一封装,而将个性化的操作使用参数PreparedStatementCallback进行回调。

public <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action)
            throws DataAccessException {

        Assert.notNull(psc, "PreparedStatementCreator must not be null");
        Assert.notNull(action, "Callback object must not be null");
        if (logger.isDebugEnabled()) {
            String sql = getSql(psc);
            logger.debug("Executing prepared SQL statement" + (sql != null ? " [" + sql + "]" : ""));
        }
        //获取数据库连接
        Connection con = DataSourceUtils.getConnection(obtainDataSource());
        PreparedStatement ps = null;
        try {
            ps = psc.createPreparedStatement(con);
            //应用用户设定的参数
            applyStatementSettings(ps);
            //调用回调函数
            T result = action.doInPreparedStatement(ps);
            handleWarnings(ps);
            return result;
        }
        catch (SQLException ex) {
            // 释放数据库连接避免当异常转换器没有被初始化的时候出现潜在的连接池死锁
            if (psc instanceof ParameterDisposer) {
                ((ParameterDisposer) psc).cleanupParameters();
            }
            String sql = getSql(psc);
            JdbcUtils.closeStatement(ps);
            ps = null;
            DataSourceUtils.releaseConnection(con, getDataSource());
            con = null;
            throw translateException("PreparedStatementCallback", sql, ex);
        }
        finally {
            if (psc instanceof ParameterDisposer) {
                ((ParameterDisposer) psc).cleanupParameters();
            }
            JdbcUtils.closeStatement(ps);
            DataSourceUtils.releaseConnection(con, getDataSource());
        }
    }

上述代码对常用的操作进行了封装,我们对几个重要的方法的封装进行解析:

1.获取数据库连接

获取数据库连接也并非直接使用DataSource.getConnection方法那么简单,同样也考虑了很多的情况。

public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
        try {
            return doGetConnection(dataSource);
        }
        catch (SQLException ex) {
            throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", ex);
        }
        catch (IllegalStateException ex) {
            throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection: " + ex.getMessage());
        }
    }
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
        Assert.notNull(dataSource, "No DataSource specified");

        ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
        if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
            conHolder.requested();
            if (!conHolder.hasConnection()) {
                logger.debug("Fetching resumed JDBC Connection from DataSource");
                conHolder.setConnection(fetchConnection(dataSource));
            }
            return conHolder.getConnection();
        }

        logger.debug("Fetching JDBC Connection from DataSource");
        Connection con = fetchConnection(dataSource);
        //当前线程支持同步
        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            logger.debug("Registering transaction synchronization for JDBC Connection");
            // 在事物中使用同一数据库连接
            ConnectionHolder holderToUse = conHolder;
            if (holderToUse == null) {
                holderToUse = new ConnectionHolder(con);
            }
            else {
                holderToUse.setConnection(con);
            }
            //记录数据库连接
            holderToUse.requested();
            TransactionSynchronizationManager.registerSynchronization(
                    new ConnectionSynchronization(holderToUse, dataSource));
            holderToUse.setSynchronizedWithTransaction(true);
            if (holderToUse != conHolder) {
                TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
            }
        }

        return con;
    }

在数据库连接方面,Spring主要考虑的是关于事物方面的处理。基于事物处理的特殊性,Spring需要保证线程中的数据库操作都是使用同一事物连接。

2.应用用户设定的输入参数
protected void applyStatementSettings(Statement stmt) throws SQLException {
        int fetchSize = getFetchSize();
        if (fetchSize != -1) {
            stmt.setFetchSize(fetchSize);
        }
        int maxRows = getMaxRows();
        if (maxRows != -1) {
            stmt.setMaxRows(maxRows);
        }
        DataSourceUtils.applyTimeout(stmt, getDataSource(), getQueryTimeout());
    }

setFetchSize最主要是为了减少网络交互次数设计的。访问ResultSet时,如果它每次只从服务器上读取一行数据,则会产生大量的开销。setFetchSize的意思是当调用rs.next时,ResultSet会一次性的从服务器上取得多少行数据回来,这样下次rs.next时,它可以直接从内存中获取数据而不需要网络交互,提高了效率。这个设置可能会被某些JDBC驱动忽略,而且设置过大会造成内存的上升。

setMaxRows将此Statement对象生成的所有ResultSet对象可以包含的最大行数限制设置为给定数。

3.调用回调函数

处理一些通用方法外的个性化处理,也就是PrepareStatementCallback类型的参数的doInPrepareStatement方法的回调。

4.警告处理
protected void handleWarnings(Statement stmt) throws SQLException {
        //当设置为忽略警告时只尝试打印日志
        if (isIgnoreWarnings()) {
            if (logger.isDebugEnabled()) {
                //如果日志开启的情况下打印日志
                SQLWarning warningToLog = stmt.getWarnings();
                while (warningToLog != null) {
                    logger.debug("SQLWarning ignored: SQL state '" + warningToLog.getSQLState() + "', error code '" +
                            warningToLog.getErrorCode() + "', message [" + warningToLog.getMessage() + "]");
                    warningToLog = warningToLog.getNextWarning();
                }
            }
        }
        else {
            handleWarnings(stmt.getWarnings());
        }
    }

这里用到了一个类SQLWarning,SQLWarning提供关于数据库访问警告信息的异常。这些警告直接链接到导致报告警告的方法所在的对象。警告可以从Connection、Statement和ResultSet对象中获得。试图在已经关闭的连接上获取警告将导致抛出异常。类似的,试图在已经关闭的语句上或已经关闭的结果集上获取警告也将会导致抛出异常。注意,关闭语句时还会关闭它可能生成的结果集。

很多人不是很理解什么情况下会产生警告而不是异常,在这里举个常见的例子,最常见的警告DataTruncation:DataTruncation直接继承SQLWarning,由于某种原因意外的截断数据值时会抛出这个以警告形式的异常。

对于警告的处理方式并不是直接抛出异常,出现警告很可能会出现数据错误,但是,并不一定影响程序执行,所以用户可以自己设置处理警告的方式,如默认的是忽略警告,当出现警告时只打印警告日志,而另一种方式只直接抛出异常。

5.资源释放

数据库的连接释放并不是直接调用了Connection的API中的close方法。考虑到存在事物的情况,如果当前线程存在事物,那么说明在当前线程中存在共用数据库连接,这种情况下直接使用ConnectionHolder中的released方法进行连接数减一,而不是真正的释放连接。

public static void releaseConnection(@Nullable Connection con, @Nullable DataSource dataSource) {
        try {
            doReleaseConnection(con, dataSource);
        }
        catch (SQLException ex) {
            logger.debug("Could not close JDBC Connection", ex);
        }
        catch (Throwable ex) {
            logger.debug("Unexpected exception on closing JDBC Connection", ex);
        }
    }
public static void doReleaseConnection(@Nullable Connection con, @Nullable DataSource dataSource) throws SQLException {
        if (con == null) {
            return;
        }
        if (dataSource != null) {
            //当前线程中存在共用数据库连接,这种情况下直接使用ConnectionHolder中的released方法进行连接数减一,而不是真正的释放连接。
            ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
            if (conHolder != null && connectionEquals(conHolder, con)) {
                // 这是事务连接:不要关闭它。
                conHolder.released();
                return;
            }
        }
        logger.debug("Returning JDBC Connection to DataSource");
        doCloseConnection(con, dataSource);
    }

update中的回调函数

PreparedStatementCallback作为一个接口,其中只有一个函数doInPreparedStatement,这个函数是用于调用通用方法execute的时候无法处理的一些个性化处理方法,在update中的函数实现为:

public Integer doInPreparedStatement(PreparedStatement ps){
    try {
                if (pss != null) {
                    pss.setValues(ps);
                }
                int rows = ps.executeUpdate();
                if (logger.isTraceEnabled()) {
                    logger.trace("SQL update affected " + rows + " rows");
                }
                return rows;
            }
            finally {
                if (pss instanceof ParameterDisposer) {
                    ((ParameterDisposer) pss).cleanupParameters();
                }
            }
}

其中用于真正执行的SQL的是ps.executeUpdate没有其他什么需要分析的,因为我们平时在直接使用JDBC方式进行调用的时候会经常使用此方法。但是,对于设置输入参数的函数ps.setValues(ps),我们有必要去深究一下:

public void setValues(PreparedStatement ps) throws SQLException {
        int parameterPosition = 1;
        if (this.args != null && this.argTypes != null) {
            //=遍历每个参数以 做 类型匹配及转换
            for (int i = 0; i < this.args.length; i++) {
                Object arg = this.args[i];
                //如果是集合类则需要进入集合类内部递归解析集合内部属性
                if (arg instanceof Collection && this.argTypes[i] != Types.ARRAY) {
                    Collection<?> entries = (Collection<?>) arg;
                    for (Object entry : entries) {
                        if (entry instanceof Object[]) {
                            Object[] valueArray = ((Object[]) entry);
                            for (Object argValue : valueArray) {
                                doSetValue(ps, parameterPosition, this.argTypes[i], argValue);
                                parameterPosition++;
                            }
                        }
                        else {
                            doSetValue(ps, parameterPosition, this.argTypes[i], entry);
                            parameterPosition++;
                        }
                    }
                }
                else {
                    //解析当前属性
                    doSetValue(ps, parameterPosition, this.argTypes[i], arg);
                    parameterPosition++;
                }
            }
        }
    }

参考:《Spring源码深度解析》 郝佳  编著:

原文地址:https://www.cnblogs.com/Joe-Go/p/10250322.html