版本:
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,实在很无语。