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