All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.integration.jdbc.StoredProcExecutor Maven / Gradle / Ivy

/*
 * Copyright 2002-2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.integration.jdbc;

import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.sql.DataSource;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.expression.BeanResolver;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.integration.expression.ExpressionUtils;
import org.springframework.integration.jdbc.storedproc.ProcedureParameter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.SqlInOutParameter;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcCall;
import org.springframework.jdbc.core.simple.SimpleJdbcCallOperations;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedMetric;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.CacheStats;
import com.google.common.cache.LoadingCache;


/**
 * This class is used by all Stored Procedure (Stored Function) components and
 * provides the core functionality to execute those.
 *
 * @author Gunnar Hillert
 * @author Artem Bilan
 * @author Gary Russell
 * @since 2.1
 *
 */
@ManagedResource
public class StoredProcExecutor implements BeanFactoryAware, InitializingBean {

	private static final boolean guavaPresent = ClassUtils.isPresent("com.google.common.cache.LoadingCache",
			StoredProcExecutor.class.getClassLoader());

	private volatile EvaluationContext evaluationContext;

	private volatile BeanFactory beanFactory = null;

	private volatile int jdbcCallOperationsCacheSize = 10;

	/**
	 * For {@code optional} Google Guava library in the CLASSPATH
	 */
	private volatile GuavaCacheWrapper guavaCacheWrapper;

	private final Object jdbcCallOperationsMapMonitor = new Object();

	private volatile Map jdbcCallOperationsMap;

	private volatile Expression storedProcedureNameExpression;

	/**
	 * For fully supported databases, the underlying  {@link SimpleJdbcCall} can
	 * retrieve the parameter information for the to be invoked Stored Procedure
	 * or Function from the JDBC Meta-data. However, if the used database does
	 * not support meta data lookups or if you like to provide customized
	 * parameter definitions, this flag can be set to 'true'. It defaults to 'false'.
	 */
	private volatile boolean ignoreColumnMetaData = false;

	/**
	 * If this variable is set to true then all results from a stored procedure call
	 * that don't have a corresponding SqlOutParameter declaration will be bypassed.
	 *
	 * The value is set on the underlying {@link JdbcTemplate}.
	 *
	 * Value defaults to true.
	 */
	private volatile boolean skipUndeclaredResults = true;

	/**
	 * If your database system is not fully supported by Spring and thus obtaining
	 * parameter definitions from the JDBC Meta-data is not possible, you must define
	 * the {@link SqlParameter} explicitly. See also {@link SqlOutParameter} and
	 * {@link SqlInOutParameter}.
	 */
	private volatile List sqlParameters = new ArrayList(0);

	/**
	 * By default bean properties of the passed in {@link Message} will be used
	 * as a source for the Stored Procedure's input parameters. By default a
	 * {@link BeanPropertySqlParameterSourceFactory} will be used.
	 *
	 * This may be sufficient for basic use cases. For more sophisticated options
	 * consider passing in one or more {@link ProcedureParameter}.
	 */
	private volatile SqlParameterSourceFactory sqlParameterSourceFactory = null;

	/**
	 * Indicates that whether only the payload of the passed-in {@link Message}
	 * shall be used as a source of parameters.
	 *
	 * @see #setUsePayloadAsParameterSource(boolean)
	 */
	private volatile Boolean usePayloadAsParameterSource = null;

	/**
	 * Custom Stored Procedure parameters that may contain static values
	 * or Strings representing an {@link Expression}.
	 */
	private volatile List procedureParameters;

	private volatile boolean isFunction = false;

	private volatile boolean returnValueRequired = false;

	private volatile Map> returningResultSetRowMappers = new HashMap>(0);

	private final DataSource dataSource;

	//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


	/**
	 * Constructor taking {@link DataSource} from which the DB Connection can be
	 * obtained.
	 *
	 * @param dataSource used to create a {@link SimpleJdbcCall} instance, must not be Null
	 */
	public StoredProcExecutor(DataSource dataSource) {

		Assert.notNull(dataSource, "dataSource must not be null.");
		this.dataSource = dataSource;
	}

	/**
	 * Verifies parameters, sets the parameters on {@link SimpleJdbcCallOperations}
	 * and ensures the appropriate {@link SqlParameterSourceFactory} is defined
	 * when {@link ProcedureParameter} are passed in.
	 */
	@Override
	public void afterPropertiesSet() {

		if (this.storedProcedureNameExpression == null) {
			throw new IllegalArgumentException("You must either provide a "
					+ "Stored Procedure Name or a Stored Procedure Name Expression.");
		}

		if (this.procedureParameters != null) {

			if (this.sqlParameterSourceFactory == null) {
				ExpressionEvaluatingSqlParameterSourceFactory expressionSourceFactory =
						new ExpressionEvaluatingSqlParameterSourceFactory();

				expressionSourceFactory.setBeanFactory(this.beanFactory);
				expressionSourceFactory.setStaticParameters(ProcedureParameter.convertStaticParameters(this.procedureParameters));
				expressionSourceFactory.setParameterExpressions(ProcedureParameter.convertExpressions(this.procedureParameters));

				this.sqlParameterSourceFactory = expressionSourceFactory;

			}
			else {

				if (!(this.sqlParameterSourceFactory instanceof ExpressionEvaluatingSqlParameterSourceFactory)) {
					throw new IllegalStateException("You are providing 'ProcedureParameters'. "
							+ "Was expecting the the provided sqlParameterSourceFactory "
							+ "to be an instance of 'ExpressionEvaluatingSqlParameterSourceFactory', "
							+ "however the provided one is of type '" + this.sqlParameterSourceFactory.getClass().getName() + "'");
				}

			}

			if (this.usePayloadAsParameterSource == null) {
				this.usePayloadAsParameterSource = false;
			}

		}
		else {

			if (this.sqlParameterSourceFactory == null) {
				this.sqlParameterSourceFactory = new BeanPropertySqlParameterSourceFactory();
			}

			if (this.usePayloadAsParameterSource == null) {
				this.usePayloadAsParameterSource = true;
			}

		}

		if (guavaPresent) {
			this.guavaCacheWrapper = new GuavaCacheWrapper(this, this.jdbcCallOperationsCacheSize);
		}
		else {
			this.jdbcCallOperationsMap =
					new LinkedHashMap(this.jdbcCallOperationsCacheSize + 1, 0.75f,
							true) {

						private static final long serialVersionUID = 3801124242820219131L;

						@Override
						protected boolean removeEldestEntry(Entry eldest) {
							return size() > StoredProcExecutor.this.jdbcCallOperationsCacheSize;
						}

					};
		}

		this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(this.beanFactory);
	}

	private SimpleJdbcCall createSimpleJdbcCall(String storedProcedureName) {

		final SimpleJdbcCall simpleJdbcCall = new SimpleJdbcCall(this.dataSource);

		if (this.isFunction) {
			simpleJdbcCall.withFunctionName(storedProcedureName);
		}
		else {
			simpleJdbcCall.withProcedureName(storedProcedureName);
		}

		if (this.ignoreColumnMetaData) {
			simpleJdbcCall.withoutProcedureColumnMetaDataAccess();
		}

		simpleJdbcCall.declareParameters(this.sqlParameters.toArray(new SqlParameter[this.sqlParameters.size()]));


		if (!this.returningResultSetRowMappers.isEmpty()) {

			for (Entry> mapEntry : this.returningResultSetRowMappers.entrySet()) {
				simpleJdbcCall.returningResultSet(mapEntry.getKey(), mapEntry.getValue());
			}
		}

		if (this.returnValueRequired) {
			simpleJdbcCall.withReturnValue();
		}

		simpleJdbcCall.getJdbcTemplate().setSkipUndeclaredResults(this.skipUndeclaredResults);

		return simpleJdbcCall;
	}

	/**
	 * Execute a Stored Procedure or Function - Use when no {@link Message} is
	 * available to extract {@link ProcedureParameter} values from it.
	 *
	 * @return Map containing the stored procedure results if any.
	 */
	public Map executeStoredProcedure() {
		return executeStoredProcedureInternal(new Object(), this.evaluateExpression(null));
	}

	/**
	 * Execute a Stored Procedure or Function - Use with {@link Message} is
	 * available to extract {@link ProcedureParameter} values from it.
	 *
	 * @param message A message.
	 * @return Map containing the stored procedure results if any.
	 */
	public Map executeStoredProcedure(Message message) {

		Assert.notNull(message, "The message parameter must not be null.");
		Assert.notNull(this.usePayloadAsParameterSource, "Property usePayloadAsParameterSource "
				+ "was Null. Did you call afterPropertiesSet()?");

		final Object input;

		if (this.usePayloadAsParameterSource) {
			input = message.getPayload();
		}
		else {
			input = message;
		}

		return executeStoredProcedureInternal(input, evaluateExpression(message));

	}

	private String evaluateExpression(Message message) {
		final String storedProcedureNameToUse = this.storedProcedureNameExpression.getValue(this.evaluationContext,
				message, String.class);

		Assert.hasText(storedProcedureNameToUse, String.format(
				"Unable to resolve Stored Procedure/Function name for the provided Expression '%s'.",
				this.storedProcedureNameExpression.getExpressionString()));
		return storedProcedureNameToUse;
	}

	/**
	 * Execute the Stored Procedure using the passed in {@link Message} as a source
	 * for parameters.
	 *
	 * @param input The message is used to extract parameters for the stored procedure.
	 * @return A map containing the return values from the Stored Procedure call if any.
	 */
	private Map executeStoredProcedureInternal(Object input, String storedProcedureName) {

		Assert.notNull(this.sqlParameterSourceFactory, "Property sqlParameterSourceFactory "
				+ "was Null. Did you call afterPropertiesSet()?");

		SimpleJdbcCallOperations localSimpleJdbcCall = obtainSimpleJdbcCall(storedProcedureName);

		SqlParameterSource storedProcedureParameterSource =
				this.sqlParameterSourceFactory.createParameterSource(input);

		return localSimpleJdbcCall.execute(storedProcedureParameterSource);

	}

	private SimpleJdbcCallOperations obtainSimpleJdbcCall(String storedProcedureName) {
		if (guavaPresent) {
			return this.guavaCacheWrapper.jdbcCallOperationsCache.getUnchecked(storedProcedureName);
		}
		else {
			SimpleJdbcCallOperations operations = this.jdbcCallOperationsMap.get(storedProcedureName);
			if (operations == null) {
				synchronized (this.jdbcCallOperationsMapMonitor) {
					operations = this.jdbcCallOperationsMap.get(storedProcedureName);
					if (operations == null) {
						operations = createSimpleJdbcCall(storedProcedureName);
						this.jdbcCallOperationsMap.put(storedProcedureName, operations);
					}
				}
			}
			return operations;
		}
	}

	//~~~~~Setters for Properties~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

	/**
	 * For fully supported databases, the underlying  {@link SimpleJdbcCall} can
	 * retrieve the parameter information for the to be invoked Stored Procedure
	 * from the JDBC Meta-data. However, if the used database does not support
	 * meta data lookups or if you like to provide customized parameter definitions,
	 * this flag can be set to 'true'. It defaults to 'false'.
	 *
	 * @param ignoreColumnMetaData true to ignore column metadata.
	 */
	public void setIgnoreColumnMetaData(boolean ignoreColumnMetaData) {
		this.ignoreColumnMetaData = ignoreColumnMetaData;
	}

	/**
	 * Custom Stored Procedure parameters that may contain static values
	 * or Strings representing an {@link Expression}.
	 *
	 * @param procedureParameters The parameters.
	 */
	public void setProcedureParameters(List procedureParameters) {

		Assert.notEmpty(procedureParameters, "procedureParameters must not be null or empty.");

		for (ProcedureParameter procedureParameter : procedureParameters) {
			Assert.notNull(procedureParameter, "The provided list (procedureParameters) cannot contain null values.");
		}

		this.procedureParameters = procedureParameters;

	}

	/**
	 * If you database system is not fully supported by Spring and thus obtaining
	 * parameter definitions from the JDBC Meta-data is not possible, you must define
	 * the {@link SqlParameter} explicitly.
	 *
	 * @param sqlParameters The parameters.
	 */
	public void setSqlParameters(List sqlParameters) {
		Assert.notEmpty(sqlParameters, "sqlParameters must not be null or empty.");

		for (SqlParameter sqlParameter : sqlParameters) {
			Assert.notNull(sqlParameter, "The provided list (sqlParameters) cannot contain null values.");
		}

		this.sqlParameters = sqlParameters;
	}

	/**
	 * Provides the ability to set a custom {@link SqlParameterSourceFactory}.
	 * Keep in mind that if {@link ProcedureParameter} are set explicitly and
	 * you would like to provide a custom {@link SqlParameterSourceFactory},
	 * then you must provide an instance of {@link ExpressionEvaluatingSqlParameterSourceFactory}.
	 *
	 * If not the SqlParameterSourceFactory will be replaced the default
	 * {@link ExpressionEvaluatingSqlParameterSourceFactory}.
	 *
	 * @param sqlParameterSourceFactory The paramtere source factory.
	 */
	public void setSqlParameterSourceFactory(SqlParameterSourceFactory sqlParameterSourceFactory) {
		Assert.notNull(sqlParameterSourceFactory, "sqlParameterSourceFactory must not be null.");
		this.sqlParameterSourceFactory = sqlParameterSourceFactory;
	}

	/**
	 * @return the name of the Stored Procedure or Function if set. Null otherwise.
	 * */
	@ManagedAttribute(defaultValue = "Null if not Set.")
	public String getStoredProcedureName() {
		return this.storedProcedureNameExpression instanceof LiteralExpression ?
				this.storedProcedureNameExpression.getValue(String.class) : null;
	}

	/**
	 * @return the Stored Procedure Name Expression as a String if set. Null otherwise.
	 * */
	@ManagedAttribute(defaultValue = "Null if not Set.")
	public String getStoredProcedureNameExpressionAsString() {
		return this.storedProcedureNameExpression != null
				? this.storedProcedureNameExpression.getExpressionString()
				: null;
	}

	/**
	 * The name of the Stored Procedure or Stored Function to be executed.
	 * If {@link StoredProcExecutor#isFunction} is set to "true", then this
	 * property specifies the Stored Function name.
	 *
	 * Alternatively you can also specify the Stored Procedure name via
	 * {@link StoredProcExecutor#setStoredProcedureNameExpression(Expression)}.
	 *
	 * E.g., that way you can specify the name of the Stored Procedure or Stored Function
	 * through {@link MessageHeaders}.
	 *
	 * @param storedProcedureName Must not be null and must not be empty
	 *
	 * @see StoredProcExecutor#setStoredProcedureNameExpression(Expression)
	 */
	public void setStoredProcedureName(String storedProcedureName) {
		Assert.hasText(storedProcedureName, "storedProcedureName must not be null and cannot be empty.");
		this.storedProcedureNameExpression = new LiteralExpression(storedProcedureName);
	}

	/**
	 * Using the {@link StoredProcExecutor#storedProcedureNameExpression} the
	 * {@link Message} can be used as source for the name of the
	 * Stored Procedure or Stored Function.
	 *
	 * If {@link StoredProcExecutor#isFunction} is set to "true", then this
	 * property specifies the Stored Function name.
	 *
	 * By providing a SpEL expression as value for this setter, a subset of the
	 * original payload, a header value or any other resolvable SpEL expression
	 * can be used as the basis for the Stored Procedure / Function.
	 *
	 * For the Expression evaluation the full message is available as the root object.
	 *
	 * For instance the following SpEL expressions (among others) are possible:
	 *
	 * 
    *
  • payload.foo
  • *
  • headers.foobar
  • *
  • new java.util.Date()
  • *
  • 'foo' + 'bar'
  • *
* * Alternatively you can also specify the Stored Procedure name via * {@link StoredProcExecutor#setStoredProcedureName(String)} * * @param storedProcedureNameExpression Must not be null. * */ public void setStoredProcedureNameExpression(Expression storedProcedureNameExpression) { Assert.notNull(storedProcedureNameExpression, "storedProcedureNameExpression must not be null."); this.storedProcedureNameExpression = storedProcedureNameExpression; } /** * If set to 'true', the payload of the Message will be used as a source for * providing parameters. If false the entire {@link Message} will be available * as a source for parameters. * * If no {@link ProcedureParameter} are passed in, this property will default to * true. This means that using a default {@link BeanPropertySqlParameterSourceFactory} * the bean properties of the payload will be used as a source for parameter * values for the to-be-executed Stored Procedure or Function. * * However, if {@link ProcedureParameter}s are passed in, then this property * will by default evaluate to false. {@link ProcedureParameter} * allow for SpEl Expressions to be provided and therefore it is highly * beneficial to have access to the entire {@link Message}. * * @param usePayloadAsParameterSource If false the entire {@link Message} is used as parameter source. */ public void setUsePayloadAsParameterSource(boolean usePayloadAsParameterSource) { this.usePayloadAsParameterSource = usePayloadAsParameterSource; } /** * Indicates whether a Stored Procedure or a Function is being executed. * The default value is false. * * @param isFunction If set to true an Sql Function is executed rather than a Stored Procedure. */ public void setIsFunction(boolean isFunction) { this.isFunction = isFunction; } /** * Indicates the procedure's return value should be included in the results * returned. * * @param returnValueRequired true to include the return value. */ public void setReturnValueRequired(boolean returnValueRequired) { this.returnValueRequired = returnValueRequired; } /** * If this variable is set to true then all results from a stored * procedure call that don't have a corresponding {@link SqlOutParameter} * declaration will be bypassed. * * E.g. Stored Procedures may return an update count value, even though your * Stored Procedure only declared a single result parameter. The exact behavior * depends on the used database. * * The value is set on the underlying {@link JdbcTemplate}. * * Only few developers will probably ever like to process update counts, thus * the value defaults to true. * * @param skipUndeclaredResults The boolean. * */ public void setSkipUndeclaredResults(boolean skipUndeclaredResults) { this.skipUndeclaredResults = skipUndeclaredResults; } /** * If the Stored Procedure returns ResultSets you may provide a map of * {@link RowMapper} to convert the {@link ResultSet} to meaningful objects. * * @param returningResultSetRowMappers The map may not be null and must not contain null values. */ public void setReturningResultSetRowMappers(Map> returningResultSetRowMappers) { Assert.notNull(returningResultSetRowMappers, "returningResultSetRowMappers must not be null."); for (RowMapper rowMapper : returningResultSetRowMappers.values()) { Assert.notNull(rowMapper, "The provided map cannot contain null values."); } this.returningResultSetRowMappers = returningResultSetRowMappers; } /** * Allows for the retrieval of metrics ({@link CacheStats}) for the * {@link GuavaCacheWrapper#jdbcCallOperationsCache}, which is used to store * instances of {@link SimpleJdbcCallOperations}. * * @return {@link CacheStats} object for {@link GuavaCacheWrapper#jdbcCallOperationsCache}. * Since Google Guava is an optional dependency for Spring Integration this method can't * return Guava {@link CacheStats} type directly because of some reflection manipulation * by the Spring bean definition phase. */ public Object getJdbcCallOperationsCacheStatistics() { if (!guavaPresent) { throw new UnsupportedOperationException("The Google Guava library isn't present in the classpath."); } return this.guavaCacheWrapper.jdbcCallOperationsCache.stats(); } /** * Allows for the retrieval of metrics ({@link CacheStats}) for the * {@link GuavaCacheWrapper#jdbcCallOperationsCache}. * * Provides the properties of {@link CacheStats} as a {@link Map}. This allows * for exposing the those properties easily via JMX. * * @return Map containing metrics of the JdbcCallOperationsCache * * @see StoredProcExecutor#getJdbcCallOperationsCacheStatistics() */ @ManagedMetric public Map getJdbcCallOperationsCacheStatisticsAsMap() { if (!guavaPresent) { throw new UnsupportedOperationException("The Google Guava library isn't present in the classpath."); } final CacheStats cacheStats = (CacheStats) getJdbcCallOperationsCacheStatistics(); final Map cacheStatistics = new HashMap(11); cacheStatistics.put("averageLoadPenalty", cacheStats.averageLoadPenalty()); cacheStatistics.put("evictionCount", cacheStats.evictionCount()); cacheStatistics.put("hitCount", cacheStats.hitCount()); cacheStatistics.put("hitRate", cacheStats.hitRate()); cacheStatistics.put("loadCount", cacheStats.loadCount()); cacheStatistics.put("loadExceptionCount", cacheStats.loadExceptionCount()); cacheStatistics.put("loadExceptionRate", cacheStats.loadExceptionRate()); cacheStatistics.put("loadSuccessCount", cacheStats.loadSuccessCount()); cacheStatistics.put("missCount", cacheStats.missCount()); cacheStatistics.put("missRate", cacheStats.missRate()); cacheStatistics.put("totalLoadTime", cacheStats.totalLoadTime()); return Collections.unmodifiableMap(cacheStatistics); } /** * Defines the maximum number of {@link SimpleJdbcCallOperations} * ({@link SimpleJdbcCall}) instances to be held by * {@link GuavaCacheWrapper#jdbcCallOperationsCache}. * * A value of zero will disable the cache. The default is 10. * * @see CacheBuilder#maximumSize(long) * @param jdbcCallOperationsCacheSize Must not be negative. */ public void setJdbcCallOperationsCacheSize(int jdbcCallOperationsCacheSize) { Assert.isTrue(jdbcCallOperationsCacheSize >= 0, "jdbcCallOperationsCacheSize must not be negative."); this.jdbcCallOperationsCacheSize = jdbcCallOperationsCacheSize; } /** * Allows to set the optional {@link BeanFactory} which is used to add a * {@link BeanResolver} to the {@link StandardEvaluationContext}. If not set * this property defaults to null. * * @param beanFactory If set must not be null. */ @Override public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; } /** * The lazy-load workaround class to avoid {@link NoClassDefFoundError} * for {@link CacheLoader} class, when Google Guava isn't present in the CLASSPATH. * * @since 4.2 */ private static final class GuavaCacheWrapper { private final LoadingCache jdbcCallOperationsCache; private GuavaCacheWrapper(final StoredProcExecutor executor, int size) { this.jdbcCallOperationsCache = CacheBuilder.newBuilder() .maximumSize(size) .recordStats() .build(new CacheLoader() { @Override public SimpleJdbcCallOperations load(String key) throws Exception { return executor.createSimpleJdbcCall(key); } }); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy