在構建一個系統的過程當中不免須要對數據存儲,而存儲通常會有緩存(內存)、數據庫(硬盤)兩種存儲介質。html
本篇文章咱們主要來介紹下在咱們經過spring構建應用的過程當中如何進行數據庫鏈接、以及數據庫鏈接的幾種方式進行簡單介紹。java
spring中鏈接數據庫有以下幾種方式:spring
直接經過驅動鏈接數據庫的方式sql
spring提供的JdbcTemplate數據庫
spring集成Mybatis,經過Mybatis的方式進行數據庫鏈接apache
通常初學者在學到jdbc這個階段都會動手寫下下面這樣的連接數據庫的代碼,只需三步就能夠從數據庫總拿到數據,這個時候是否是在竊喜終於按照教程把數據拿出來了。見下面代碼:api
Class.forName("org.apache.phoenix.jdbc.PhoenixDriver"); Connection connection = DriverManager.getConnection("jdbc:phoenix:10.1.168.1:2181/hbase"); ResultSet rs = connection.createStatement().executeQuery("select * from table limit 10 ");
針對上面的連接數據的代碼來深刻挖下爲何這樣就能鏈接上數據庫:緩存
org.apache.phoenix.jdbc.PhoenixDriver
這個類在在加載(也就是執行Class.forName(driver)
)的過程當中會執行其靜態代碼塊DriverManager.registerDriver(INSTANCE);
session
執行registerDriver方法後會往靜態registeredDrivers list中添加PhoenixDriver類。mybatis
public static synchronized void registerDriver(java.sql.Driver driver, DriverAction da) throws SQLException { /* Register the driver if it has not already been added to our list */ if(driver != null) { registeredDrivers.addIfAbsent(new DriverInfo(driver, da)); } else { // This is for compatibility with the original DriverManager throw new NullPointerException(); } println("registerDriver: " + driver); }
後面就是進行鏈接操做DriverManager.getConnection(url)
方法源代碼以下:
// Worker method called by the public getConnection() methods. private static Connection getConnection( String url, java.util.Properties info, Class<?> caller) throws SQLException { /* * When callerCl is null, we should check the application's * (which is invoking this class indirectly) * classloader, so that the JDBC driver class outside rt.jar * can be loaded from here. */ ClassLoader callerCL = caller != null ? caller.getClassLoader() : null; synchronized(DriverManager.class) { // synchronize loading of the correct classloader. if (callerCL == null) { callerCL = Thread.currentThread().getContextClassLoader(); } } if(url == null) { throw new SQLException("The url cannot be null", "08001"); } println("DriverManager.getConnection(\"" + url + "\")"); // Walk through the loaded registeredDrivers attempting to make a connection. // Remember the first exception that gets raised so we can reraise it. SQLException reason = null; for(DriverInfo aDriver : registeredDrivers) { // If the caller does not have permission to load the driver then // skip it. if(isDriverAllowed(aDriver.driver, callerCL)) { try { println(" trying " + aDriver.driver.getClass().getName()); Connection con = aDriver.driver.connect(url, info); if (con != null) { // Success! println("getConnection returning " + aDriver.driver.getClass().getName()); return (con); } } catch (SQLException ex) { if (reason == null) { reason = ex; } } } else { println(" skipping: " + aDriver.getClass().getName()); } } // if we got here nobody could connect. if (reason != null) { println("getConnection failed: " + reason); throw reason; } println("getConnection: no suitable driver found for "+ url); throw new SQLException("No suitable driver found for "+ url, "08001"); } }
for(DriverInfo aDriver : registeredDrivers)
去遍歷全部的Drivers而後建立鏈接,得到鏈接了後面就能夠開始進行數據庫操做了。
這種方式是spring針對原始的JDBC的方式進行了一層封裝將全部的操做都託管給了DataSource。
在spring中咱們能夠經過配置文件生成JdbcTemplate:
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"></property> </bean>
或者
@Bean public JdbcTemplate getJdbcTemplate() { DruidDataSource druidDataSource = new DruidDataSource(); return new JdbcTemplate(druidDataSource); }
得到了JdbcTemplate就能夠拿他進行數據庫操做了。
SqlRowSet rowSet = jdbcTemplate.queryForRowSet(sql);
經過上面的範式就能夠獲取數據庫中的數據了。這個能夠換成queryForXXX(sql)更多查詢方式
對於一個普通的類經過繼承org.springframework.jdbc.core.support.JdbcDaoSupport
這個類,而後向類中注入DataSource就能夠實現JDBC的功能了。
<bean id = "exampleDao" class = "com.liutxer.ExampleDao"> <property name="dataSource" ref = "dataSource"></property> </bean>
這樣類exampleDao
經過org.springframework.jdbc.core.support.JdbcDaoSupport#getJdbcTemplate
這個方法得到JdbcTemplate進行數據庫操做。
下面看下爲何會這樣?
其實原理比較簡單JdbcDaoSupport這個類組合了JdbcTemplate在進行DataSource注入的時候會去建立一個JdbcTemplate,後面就能夠經過JdbcDaoSupport#getJdbcTemplate方法拿到建立好的實例操做數據庫了。
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close" </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean" p:dataSource-ref="dataSource" p:mapperLocations="classpath*:phoenix/**/*.xml"/> <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate" c:_0-ref="sqlSessionFactory" scope="prototype"/>
經過上面的方式得到操做數據的句柄(sqlSessionTemplate
),示例經過句柄操做數據得到數據。sqlSessionTemplate.selectList()
這裏換成selectxxx()等,具體能夠參見org.mybatis.spring.SqlSessionTemplate
中的方法。
<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean"> <property name="mapperInterface" value="com.liutxer.dao.UserMapper" /> <property name="sqlSessionFactory" ref="sqlSessionFactory" /> </bean>
使用上面的方式會生成一個MapperFactoryBean
在Spring中獲取userMapper
對象的時候會自動經過MapperFactoryBean
建立出來,這樣就能夠直接使用userMapper中的接口方法去查詢數據庫。
注意1:mapperInterface接口中的接口名稱必須和mapper.xml配置中id一致,這樣才能匹配到具體的sql語句。
注意2:同時若是接口中參數名稱和sql語句中參數不一致能夠經過在接口中加入註解@Param("code")
來進行參數匹配List<User> findUserById(@Param("id") String userId);
注意3:Mybatis從數據庫中拿到數據會自動進行a_b => aB
的匹配,因此代碼中用駝峯數據庫中用下劃線的方式,Mybatis可以進行自動匹配
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="org.mybatis.spring.sample.mapper" /> <!-- optional unless there are multiple session factories defined --> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" /> </bean>
這種方式原理和上面那種方式很類似都是經過MapperFactoryBean
來生成mapper
。MapperScannerConfigurer
在spring
啓動的過程當中會去掃描basePackage
下面全部的接口動態生成MapperFactoryBean
。
注:sqlSessionFactoryBeanName
這個參數不是必須得,若是spring容器中有多個sqlSessionFactory
才須要明確指出來
爲何要使用這種方式?
這種方式主要是解決第二種方式針對每一個mapper接口都要進行一次匹配操做,而致使配置拖沓。
其實Mybatis三種實現數據庫操做的方式最終都是經過sqlSessionTemplate
來操做數據庫的。爲何這麼說,下面來一層層剖析。org.mybatis.spring.mapper.MapperScannerConfigurer#postProcessBeanDefinitionRegistry
這個方法在spring容器啓動的過程當中會被調用,函數體:
@Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { if (this.processPropertyPlaceHolders) { processPropertyPlaceHolders(); } ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry); scanner.setAddToConfig(this.addToConfig); scanner.setAnnotationClass(this.annotationClass); scanner.setMarkerInterface(this.markerInterface); scanner.setSqlSessionFactory(this.sqlSessionFactory); scanner.setSqlSessionTemplate(this.sqlSessionTemplate); scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName); scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName); scanner.setResourceLoader(this.applicationContext); scanner.setBeanNameGenerator(this.nameGenerator); scanner.registerFilters(); scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS)); }
定義了一個scanner(ClassPathMapperScanner
),最後會調用scanner.scan進行掃描basePackage,跟蹤調用層次關係最後會調用到org.mybatis.spring.mapper.ClassPathMapperScanner#processBeanDefinitions
方法
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) { GenericBeanDefinition definition; for (BeanDefinitionHolder holder : beanDefinitions) { definition = (GenericBeanDefinition) holder.getBeanDefinition(); if (logger.isDebugEnabled()) { logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + definition.getBeanClassName() + "' mapperInterface"); } // the mapper interface is the original class of the bean // but, the actual class of the bean is MapperFactoryBean definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59 definition.setBeanClass(this.mapperFactoryBean.getClass()); definition.getPropertyValues().add("addToConfig", this.addToConfig); boolean explicitFactoryUsed = false; if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) { definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName)); explicitFactoryUsed = true; } else if (this.sqlSessionFactory != null) { definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory); explicitFactoryUsed = true; } if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) { if (explicitFactoryUsed) { logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored."); } definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName)); explicitFactoryUsed = true; } else if (this.sqlSessionTemplate != null) { if (explicitFactoryUsed) { logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored."); } definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate); explicitFactoryUsed = true; } if (!explicitFactoryUsed) { if (logger.isDebugEnabled()) { logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'."); } definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); } } }
實例化一個GenericBeanDefinition
放到容器,從方法體能夠看到definition.setBeanClass(this.mapperFactoryBean.getClass());
definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
很明顯了這就是在實例化一個MapperFactoryBean
對象。
下面轉戰MapperFactoryBean
類,發現進行實例化的時候,設置SqlSessionFactory對象的時候進行了SqlSessionTemplate的實例化。
if (!this.externalSqlSession) { this.sqlSession = new SqlSessionTemplate(sqlSessionFactory); } }
這裏就生成了一個SqlSessionTemplate
來進行數據庫操做。
再來完全點轉戰SqlSessionTemplate
的實現,new SqlSessionTemplate(sqlSessionFactory)
最後會調用到
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) { notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required"); notNull(executorType, "Property 'executorType' is required"); this.sqlSessionFactory = sqlSessionFactory; this.executorType = executorType; this.exceptionTranslator = exceptionTranslator; this.sqlSessionProxy = (SqlSession) newProxyInstance( SqlSessionFactory.class.getClassLoader(), new Class[] { SqlSession.class }, new SqlSessionInterceptor()); }
這個方法體重點是this.sqlSessionProxy
這個屬性,由JDK提供的動態代理來動態實例惡化SqlSession.class
這裏SqlSessionInterceptor
經過openSession建立SqlSession
。
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { final Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); final Executor executor = configuration.newExecutor(tx, execType); return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
主要是實例化DefaultSqlSession
執行器executor
。
調用sqlSessionTemplate.selectList方法,最終調用
@Override public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement); return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
調用鏈繼續,後面調用:
@Override public <E> List<E> doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null; try { flushStatements(); Configuration configuration = ms.getConfiguration(); StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameterObject, rowBounds, resultHandler, boundSql); Connection connection = getConnection(ms.getStatementLog()); stmt = handler.prepare(connection); handler.parameterize(stmt); return handler.<E>query(stmt, resultHandler); } finally { closeStatement(stmt); } }
看到了什麼?
Connection
、StatementHandler
,又回到了咱們最開始討論的純JDBC方式從數據庫中獲取數據。再往下異步就能夠看到
@Override public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { PreparedStatement ps = (PreparedStatement) statement; ps.execute(); return resultSetHandler.<E> handleResultSets(ps); }
獲取結果的步驟了。
好吧!寫了這麼多,感受也有點亂了,各位看官能看到這裏也說明足夠有耐性了。JdbcTemplate
的深挖就不繼續了,比起Mybatis這種封裝方式輕量級太多,往下扒兩層就出來了。
堅持深挖源碼的習慣,保持好的學習方式。