View Javadoc
1   /*
2    * SPDX-License-Identifier: Apache-2.0
3    * See LICENSE file for details.
4    *
5    * Copyright 2015-2026 the original author or authors.
6    */
7   package org.springframework.orm.ibatis;
8   
9   import com.ibatis.common.xml.NodeletException;
10  import com.ibatis.sqlmap.client.SqlMapClient;
11  import com.ibatis.sqlmap.client.SqlMapClientBuilder;
12  import com.ibatis.sqlmap.engine.builder.xml.SqlMapConfigParser;
13  import com.ibatis.sqlmap.engine.builder.xml.SqlMapParser;
14  import com.ibatis.sqlmap.engine.builder.xml.XmlParserState;
15  import com.ibatis.sqlmap.engine.impl.ExtendedSqlMapClient;
16  import com.ibatis.sqlmap.engine.transaction.TransactionConfig;
17  import com.ibatis.sqlmap.engine.transaction.TransactionManager;
18  import com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig;
19  
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.lang.reflect.Field;
23  import java.util.Properties;
24  
25  import javax.sql.DataSource;
26  
27  import org.springframework.beans.factory.FactoryBean;
28  import org.springframework.beans.factory.InitializingBean;
29  import org.springframework.core.io.Resource;
30  import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
31  import org.springframework.jdbc.support.lob.LobHandler;
32  import org.springframework.util.ObjectUtils;
33  
34  /**
35   * {@link org.springframework.beans.factory.FactoryBean} that creates an iBATIS
36   * {@link com.ibatis.sqlmap.client.SqlMapClient}. This is the usual way to set up a shared iBATIS SqlMapClient in a
37   * Spring application context; the SqlMapClient can then be passed to iBATIS-based DAOs via dependency injection.
38   * <p>
39   * Either {@link org.springframework.jdbc.datasource.DataSourceTransactionManager} or
40   * {@link org.springframework.transaction.jta.JtaTransactionManager} can be used for transaction demarcation in
41   * combination with a SqlMapClient, with JTA only necessary for transactions which span multiple databases.
42   * <p>
43   * Allows for specifying a DataSource at the SqlMapClient level. This is preferable to per-DAO DataSource references, as
44   * it allows for lazy loading and avoids repeated DataSource references in every DAO.
45   * <p>
46   * <b>Note:</b> As of Spring 2.5.5, this class (finally) requires iBATIS 2.3 or higher. The new "mappingLocations"
47   * feature requires iBATIS 2.3.2.
48   *
49   * @author Juergen Hoeller
50   *
51   * @since 24.02.2004
52   *
53   * @see #setConfigLocation
54   * @see #setDataSource
55   * @see SqlMapClientTemplate#setSqlMapClient
56   * @see SqlMapClientTemplate#setDataSource
57   *
58   * @deprecated as of Spring 3.2, in favor of the native Spring support in the Mybatis follow-up project
59   *             (https://mybatis.org/)
60   */
61  @Deprecated
62  public class SqlMapClientFactoryBean implements FactoryBean<SqlMapClient>, InitializingBean {
63  
64    private static final ThreadLocal<LobHandler> configTimeLobHandlerHolder = new ThreadLocal<>();
65  
66    /**
67     * Return the LobHandler for the currently configured iBATIS SqlMapClient, to be used by TypeHandler implementations
68     * like ClobStringTypeHandler.
69     * <p>
70     * This instance will be set before initialization of the corresponding SqlMapClient, and reset immediately
71     * afterwards. It is thus only available during configuration.
72     *
73     * @see #setLobHandler
74     * @see org.springframework.orm.ibatis.support.ClobStringTypeHandler
75     * @see org.springframework.orm.ibatis.support.BlobByteArrayTypeHandler
76     * @see org.springframework.orm.ibatis.support.BlobSerializableTypeHandler
77     */
78    public static LobHandler getConfigTimeLobHandler() {
79      return configTimeLobHandlerHolder.get();
80    }
81  
82    private Resource[] configLocations;
83  
84    private Resource[] mappingLocations;
85  
86    private Properties sqlMapClientProperties;
87  
88    private DataSource dataSource;
89  
90    private boolean useTransactionAwareDataSource = true;
91  
92    private Class<? extends TransactionConfig> transactionConfigClass = ExternalTransactionConfig.class;
93  
94    private Properties transactionConfigProperties;
95  
96    private LobHandler lobHandler;
97  
98    private SqlMapClient sqlMapClient;
99  
100   public SqlMapClientFactoryBean() {
101     this.transactionConfigProperties = new Properties();
102     this.transactionConfigProperties.setProperty("SetAutoCommitAllowed", "false");
103   }
104 
105   /**
106    * Set the location of the iBATIS SqlMapClient config file. A typical value is "WEB-INF/sql-map-config.xml".
107    *
108    * @see #setConfigLocations
109    */
110   public void setConfigLocation(Resource configLocation) {
111     this.configLocations = (configLocation != null ? new Resource[] { configLocation } : null);
112   }
113 
114   /**
115    * Set multiple locations of iBATIS SqlMapClient config files that are going to be merged into one unified
116    * configuration at runtime.
117    *
118    * @param configLocations
119    *          the config locations
120    */
121   public void setConfigLocations(Resource[] configLocations) {
122     this.configLocations = configLocations;
123   }
124 
125   /**
126    * Set locations of iBATIS sql-map mapping files that are going to be merged into the SqlMapClient configuration at
127    * runtime.
128    * <p>
129    * This is an alternative to specifying "&lt;sqlMap&gt;" entries in a sql-map-client config file. This property being
130    * based on Spring's resource abstraction also allows for specifying resource patterns here: e.g. "/myApp/*-map.xml".
131    * <p>
132    * Note that this feature requires iBATIS 2.3.2; it will not work with any previous iBATIS version.
133    *
134    * @param mappingLocations
135    *          the mapping locations
136    */
137   public void setMappingLocations(Resource[] mappingLocations) {
138     this.mappingLocations = mappingLocations;
139   }
140 
141   /**
142    * Set optional properties to be passed into the SqlMapClientBuilder, as alternative to a {@code &lt;properties&gt;}
143    * tag in the sql-map-config.xml file. Will be used to resolve placeholders in the config file.
144    *
145    * @param sqlMapClientProperties
146    *          the client properties
147    *
148    * @see #setConfigLocation
149    * @see com.ibatis.sqlmap.client.SqlMapClientBuilder#buildSqlMapClient(java.io.InputStream, java.util.Properties)
150    */
151   public void setSqlMapClientProperties(Properties sqlMapClientProperties) {
152     this.sqlMapClientProperties = sqlMapClientProperties;
153   }
154 
155   /**
156    * Set the DataSource to be used by iBATIS SQL Maps. This will be passed to the SqlMapClient as part of a
157    * TransactionConfig instance.
158    * <p>
159    * If specified, this will override corresponding settings in the SqlMapClient properties. Usually, you will specify
160    * DataSource and transaction configuration <i>either</i> here <i>or</i> in SqlMapClient properties.
161    * <p>
162    * Specifying a DataSource for the SqlMapClient rather than for each individual DAO allows for lazy loading, for
163    * example when using PaginatedList results.
164    * <p>
165    * With a DataSource passed in here, you don't need to specify one for each DAO. Passing the SqlMapClient to the DAOs
166    * is enough, as it already carries a DataSource. Thus, it's recommended to specify the DataSource at this central
167    * location only.
168    * <p>
169    * Thanks to Brandon Goodin from the iBATIS team for the hint on how to make this work with Spring's integration
170    * strategy!
171    *
172    * @param dataSource
173    *          the data source
174    *
175    * @see #setTransactionConfigClass
176    * @see #setTransactionConfigProperties
177    * @see com.ibatis.sqlmap.client.SqlMapClient#getDataSource
178    * @see SqlMapClientTemplate#setDataSource
179    */
180   public void setDataSource(DataSource dataSource) {
181     this.dataSource = dataSource;
182   }
183 
184   /**
185    * Set whether to use a transaction-aware DataSource for the SqlMapClient, i.e. whether to automatically wrap the
186    * passed-in DataSource with Spring's TransactionAwareDataSourceProxy.
187    * <p>
188    * Default is "true": When the SqlMapClient performs direct database operations outside of Spring's
189    * SqlMapClientTemplate (for example, lazy loading or direct SqlMapClient access), it will still participate in active
190    * Spring-managed transactions.
191    * <p>
192    * As a further effect, using a transaction-aware DataSource will apply remaining transaction timeouts to all created
193    * JDBC Statements. This means that all operations performed by the SqlMapClient will automatically participate in
194    * Spring-managed transaction timeouts.
195    * <p>
196    * Turn this flag off to get raw DataSource handling, without Spring transaction checks. Operations on Spring's
197    * SqlMapClientTemplate will still detect Spring-managed transactions, but lazy loading or direct SqlMapClient access
198    * won't.
199    *
200    * @param useTransactionAwareDataSource
201    *          whether to use transaction aware datasource
202    *
203    * @see #setDataSource
204    * @see org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy
205    * @see org.springframework.jdbc.datasource.DataSourceTransactionManager
206    * @see SqlMapClientTemplate
207    * @see com.ibatis.sqlmap.client.SqlMapClient
208    */
209   public void setUseTransactionAwareDataSource(boolean useTransactionAwareDataSource) {
210     this.useTransactionAwareDataSource = useTransactionAwareDataSource;
211   }
212 
213   /**
214    * Set the iBATIS TransactionConfig class to use. Default is
215    * {@code com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig}.
216    * <p>
217    * Will only get applied when using a Spring-managed DataSource. An instance of this class will get populated with the
218    * given DataSource and initialized with the given properties.
219    * <p>
220    * The default ExternalTransactionConfig is appropriate if there is external transaction management that the
221    * SqlMapClient should participate in: be it Spring transaction management, EJB CMT or plain JTA. This should be the
222    * typical scenario. If there is no active transaction, SqlMapClient operations will execute SQL statements
223    * non-transactionally.
224    * <p>
225    * JdbcTransactionConfig or JakartaTransactionConfig/JavaxTransactionConfig is only necessary when using the iBATIS
226    * SqlMapTransactionManager API instead of external transactions. If there is no explicit transaction, SqlMapClient
227    * operations will automatically start a transaction for their own scope (in contrast to the external transaction
228    * mode, see above).
229    * <p>
230    * <b>It is strongly recommended to use iBATIS SQL Maps with Spring transaction management (or EJB CMT).</b> In this
231    * case, the default ExternalTransactionConfig is fine. Lazy loading and SQL Maps operations without explicit
232    * transaction demarcation will execute non-transactionally.
233    * <p>
234    * Even with Spring transaction management, it might be desirable to specify JdbcTransactionConfig: This will still
235    * participate in existing Spring-managed transactions, but lazy loading and operations without explicit transaction
236    * demaration will execute in their own auto-started transactions. However, this is usually not necessary.
237    *
238    * @param transactionConfigClass
239    *          the transaction config class
240    *
241    * @see #setDataSource
242    * @see #setTransactionConfigProperties
243    * @see com.ibatis.sqlmap.engine.transaction.TransactionConfig
244    * @see com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig
245    * @see com.ibatis.sqlmap.engine.transaction.jdbc.JdbcTransactionConfig
246    * @see com.ibatis.sqlmap.engine.transaction.jta.JakartaTransactionConfig
247    * @see com.ibatis.sqlmap.engine.transaction.jta.JavaxTransactionConfig
248    * @see com.ibatis.sqlmap.client.SqlMapTransactionManager
249    */
250   public void setTransactionConfigClass(Class<? extends TransactionConfig> transactionConfigClass) {
251     if (transactionConfigClass == null || !TransactionConfig.class.isAssignableFrom(transactionConfigClass)) {
252       throw new IllegalArgumentException("Invalid transactionConfigClass: does not implement "
253           + "com.ibatis.sqlmap.engine.transaction.TransactionConfig");
254     }
255     this.transactionConfigClass = transactionConfigClass;
256   }
257 
258   /**
259    * Set properties to be passed to the TransactionConfig instance used by this SqlMapClient. Supported properties
260    * depend on the concrete TransactionConfig implementation used:
261    * <p>
262    * <ul>
263    * <li><b>ExternalTransactionConfig</b> supports "DefaultAutoCommit" (default: false) and "SetAutoCommitAllowed"
264    * (default: true). Note that Spring uses SetAutoCommitAllowed = false as default, in contrast to the iBATIS default,
265    * to always keep the original autoCommit value as provided by the connection pool.
266    * <li><b>JdbcTransactionConfig</b> does not supported any properties.
267    * <li><b>JakartaTransactionConfig</b> (jakarta namespace) and <b>JavaxTransactionConfig</b> (javax namespace) both
268    * support "UserTransaction" (no default), specifying the JNDI location of the JTA UserTransaction (usually
269    * "java:comp/UserTransaction").
270    * </ul>
271    *
272    * @param transactionConfigProperties
273    *          the transaction config properties
274    *
275    * @see com.ibatis.sqlmap.engine.transaction.TransactionConfig#initialize
276    * @see com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig
277    * @see com.ibatis.sqlmap.engine.transaction.jdbc.JdbcTransactionConfig
278    * @see com.ibatis.sqlmap.engine.transaction.jta.JakartaTransactionConfig
279    * @see com.ibatis.sqlmap.engine.transaction.jta.JavaxTransactionConfig
280    */
281   public void setTransactionConfigProperties(Properties transactionConfigProperties) {
282     this.transactionConfigProperties = transactionConfigProperties;
283   }
284 
285   /**
286    * Set the LobHandler to be used by the SqlMapClient. Will be exposed at config time for TypeHandler implementations.
287    *
288    * @param lobHandler
289    *          the lob handler
290    *
291    * @see #getConfigTimeLobHandler
292    * @see com.ibatis.sqlmap.engine.type.TypeHandler
293    * @see org.springframework.orm.ibatis.support.ClobStringTypeHandler
294    * @see org.springframework.orm.ibatis.support.BlobByteArrayTypeHandler
295    * @see org.springframework.orm.ibatis.support.BlobSerializableTypeHandler
296    */
297   public void setLobHandler(LobHandler lobHandler) {
298     this.lobHandler = lobHandler;
299   }
300 
301   @Override
302   public void afterPropertiesSet() throws Exception {
303     if (this.lobHandler != null) {
304       // Make given LobHandler available for SqlMapClient configuration.
305       // Do early because mapping resource might refer to custom types.
306       configTimeLobHandlerHolder.set(this.lobHandler);
307     }
308 
309     try {
310       this.sqlMapClient = buildSqlMapClient(this.configLocations, this.mappingLocations, this.sqlMapClientProperties);
311 
312       // Tell the SqlMapClient to use the given DataSource, if any.
313       if (this.dataSource != null) {
314         TransactionConfig transactionConfig = this.transactionConfigClass.getDeclaredConstructor().newInstance();
315         DataSource dataSourceToUse = this.dataSource;
316         if (this.useTransactionAwareDataSource && !(this.dataSource instanceof TransactionAwareDataSourceProxy)) {
317           dataSourceToUse = new TransactionAwareDataSourceProxy(this.dataSource);
318         }
319         transactionConfig.setDataSource(dataSourceToUse);
320         transactionConfig.setProperties(this.transactionConfigProperties);
321         applyTransactionConfig(this.sqlMapClient, transactionConfig);
322       }
323     } finally {
324       if (this.lobHandler != null) {
325         // Reset LobHandler holder.
326         configTimeLobHandlerHolder.remove();
327       }
328     }
329   }
330 
331   /**
332    * Build a SqlMapClient instance based on the given standard configuration.
333    * <p>
334    * The default implementation uses the standard iBATIS {@link SqlMapClientBuilder} API to build a SqlMapClient
335    * instance based on an InputStream.
336    *
337    * @param configLocations
338    *          the config files to load from
339    * @param mappingLocations
340    *          the mapping files to load from
341    * @param properties
342    *          the SqlMapClient properties (if any)
343    *
344    * @return the SqlMapClient instance (never {@code null})
345    *
346    * @throws IOException
347    *           if loading the config file failed
348    *
349    * @see com.ibatis.sqlmap.client.SqlMapClientBuilder#buildSqlMapClient
350    */
351   protected SqlMapClient buildSqlMapClient(Resource[] configLocations, Resource[] mappingLocations,
352       Properties properties) throws IOException {
353 
354     if (ObjectUtils.isEmpty(configLocations)) {
355       throw new IllegalArgumentException("At least 1 'configLocation' entry is required");
356     }
357 
358     SqlMapClient client = null;
359     SqlMapConfigParser configParser = new SqlMapConfigParser();
360     for (Resource configLocation : configLocations) {
361       InputStream is = configLocation.getInputStream();
362       try {
363         client = configParser.parse(is, properties);
364       } catch (RuntimeException ex) {
365         throw new IOException("Failed to parse config resource: " + configLocation, ex.getCause());
366       }
367     }
368 
369     if (mappingLocations != null) {
370       SqlMapParser mapParser = SqlMapParserFactory.createSqlMapParser(configParser);
371       for (Resource mappingLocation : mappingLocations) {
372         try {
373           mapParser.parse(mappingLocation.getInputStream());
374         } catch (NodeletException ex) {
375           throw new IOException("Failed to parse mapping resource: " + mappingLocation, ex);
376         }
377       }
378     }
379 
380     return client;
381   }
382 
383   /**
384    * Apply the given iBATIS TransactionConfig to the SqlMapClient.
385    * <p>
386    * The default implementation casts to ExtendedSqlMapClient, retrieves the maximum number of concurrent transactions
387    * from the SqlMapExecutorDelegate, and sets an iBATIS TransactionManager with the given TransactionConfig.
388    *
389    * @param sqlMapClient
390    *          the SqlMapClient to apply the TransactionConfig to
391    * @param transactionConfig
392    *          the iBATIS TransactionConfig to apply
393    *
394    * @see com.ibatis.sqlmap.engine.impl.ExtendedSqlMapClient
395    * @see com.ibatis.sqlmap.engine.impl.SqlMapExecutorDelegate#getMaxTransactions
396    * @see com.ibatis.sqlmap.engine.impl.SqlMapExecutorDelegate#setTxManager
397    */
398   protected void applyTransactionConfig(SqlMapClient sqlMapClient, TransactionConfig transactionConfig) {
399     if (!(sqlMapClient instanceof ExtendedSqlMapClient)) {
400       throw new IllegalArgumentException("Cannot set TransactionConfig with DataSource for SqlMapClient if not of type "
401           + "ExtendedSqlMapClient: " + sqlMapClient);
402     }
403     ExtendedSqlMapClient extendedClient = (ExtendedSqlMapClient) sqlMapClient;
404     extendedClient.getDelegate().setTxManager(new TransactionManager(transactionConfig));
405   }
406 
407   @Override
408   public SqlMapClient getObject() {
409     return this.sqlMapClient;
410   }
411 
412   @Override
413   public Class<? extends SqlMapClient> getObjectType() {
414     return (this.sqlMapClient != null ? this.sqlMapClient.getClass() : SqlMapClient.class);
415   }
416 
417   @Override
418   public boolean isSingleton() {
419     return true;
420   }
421 
422   /**
423    * Inner class to avoid hard-coded iBATIS 2.3.2 dependency (XmlParserState class).
424    */
425   private static class SqlMapParserFactory {
426 
427     public static SqlMapParser createSqlMapParser(SqlMapConfigParser configParser) {
428       // Ideally: XmlParserState state = configParser.getState();
429       XmlParserState state;
430       try {
431         Field stateField = SqlMapConfigParser.class.getDeclaredField("state");
432         stateField.setAccessible(true);
433         state = (XmlParserState) stateField.get(configParser);
434       } catch (Exception ex) {
435         throw new IllegalStateException("iBATIS 2.3.2 'state' field not found in SqlMapConfigParser class - "
436             + "please upgrade to IBATIS 2.3.2 or higher in order to use the new 'mappingLocations' feature. " + ex);
437       }
438       return new SqlMapParser(state);
439     }
440   }
441 
442 }