版本:
Spring 3.0.4(2.x版本中也存在类似问题)
iBatis 2.3.4.726(2.3.x版本都适用)
起因:
使用Spring管理iBatis实例,标准方式采用SqlMapClientFactoryBean创建SqlMapClient
<bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean"> <property name="configLocation" value="classpath:/com/foo/xxx/sqlMapConfig.xml" /> <property name="dataSource" ref="dataSource" /> <property name="mappingLocations" value="classpath*:/**/sqlmap/*SqlMap.xml" /> </bean>
使用mappingLocations方式定义可以让sqlmap不必添加到sqlMapConfig.xml
即避免了如下形式
<sqlMapConfig> ... <sqlMap url="someSqlMap" resource="com/foo/xxx/someSqlMap.xml"/> ... </sqlMapConfig>
随之而来的问题是在sqlMap中的cache无法在执行预订方法时自动flush!
<cacheModel id="mycache" type ="LRU" readOnly="false" serialize="false"> <flushInterval hours="24"/> <flushOnExecute statement="myobj.insert"/> <property name="cache-size" value="50" /> </cacheModel>
在myobj.insert被执行时cache不会flush,即无报错也无提醒,彻头彻尾的坑爹,不清楚Spring开发组为什么会让这问题一直存在。
产生问题的原因网上有不少分析贴(不再重复,需要的可搜索相关帖子),原因是Spring在处理mappingLocations之前,iBatis已经完成对sqlMapConfig里注册的sqlMap的cache设置(真绕口=.="),非亲生的sqlMap被无情的抛弃了。解决办法大都建议放弃mappingLocations的方式,把sqlMap写到sqlMapConfig里——还真是够简单——或者说够偷懒。
找到原因就想办法解决吧,从SqlMapClientFactoryBean下手,扩展一份
/** * * @author Foxswily */ @SuppressWarnings("deprecation") public class MySqlMapClientFactoryBean implements FactoryBean<SqlMapClient>, InitializingBean { private static final ThreadLocal<LobHandler> configTimeLobHandlerHolder = new ThreadLocal<LobHandler>(); /** * Return the LobHandler for the currently configured iBATIS SqlMapClient, * to be used by TypeHandler implementations like ClobStringTypeHandler. * <p> * This instance will be set before initialization of the corresponding * SqlMapClient, and reset immediately afterwards. It is thus only available * during configuration. * * @see #setLobHandler * @see org.springframework.orm.ibatis.support.ClobStringTypeHandler * @see org.springframework.orm.ibatis.support.BlobByteArrayTypeHandler * @see org.springframework.orm.ibatis.support.BlobSerializableTypeHandler */ public static LobHandler getConfigTimeLobHandler() { return configTimeLobHandlerHolder.get(); } private Resource[] configLocations; private Resource[] mappingLocations; private Properties sqlMapClientProperties; private DataSource dataSource; private boolean useTransactionAwareDataSource = true; @SuppressWarnings("rawtypes") private Class transactionConfigClass = ExternalTransactionConfig.class; private Properties transactionConfigProperties; private LobHandler lobHandler; private SqlMapClient sqlMapClient; public MySqlMapClientFactoryBean() { this.transactionConfigProperties = new Properties(); this.transactionConfigProperties.setProperty("SetAutoCommitAllowed", "false"); } /** * Set the location of the iBATIS SqlMapClient config file. A typical value * is "WEB-INF/sql-map-config.xml". * * @see #setConfigLocations */ public void setConfigLocation(Resource configLocation) { this.configLocations = (configLocation != null ? new Resource[] { configLocation } : null); } /** * Set multiple locations of iBATIS SqlMapClient config files that are going * to be merged into one unified configuration at runtime. */ public void setConfigLocations(Resource[] configLocations) { this.configLocations = configLocations; } /** * Set locations of iBATIS sql-map mapping files that are going to be merged * into the SqlMapClient configuration at runtime. * <p> * This is an alternative to specifying "<sqlMap>" entries in a * sql-map-client config file. This property being based on Spring's * resource abstraction also allows for specifying resource patterns here: * e.g. "/myApp/*-map.xml". * <p> * Note that this feature requires iBATIS 2.3.2; it will not work with any * previous iBATIS version. */ public void setMappingLocations(Resource[] mappingLocations) { this.mappingLocations = mappingLocations; } /** * Set optional properties to be passed into the SqlMapClientBuilder, as * alternative to a <code><properties></code> tag in the * sql-map-config.xml file. Will be used to resolve placeholders in the * config file. * * @see #setConfigLocation * @see com.ibatis.sqlmap.client.SqlMapClientBuilder#buildSqlMapClient(java.io.InputStream, * java.util.Properties) */ public void setSqlMapClientProperties(Properties sqlMapClientProperties) { this.sqlMapClientProperties = sqlMapClientProperties; } /** * Set the DataSource to be used by iBATIS SQL Maps. This will be passed to * the SqlMapClient as part of a TransactionConfig instance. * <p> * If specified, this will override corresponding settings in the * SqlMapClient properties. Usually, you will specify DataSource and * transaction configuration <i>either</i> here <i>or</i> in SqlMapClient * properties. * <p> * Specifying a DataSource for the SqlMapClient rather than for each * individual DAO allows for lazy loading, for example when using * PaginatedList results. * <p> * With a DataSource passed in here, you don't need to specify one for each * DAO. Passing the SqlMapClient to the DAOs is enough, as it already * carries a DataSource. Thus, it's recommended to specify the DataSource at * this central location only. * <p> * Thanks to Brandon Goodin from the iBATIS team for the hint on how to make * this work with Spring's integration strategy! * * @see #setTransactionConfigClass * @see #setTransactionConfigProperties * @see com.ibatis.sqlmap.client.SqlMapClient#getDataSource * @see SqlMapClientTemplate#setDataSource * @see SqlMapClientTemplate#queryForPaginatedList */ public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } /** * Set whether to use a transaction-aware DataSource for the SqlMapClient, * i.e. whether to automatically wrap the passed-in DataSource with Spring's * TransactionAwareDataSourceProxy. * <p> * Default is "true": When the SqlMapClient performs direct database * operations outside of Spring's SqlMapClientTemplate (for example, lazy * loading or direct SqlMapClient access), it will still participate in * active Spring-managed transactions. * <p> * As a further effect, using a transaction-aware DataSource will apply * remaining transaction timeouts to all created JDBC Statements. This means * that all operations performed by the SqlMapClient will automatically * participate in Spring-managed transaction timeouts. * <p> * Turn this flag off to get raw DataSource handling, without Spring * transaction checks. Operations on Spring's SqlMapClientTemplate will * still detect Spring-managed transactions, but lazy loading or direct * SqlMapClient access won't. * * @see #setDataSource * @see org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy * @see org.springframework.jdbc.datasource.DataSourceTransactionManager * @see SqlMapClientTemplate * @see com.ibatis.sqlmap.client.SqlMapClient */ public void setUseTransactionAwareDataSource(boolean useTransactionAwareDataSource) { this.useTransactionAwareDataSource = useTransactionAwareDataSource; } /** * Set the iBATIS TransactionConfig class to use. Default is * <code>com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig</code> * . * <p> * Will only get applied when using a Spring-managed DataSource. An instance * of this class will get populated with the given DataSource and * initialized with the given properties. * <p> * The default ExternalTransactionConfig is appropriate if there is external * transaction management that the SqlMapClient should participate in: be it * Spring transaction management, EJB CMT or plain JTA. This should be the * typical scenario. If there is no active transaction, SqlMapClient * operations will execute SQL statements non-transactionally. * <p> * JdbcTransactionConfig or JtaTransactionConfig is only necessary when * using the iBATIS SqlMapTransactionManager API instead of external * transactions. If there is no explicit transaction, SqlMapClient * operations will automatically start a transaction for their own scope (in * contrast to the external transaction mode, see above). * <p> * <b>It is strongly recommended to use iBATIS SQL Maps with Spring * transaction management (or EJB CMT).</b> In this case, the default * ExternalTransactionConfig is fine. Lazy loading and SQL Maps operations * without explicit transaction demarcation will execute * non-transactionally. * <p> * Even with Spring transaction management, it might be desirable to specify * JdbcTransactionConfig: This will still participate in existing * Spring-managed transactions, but lazy loading and operations without * explicit transaction demaration will execute in their own auto-started * transactions. However, this is usually not necessary. * * @see #setDataSource * @see #setTransactionConfigProperties * @see com.ibatis.sqlmap.engine.transaction.TransactionConfig * @see com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig * @see com.ibatis.sqlmap.engine.transaction.jdbc.JdbcTransactionConfig * @see com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig * @see com.ibatis.sqlmap.client.SqlMapTransactionManager */ @SuppressWarnings("rawtypes") public void setTransactionConfigClass(Class transactionConfigClass) { if (transactionConfigClass == null || !TransactionConfig.class.isAssignableFrom(transactionConfigClass)) { throw new IllegalArgumentException( "Invalid transactionConfigClass: does not implement " + "com.ibatis.sqlmap.engine.transaction.TransactionConfig"); } this.transactionConfigClass = transactionConfigClass; } /** * Set properties to be passed to the TransactionConfig instance used by * this SqlMapClient. Supported properties depend on the concrete * TransactionConfig implementation used: * <p> * <ul> * <li><b>ExternalTransactionConfig</b> supports "DefaultAutoCommit" * (default: false) and "SetAutoCommitAllowed" (default: true). Note that * Spring uses SetAutoCommitAllowed = false as default, in contrast to the * iBATIS default, to always keep the original autoCommit value as provided * by the connection pool. * <li><b>JdbcTransactionConfig</b> does not supported any properties. * <li><b>JtaTransactionConfig</b> supports "UserTransaction" (no default), * specifying the JNDI location of the JTA UserTransaction (usually * "java:comp/UserTransaction"). * </ul> * * @see com.ibatis.sqlmap.engine.transaction.TransactionConfig#initialize * @see com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig * @see com.ibatis.sqlmap.engine.transaction.jdbc.JdbcTransactionConfig * @see com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig */ public void setTransactionConfigProperties(Properties transactionConfigProperties) { this.transactionConfigProperties = transactionConfigProperties; } /** * Set the LobHandler to be used by the SqlMapClient. Will be exposed at * config time for TypeHandler implementations. * * @see #getConfigTimeLobHandler * @see com.ibatis.sqlmap.engine.type.TypeHandler * @see org.springframework.orm.ibatis.support.ClobStringTypeHandler * @see org.springframework.orm.ibatis.support.BlobByteArrayTypeHandler * @see org.springframework.orm.ibatis.support.BlobSerializableTypeHandler */ public void setLobHandler(LobHandler lobHandler) { this.lobHandler = lobHandler; } public void afterPropertiesSet() throws Exception { if (this.lobHandler != null) { // Make given LobHandler available for SqlMapClient configuration. // Do early because because mapping resource might refer to custom // types. configTimeLobHandlerHolder.set(this.lobHandler); } try { this.sqlMapClient = buildSqlMapClient(this.configLocations, this.mappingLocations, this.sqlMapClientProperties); // Tell the SqlMapClient to use the given DataSource, if any. if (this.dataSource != null) { TransactionConfig transactionConfig = (TransactionConfig) this.transactionConfigClass .newInstance(); DataSource dataSourceToUse = this.dataSource; if (this.useTransactionAwareDataSource && !(this.dataSource instanceof TransactionAwareDataSourceProxy)) { dataSourceToUse = new TransactionAwareDataSourceProxy(this.dataSource); } transactionConfig.setDataSource(dataSourceToUse); transactionConfig.initialize(this.transactionConfigProperties); applyTransactionConfig(this.sqlMapClient, transactionConfig); } } finally { if (this.lobHandler != null) { // Reset LobHandler holder. configTimeLobHandlerHolder.set(null); } } } /** * Build a SqlMapClient instance based on the given standard configuration. * <p> * The default implementation uses the standard iBATIS * {@link SqlMapClientBuilder} API to build a SqlMapClient instance based on * an InputStream (if possible, on iBATIS 2.3 and higher) or on a Reader (on * iBATIS up to version 2.2). * * @param configLocations * the config files to load from * @param properties * the SqlMapClient properties (if any) * @return the SqlMapClient instance (never <code>null</code>) * @throws IOException * if loading the config file failed * @throws NoSuchFieldException * @throws SecurityException * @throws IllegalAccessException * @throws IllegalArgumentException * @throws NoSuchMethodException * @throws InvocationTargetException * @see com.ibatis.sqlmap.client.SqlMapClientBuilder#buildSqlMapClient */ protected SqlMapClient buildSqlMapClient(Resource[] configLocations, Resource[] mappingLocations, Properties properties) throws IOException, SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { if (ObjectUtils.isEmpty(configLocations)) { throw new IllegalArgumentException( "At least 1 'configLocation' entry is required"); } SqlMapClient client = null; SqlMapConfigParser configParser = new SqlMapConfigParser(); for (Resource configLocation : configLocations) { InputStream is = configLocation.getInputStream(); try { client = configParser.parse(is, properties); } catch (RuntimeException ex) { throw new NestedIOException("Failed to parse config resource: " + configLocation, ex.getCause()); } } if (mappingLocations != null) { SqlMapParser mapParser = SqlMapParserFactory.createSqlMapParser(configParser); for (Resource mappingLocation : mappingLocations) { try { mapParser.parse(mappingLocation.getInputStream()); } catch (NodeletException ex) { throw new NestedIOException("Failed to parse mapping resource: " + mappingLocation, ex); } } } //*************其实只改这一点而已,为了方便他人,全source贴出************** //为了取sqlMapConfig,反射private的field Field stateField = configParser.getClass().getDeclaredField("state"); stateField.setAccessible(true); XmlParserState state = (XmlParserState) stateField.get(configParser); SqlMapConfiguration sqlMapConfig = state.getConfig(); //反射取设置cache的方法,执行 Method wireUpCacheModels = sqlMapConfig.getClass().getDeclaredMethod( "wireUpCacheModels"); wireUpCacheModels.setAccessible(true); wireUpCacheModels.invoke(sqlMapConfig); //************************************************************************* return client; } /** * Apply the given iBATIS TransactionConfig to the SqlMapClient. * <p> * The default implementation casts to ExtendedSqlMapClient, retrieves the * maximum number of concurrent transactions from the * SqlMapExecutorDelegate, and sets an iBATIS TransactionManager with the * given TransactionConfig. * * @param sqlMapClient * the SqlMapClient to apply the TransactionConfig to * @param transactionConfig * the iBATIS TransactionConfig to apply * @see com.ibatis.sqlmap.engine.impl.ExtendedSqlMapClient * @see com.ibatis.sqlmap.engine.impl.SqlMapExecutorDelegate#getMaxTransactions * @see com.ibatis.sqlmap.engine.impl.SqlMapExecutorDelegate#setTxManager */ protected void applyTransactionConfig(SqlMapClient sqlMapClient, TransactionConfig transactionConfig) { if (!(sqlMapClient instanceof ExtendedSqlMapClient)) { throw new IllegalArgumentException( "Cannot set TransactionConfig with DataSource for SqlMapClient if not of type " + "ExtendedSqlMapClient: " + sqlMapClient); } ExtendedSqlMapClient extendedClient = (ExtendedSqlMapClient) sqlMapClient; transactionConfig.setMaximumConcurrentTransactions(extendedClient.getDelegate() .getMaxTransactions()); extendedClient.getDelegate().setTxManager( new TransactionManager(transactionConfig)); } public SqlMapClient getObject() { return this.sqlMapClient; } public Class<? extends SqlMapClient> getObjectType() { return (this.sqlMapClient != null ? this.sqlMapClient.getClass() : SqlMapClient.class); } public boolean isSingleton() { return true; } /** * Inner class to avoid hard-coded iBATIS 2.3.2 dependency (XmlParserState * class). */ private static class SqlMapParserFactory { public static SqlMapParser createSqlMapParser(SqlMapConfigParser configParser) { // Ideally: XmlParserState state = configParser.getState(); // Should raise an enhancement request with iBATIS... XmlParserState state = null; try { Field stateField = SqlMapConfigParser.class.getDeclaredField("state"); stateField.setAccessible(true); state = (XmlParserState) stateField.get(configParser); } catch (Exception ex) { throw new IllegalStateException( "iBATIS 2.3.2 'state' field not found in SqlMapConfigParser class - " + "please upgrade to IBATIS 2.3.2 or higher in order to use the new 'mappingLocations' feature. " + ex); } return new SqlMapParser(state); } } }
修改Spring配置文件
<bean id="sqlMapClient" class="com.foo.xxx.MySqlMapClientFactoryBean"> <property name="configLocation" value="classpath:/com/foo/xxx/sqlMapConfig.xml" /> <property name="dataSource" ref="dataSource" /> <property name="mappingLocations" value="classpath*:/**/sqlmap/*SqlMap.xml" /> </bean>
至此,cache又工作如初了。
编后:
·反射会消耗,但仅仅在初始化时一次性消耗,还可以接受。
·iBatis的cache能力比较弱,但没太多要求的情况下是个省事的方案,聊胜于无。
·Mybatis3.0的cache暂时不用为好,EhCache选项本身还有bug,实在很无语。