spring源码分析之spring-jdbc模块详解

时间:2020-12-08 22:29:19

0 概述

Spring将替我们完成所有使用JDBC API进行开发的单调乏味的、底层细节处理工作。下表描述了哪些是spring帮助我们做好的,哪些是我们要做的。

Action  Spring  You
Define connection parameters.    X
Open the connection.  X  
Specify the SQL statement.    X
Declare parameters and provide parameter values   X
Prepare and execute the statement. X  
Set up the loop to iterate through the results (if any). X  
Do the work for each iteration.   X
Process any exception.  X  
Handle transactions.  X  
Close the connection,statement and resultset. X  

工作模式 
使用Spring进行基本的JDBC访问数据库有多种选择。Spring至少提供了三种不同的工作模式:JdbcTemplate, 一个在Spring2.5中新提供的SimpleJdbc类能够更好的处理数据库元数据; 还有一种称之为RDBMS Object的风格的面向对象封装方式, 有点类似于JDO的查询设计。 我们在这里简要列举你采取某一种工作方式的主要理由. 不过请注意, 即使你选择了其中的一种工作模式, 你依然可以在你的代码中混用其他任何一种模式以获取其带来的好处和优势。 所有的工作模式都必须要求JDBC 2.0以上的数据库驱动的支持, 其中一些高级的功能可能需要JDBC 3.0以上的数据库驱动支持。 

JdbcTemplate - 这是经典的也是最常用的Spring对于JDBC访问的方案。这也是最低级别的封装, 其他的工作模式事实上在底层使用了JdbcTemplate作为其底层的实现基础。JdbcTemplate在JDK 1.4以上的环境上工作得很好。 

NamedParameterJdbcTemplate - 对JdbcTemplate做了封装,提供了更加便捷的基于命名参数的使用方式而不是传统的JDBC所使用的“?”作为参数的占位符。这种方式在你需要为某个SQL指定许多个参数时,显得更加直观而易用。该特性必须工作在JDK 1.4以上。 

SimpleJdbcTemplate - 这个类结合了JdbcTemplate和NamedParameterJdbcTemplate的最常用的功能,同时它也利用了一些Java 5的特性所带来的优势,例如泛型、varargs和autoboxing等,从而提供了更加简便的API访问方式。需要工作在Java 5以上的环境中。 

SimpleJdbcInsert 和 SimpleJdbcCall - 这两个类可以充分利用数据库元数据的特性来简化配置。通过使用这两个类进行编程,你可以仅仅提供数据库表名或者存储过程的名称以及一个Map作为参数。其中Map的key需要与数据库表中的字段保持一致。这两个类通常和SimpleJdbcTemplate配合使用。这两个类需要工作在JDK 5以上,同时数据库需要提供足够的元数据信息。 

RDBMS 对象包括MappingSqlQuery, SqlUpdate and StoredProcedure - 这种方式允许你在初始化你的数据访问层时创建可重用并且线程安全的对象。该对象在你定义了你的查询语句,声明查询参数并编译相应的Query之后被模型化。一旦模型化完成,任何执行函数就可以传入不同的参数对之进行多次调用。这种方式需要工作在JDK 1.4以上。

 

1. 异常处理

   异常结构如下:

spring源码分析之spring-jdbc模块详解

 SQLExceptionTranslator是一个接口,如果你需要在 SQLException和org.springframework.dao.DataAccessException之间作转换,那么必须实现该接口。 转换器类的实现可以采用一般通用的做法(比如使用JDBC的SQLState code),如果为了使转换更准确,也可以进行定制(比如使用Oracle的error code)。 

SQLErrorCodeSQLExceptionTranslator是SQLExceptionTranslator的默认实现。 该实现使用指定数据库厂商的error code,比采用SQLState更精确。转换过程基于一个JavaBean(类型为SQLErrorCodes)中的error code。 这个JavaBean由SQLErrorCodesFactory工厂类创建,其中的内容来自于 “sql-error-codes.xml”配置文件。该文件中的数据库厂商代码基于 Database MetaData 信息中的DatabaseProductName,从而配合当前数据库的使用。 

SQLErrorCodeSQLExceptionTranslator使用以下的匹配规则: 

首先检查是否存在完成定制转换的子类实现。通常SQLErrorCodeSQLExceptionTranslator 这个类可以作为一个具体类使用,不需要进行定制,那么这个规则将不适用。 

接着将SQLException的error code与错误代码集中的error code进行匹配。 默认情况下错误代码集将从SQLErrorCodesFactory取得。 错误代码集来自classpath下的sql-error-codes.xml文件,它们将与数据库metadata信息中的database name进行映射。 

使用fallback翻译器。SQLStateSQLExceptionTranslator类是缺省的fallback翻译器。

 

2. config模块

  NamespaceHandler接口,DefaultBeanDefinitionDocumentReader使用该接口来处理在spring xml配置文件中自定义的命名空间。

spring源码分析之spring-jdbc模块详解

在jdbc模块,我们使用JdbcNamespaceHandler来处理jdbc配置的命名空间,其代码如下:

public class JdbcNamespaceHandler extends NamespaceHandlerSupport {

@Override
public void init() {
registerBeanDefinitionParser(
"embedded-database", new EmbeddedDatabaseBeanDefinitionParser());
registerBeanDefinitionParser(
"initialize-database", new InitializeDatabaseBeanDefinitionParser());
}
}

其中,

EmbeddedDatabaseBeanDefinitionParser继承了AbstractBeanDefinitionParser,解析<embedded-database>元素,并使用EmbeddedDatabaseFactoryBean创建一个BeanDefinition。顺便介绍一下用到的软件包 org.w3c.dom。

软件包 org.w3c.dom:为文档对象模型 (DOM) 提供接口,该模型是 Java API for XML Processing 的组件 API。该 Document Object Model Level 2 Core API 允许程序动态访问和更新文档的内容和结构。

Attr:Attr 接口表示 Element 对象中的属性。
CDATASection: CDATA 节用于转义文本块,该文本块包含的字符如果不转义则会被视为标记。
CharacterData: CharacterData 接口使用属性集合和用于访问 DOM 中字符数据的方法扩展节点。
Comment: 此接口继承自 CharacterData 表示注释的内容,即起始 '<!--' 和结束 '-->' 之间的所有字符。
Document: Document 接口表示整个 HTML 或 XML 文档。
DocumentFragment: DocumentFragment 是“轻量级”或“最小”Document 对象。
DocumentType: 每个 Document 都有 doctype 属性,该属性的值可以为 null,也可以为 DocumentType 对象。
DOMConfiguration: 该 DOMConfiguration 接口表示文档的配置,并维护一个可识别的参数表。
DOMError: DOMError 是一个描述错误的接口。
DOMErrorHandler: DOMErrorHandler 是在报告处理 XML 数据时发生的错误或在进行某些其他处理(如验证文档)时 DOM 实现可以调用的回调接口。
DOMImplementation: DOMImplementation 接口为执行独立于文档对象模型的任何特定实例的操作提供了许多方法。
DOMImplementationList: DOMImplementationList 接口提供对 DOM 实现的有序集合的抽象,没有定义或约束如何实现此集合。
DOMImplementationSource: 此接口允许 DOM 实现程序根据请求的功能和版本提供一个或多个实现,如下所述。
DOMLocator: DOMLocator 是一个描述位置(如发生错误的位置)的接口。
DOMStringList: DOMStringList 接口提供对 DOMString 值的有序集合的抽象,没有定义或约束此集合是如何实现的。
Element: Element 接口表示 HTML 或 XML 文档中的一个元素。
Entity: 此接口表示在 XML 文档中解析和未解析的已知实体。
EntityReference: EntityReference 节点可以用来在树中表示实体引用。
NamedNodeMap: 实现 NamedNodeMap 接口的对象用于表示可以通过名称访问的节点的集合。
NameList NameList 接口提供对并行的名称和名称空间值对(可以为 null 值)的有序集合的抽象,无需定义或约束如何实现此集合。
Node: 该 Node 接口是整个文档对象模型的主要数据类型。
NodeList: NodeList 接口提供对节点的有序集合的抽象,没有定义或约束如何实现此集合。
Notation: 此接口表示在 DTD 中声明的表示法。
ProcessingInstruction: ProcessingInstruction 接口表示“处理指令”,该指令作为一种在文档的文本中保持特定于处理器的信息的方法在 XML 中使用。
Text: 该 Text 接口继承自 CharacterData,并且表示 Element 或 Attr 的文本内容(在 XML 中称为 字符数据)。
TypeInfo: TypeInfo 接口表示从 Element 或 Attr 节点引用的类型,用与文档相关的模式指定。
UserDataHandler: 当使用 Node.setUserData() 将一个对象与节点上的键相关联时,当克隆、导入或重命名该对象关联的节点时应用程序可以提供调用的处理程序。

 3. core模块

     3.1 NativeJdbcExtractor 从线程池中的封装的对象中提取出本地的jdbc对象,其结构如下:

spring源码分析之spring-jdbc模块详解

其实现原理如下(以c3po为例):

    /**
* Retrieve the Connection via C3P0's {
@code rawConnectionOperation} API,
* using the {
@code getRawConnection} as callback to get access to the
* raw Connection (which is otherwise not directly supported by C3P0).
*
@see #getRawConnection
*/
@Override
protected Connection doGetNativeConnection(Connection con) throws SQLException {
if (con instanceof C3P0ProxyConnection) {
C3P0ProxyConnection cpCon
= (C3P0ProxyConnection) con;
try {
return (Connection) cpCon.rawConnectionOperation(
this.getRawConnectionMethod, null, new Object[] {C3P0ProxyConnection.RAW_CONNECTION});
}
catch (SQLException ex) {
throw ex;
}
catch (Exception ex) {
ReflectionUtils.handleReflectionException(ex);
}
}
return con;
}

上述代码通过调用C3P0的rawConnectionOperation api来获取Connection,使用getRawConnection的回调方法来获取原生的Connection(C3P0不直接支持原生的Connection)。

NativeJdbcExtractorAdapter是NativeJdbcExtractor的一个简单实现,它的getNativeConnection()方法检查ConnectionProxy链,并且代理doGetNativeConnection方法。Spring的TransactionAwareDataSourceProxy和LazyConnectionDataSourceProxy使用ConnectionProxy。目标Connection置于本地连接池中,被子类实现的doGetNativeConnection的方法去掉封装获取到原生的Connection。其实现如下:

@Override
public Connection getNativeConnection(Connection con) throws SQLException {
if (con == null) {
return null;
}
Connection targetCon
= DataSourceUtils.getTargetConnection(con);
Connection nativeCon
= doGetNativeConnection(targetCon);
if (nativeCon == targetCon) {
// We haven't received a different Connection, so we'll assume that there's
// some additional proxying going on. Let's check whether we get something
// different back from the DatabaseMetaData.getConnection() call.
DatabaseMetaData metaData = targetCon.getMetaData();
// The following check is only really there for mock Connections
// which might not carry a DatabaseMetaData instance.
if (metaData != null) {
Connection metaCon
= metaData.getConnection();
if (metaCon != null && metaCon != targetCon) {
// We've received a different Connection there:
// Let's retry the native extraction process with it.
nativeCon = doGetNativeConnection(metaCon);
}
}
}
return nativeCon;
}

  3.2 RowMapper

 

spring源码分析之spring-jdbc模块详解

 

3.3 元数据metaData模块

本节中spring应用到工厂模式,结合代码可以更具体了解。

spring源码分析之spring-jdbc模块详解

CallMetaDataProviderFactory创建CallMetaDataProvider的工厂类,其代码如下:

/** List of supported database products for procedure calls */
public static final List<String> supportedDatabaseProductsForProcedures = Arrays.asList(
"Apache Derby",
"DB2",
"MySQL",
"Microsoft SQL Server",
"Oracle",
"PostgreSQL",
"Sybase"
);
/** List of supported database products for function calls */
public static final List<String> supportedDatabaseProductsForFunctions = Arrays.asList(
"MySQL",
"Microsoft SQL Server",
"Oracle",
"PostgreSQL"
);

/**
* Create a CallMetaDataProvider based on the database metadata
*
@param dataSource used to retrieve metadata
*
@param context the class that holds configuration and metadata
*
@return instance of the CallMetaDataProvider implementation to be used
*/
static public CallMetaDataProvider createMetaDataProvider(DataSource dataSource, final CallMetaDataContext context) {
try {
return (CallMetaDataProvider) JdbcUtils.extractDatabaseMetaData(dataSource, new DatabaseMetaDataCallback() {
@Override
public Object processMetaData(DatabaseMetaData databaseMetaData) throws SQLException, MetaDataAccessException {
String databaseProductName
= JdbcUtils.commonDatabaseName(databaseMetaData.getDatabaseProductName());
boolean accessProcedureColumnMetaData = context.isAccessCallParameterMetaData();
if (context.isFunction()) {
if (!supportedDatabaseProductsForFunctions.contains(databaseProductName)) {
if (logger.isWarnEnabled()) {
logger.warn(databaseProductName
+ " is not one of the databases fully supported for function calls " +
"-- supported are: " + supportedDatabaseProductsForFunctions);
}
if (accessProcedureColumnMetaData) {
logger.warn(
"Metadata processing disabled - you must specify all parameters explicitly");
accessProcedureColumnMetaData
= false;
}
}
}
else {
if (!supportedDatabaseProductsForProcedures.contains(databaseProductName)) {
if (logger.isWarnEnabled()) {
logger.warn(databaseProductName
+ " is not one of the databases fully supported for procedure calls " +
"-- supported are: " + supportedDatabaseProductsForProcedures);
}
if (accessProcedureColumnMetaData) {
logger.warn(
"Metadata processing disabled - you must specify all parameters explicitly");
accessProcedureColumnMetaData
= false;
}
}
}

CallMetaDataProvider provider;
if ("Oracle".equals(databaseProductName)) {
provider
= new OracleCallMetaDataProvider(databaseMetaData);
}
else if ("DB2".equals(databaseProductName)) {
provider
= new Db2CallMetaDataProvider((databaseMetaData));
}
else if ("Apache Derby".equals(databaseProductName)) {
provider
= new DerbyCallMetaDataProvider((databaseMetaData));
}
else if ("PostgreSQL".equals(databaseProductName)) {
provider
= new PostgresCallMetaDataProvider((databaseMetaData));
}
else if ("Sybase".equals(databaseProductName)) {
provider
= new SybaseCallMetaDataProvider((databaseMetaData));
}
else if ("Microsoft SQL Server".equals(databaseProductName)) {
provider
= new SqlServerCallMetaDataProvider((databaseMetaData));
}
else {
provider
= new GenericCallMetaDataProvider(databaseMetaData);
}
if (logger.isDebugEnabled()) {
logger.debug(
"Using " + provider.getClass().getName());
}
provider.initializeWithMetaData(databaseMetaData);
if (accessProcedureColumnMetaData) {
provider.initializeWithProcedureColumnMetaData(
databaseMetaData, context.getCatalogName(), context.getSchemaName(), context.getProcedureName());
}
return provider;
}
});
}
catch (MetaDataAccessException ex) {
throw new DataAccessResourceFailureException("Error retreiving database metadata", ex);
}

}

TableMetaDataProviderFactory创建TableMetaDataProvider工厂类,其创建过程如下:

/**
* Create a TableMetaDataProvider based on the database metedata
*
@param dataSource used to retrieve metedata
*
@param context the class that holds configuration and metedata
*
@param nativeJdbcExtractor the NativeJdbcExtractor to be used
*
@return instance of the TableMetaDataProvider implementation to be used
*/
public static TableMetaDataProvider createMetaDataProvider(DataSource dataSource,
final TableMetaDataContext context, final NativeJdbcExtractor nativeJdbcExtractor) {
try {
return (TableMetaDataProvider) JdbcUtils.extractDatabaseMetaData(dataSource,
new DatabaseMetaDataCallback() {
@Override
public Object processMetaData(DatabaseMetaData databaseMetaData) throws SQLException {
String databaseProductName
=
JdbcUtils.commonDatabaseName(databaseMetaData.getDatabaseProductName());
boolean accessTableColumnMetaData = context.isAccessTableColumnMetaData();
TableMetaDataProvider provider;
if ("Oracle".equals(databaseProductName)) {
provider
= new OracleTableMetaDataProvider(databaseMetaData,
context.isOverrideIncludeSynonymsDefault());
}
else if ("HSQL Database Engine".equals(databaseProductName)) {
provider
= new HsqlTableMetaDataProvider(databaseMetaData);
}
else if ("PostgreSQL".equals(databaseProductName)) {
provider
= new PostgresTableMetaDataProvider(databaseMetaData);
}
else if ("Apache Derby".equals(databaseProductName)) {
provider
= new DerbyTableMetaDataProvider(databaseMetaData);
}
else {
provider
= new GenericTableMetaDataProvider(databaseMetaData);
}
if (nativeJdbcExtractor != null) {
provider.setNativeJdbcExtractor(nativeJdbcExtractor);
}
if (logger.isDebugEnabled()) {
logger.debug(
"Using " + provider.getClass().getSimpleName());
}
provider.initializeWithMetaData(databaseMetaData);
if (accessTableColumnMetaData) {
provider.initializeWithTableColumnMetaData(databaseMetaData, context.getCatalogName(),
context.getSchemaName(), context.getTableName());
}
return provider;
}
});
}
catch (MetaDataAccessException ex) {
throw new DataAccessResourceFailureException("Error retrieving database metadata", ex);
}
}

 3.4 使用SqlParameterSource提供参数值 
使用Map来指定参数值有时候工作得非常好,但是这并不是最简单的使用方式。Spring提供了一些其他的SqlParameterSource实现类来指定参数值。 我们首先可以看看BeanPropertySqlParameterSource类,这是一个非常简便的指定参数的实现类,只要你有一个符合JavaBean规范的类就行了。它将使用其中的getter方法来获取参数值。

SqlParameter 封装了定义sql 参数的对象。

CallableStateMentCallback,PrePareStateMentCallback,StateMentCallback,ConnectionCallback回调类分别对应JdbcTemplate中的不同处理方法。

spring源码分析之spring-jdbc模块详解

3.5 simple实现

spring源码分析之spring-jdbc模块详解

4. DataSource

      spring通过DataSource获取数据库的连接。Datasource是jdbc 规范的一部分,它通过ConnectionFactory获取。一个容器和框架可以在应用代码层中隐藏连接池和事务管理。

     当使用spring的jdbc层,你可以通过JNDI来获取DataSource,也可以通过你自己配置的第三方连接池实现来获取。流行的第三方实现由apache Jakarta Commons dbcp和c3p0.

spring源码分析之spring-jdbc模块详解

TransactionAwareDataSourceProxy作为目标DataSource的一个代理, 在对目标DataSource包装的同时,还增加了Spring的事务管理能力, 在这一点上,这个类的功能非常像J2EE服务器所提供的事务化的JNDI DataSource。 

Note 
该类几乎很少被用到,除非现有代码在被调用的时候需要一个标准的 JDBC DataSource接口实现作为参数。 这种情况下,这个类可以使现有代码参与Spring的事务管理。通常最好的做法是使用更高层的抽象 来对数据源进行管理,比如JdbcTemplate和DataSourceUtils等等。

注意:DriverManagerDataSource仅限于测试使用,因为它没有提供池的功能,这会导致在多个请求获取连接时性能很差。

5. object模块

spring源码分析之spring-jdbc模块详解

 

6. JdbcTemplate是core包的核心类。

它替我们完成了资源的创建以及释放工作,从而简化了我们对JDBC的使用。 它还可以帮助我们避免一些常见的错误,比如忘记关闭数据库连接。 JdbcTemplate将完成JDBC核心处理流程,比如SQL语句的创建、执行,而把SQL语句的生成以及查询结果的提取工作留给我们的应用代码。 它可以完成SQL查询、更新以及调用存储过程,可以对ResultSet进行遍历并加以提取。 它还可以捕获JDBC异常并将其转换成org.springframework.dao包中定义的,通用的,信息更丰富的异常。 

使用JdbcTemplate进行编码只需要根据明确定义的一组契约来实现回调接口。 PreparedStatementCreator回调接口通过给定的Connection创建一个PreparedStatement,包含SQL和任何相关的参数。 CallableStatementCreateor实现同样的处理,只不过它创建的是CallableStatement。 RowCallbackHandler接口则从数据集的每一行中提取值。 

我们可以在DAO实现类中通过传递一个DataSource引用来完成JdbcTemplate的实例化,也可以在Spring的IoC容器中配置一个JdbcTemplate的bean并赋予DAO实现类作为一个实例。 需要注意的是DataSource在Spring的IoC容器中总是配制成一个bean,第一种情况下,DataSource bean将传递给service,第二种情况下DataSource bean传递给JdbcTemplate bean。

7. NamedParameterJdbcTemplate类为JDBC操作增加了命名参数的特性支持,而不是传统的使用('?')作为参数的占位符。NamedParameterJdbcTemplate类对JdbcTemplate类进行了封装, 在底层,JdbcTemplate完成了多数的工作。

小结:

传统的JDBC对数据库的操作,有很多重复的代码,这样给程序员带来了很多额外的工作量,Spring提供了JDBC模板很好的解决了这个问题,由于传统的方法比较简单,在这里不介绍了,直接说模板吧。
 
使用JDBC模板:
    Spring的JDBC框架能够承担资源管理和异常处理的工作。对于JDBC来说,Spring提供了3个模板类
  • JdbcTemplate:Spring里最基本的JDBC模板,利用JDBC和简单的索引参数查询提供对数据库的简单访问。
  • NamedParameterJdbcTemplate:能够在执行查询时把值绑定到SQL里的命名参数,而不是使用索引参数。
  • SimpleJdbcTemplate:利用Java 5 的特性,比如自动装箱、通用(generic)和可变参数类表来简化JDBC模板的使用。

 参考文献:

http://www.360doc.com/content/14/0625/22/834950_389749909.shtml

http://www.360doc.com/content/14/0625/23/834950_389759601.shtml