INT-3401: Make Google Guava an Optional Dep

JIRA: https://jira.spring.io/browse/INT-3401

Add Google Guava CLASSPATH check for the `StoredProcExecutor`
and use `ConcurrentHashMap` as an alternative to the Guava's `Cache`,
when Guava isn't presented in the CLASSPATH

Addressing PR comments

PR Comments

PR Comments:

Introduce an internal static `StoredProcExecutor.GuavaCacheWrapper` class
to avoid the eager class loading for the `CacheLoader` anonymous class
This commit is contained in:
Artem Bilan
2015-07-15 20:46:42 -04:00
committed by Gary Russell
parent 9944e54ce5
commit 32e23d1642
3 changed files with 148 additions and 69 deletions

View File

@@ -403,7 +403,7 @@ project('spring-integration-jdbc') {
dependencies {
compile project(":spring-integration-core")
compile "org.springframework:spring-jdbc:$springVersion"
compile "com.google.guava:guava:$guavaVersion"
compile ("com.google.guava:guava:$guavaVersion", optional)
testCompile "com.h2database:h2:$h2Version"
testCompile "org.hsqldb:hsqldb:$hsqldbVersion"

View File

@@ -12,14 +12,15 @@
*/
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;
@@ -47,6 +48,7 @@ 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;
@@ -69,6 +71,9 @@ import com.google.common.cache.LoadingCache;
@IntegrationManagedResource
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;
@@ -76,9 +81,13 @@ public class StoredProcExecutor implements BeanFactoryAware, InitializingBean {
private volatile int jdbcCallOperationsCacheSize = 10;
/**
* Uses the {@link SimpleJdbcCall} implementation for executing Stored Procedures.
* For {@code optional} Google Guava library in the CLASSPATH
*/
private volatile LoadingCache<String, SimpleJdbcCallOperations> jdbcCallOperationsCache = null;
private volatile GuavaCacheWrapper guavaCacheWrapper;
private final Object jdbcCallOperationsMapMonitor = new Object();
private volatile Map<String, SimpleJdbcCallOperations> jdbcCallOperationsMap;
private volatile Expression storedProcedureNameExpression;
@@ -134,7 +143,9 @@ public class StoredProcExecutor implements BeanFactoryAware, InitializingBean {
private volatile List<ProcedureParameter>procedureParameters;
private volatile boolean isFunction = false;
private volatile boolean returnValueRequired = false;
private volatile Map<String, RowMapper<?>> returningResultSetRowMappers = new HashMap<String, RowMapper<?>>(0);
private final DataSource dataSource;
@@ -152,7 +163,6 @@ public class StoredProcExecutor implements BeanFactoryAware, InitializingBean {
Assert.notNull(dataSource, "dataSource must not be null.");
this.dataSource = dataSource;
}
/**
@@ -210,24 +220,25 @@ public class StoredProcExecutor implements BeanFactoryAware, InitializingBean {
}
jdbcCallOperationsCache = CacheBuilder.newBuilder()
.maximumSize(jdbcCallOperationsCacheSize)
.recordStats()
.build(new CacheLoader<String, SimpleJdbcCallOperations>() {
@Override
public SimpleJdbcCall load(String storedProcedureName) {
return createSimpleJdbcCall(storedProcedureName);
}
});
if (guavaPresent) {
this.guavaCacheWrapper = new GuavaCacheWrapper(this, this.jdbcCallOperationsCacheSize);
}
else {
this.jdbcCallOperationsMap =
new LinkedHashMap<String, SimpleJdbcCallOperations>(this.jdbcCallOperationsCacheSize + 1, 0.75f,
true) {
if (this.storedProcedureNameExpression instanceof LiteralExpression) {
String storedProcedureName = this.storedProcedureNameExpression.getValue(String.class);
final SimpleJdbcCall simpleJdbcCall = createSimpleJdbcCall(storedProcedureName);
this.jdbcCallOperationsCache.put(storedProcedureName, simpleJdbcCall);
private static final long serialVersionUID = 3801124242820219131L;
@Override
protected boolean removeEldestEntry(Entry<String, SimpleJdbcCallOperations> eldest) {
return size() > jdbcCallOperationsCacheSize;
}
};
}
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(beanFactory);
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(this.beanFactory);
}
private SimpleJdbcCall createSimpleJdbcCall(String storedProcedureName) {
@@ -314,7 +325,7 @@ public class StoredProcExecutor implements BeanFactoryAware, InitializingBean {
* Execute the Stored Procedure using the passed in {@link Message} as a source
* for parameters.
*
* @param message The message is used to extract parameters for the stored procedure.
* @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<String, Object> executeStoredProcedureInternal(Object input, String storedProcedureName) {
@@ -322,15 +333,34 @@ public class StoredProcExecutor implements BeanFactoryAware, InitializingBean {
Assert.notNull(sqlParameterSourceFactory, "Property sqlParameterSourceFactory "
+ "was Null. Did you call afterPropertiesSet()?");
SimpleJdbcCallOperations localSimpleJdbcCall = this.jdbcCallOperationsCache.getUnchecked(storedProcedureName);
SimpleJdbcCallOperations localSimpleJdbcCall = obtainSimpleJdbcCall(storedProcedureName);
SqlParameterSource storedProcedureParameterSource =
sqlParameterSourceFactory.createParameterSource(input);
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~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/**
@@ -548,19 +578,25 @@ public class StoredProcExecutor implements BeanFactoryAware, InitializingBean {
}
/**
* Allows for the retrieval of metrics ({@link CacheStats}}) for the
* {@link StoredProcExecutor#jdbcCallOperationsCache}, which is used to store
* Allows for the retrieval of metrics ({@link CacheStats}) for the
* {@link GuavaCacheWrapper#jdbcCallOperationsCache}, which is used to store
* instances of {@link SimpleJdbcCallOperations}.
*
* @return Cache statistics for {@link StoredProcExecutor#jdbcCallOperationsCache}
* @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 CacheStats getJdbcCallOperationsCacheStatistics() {
return this.jdbcCallOperationsCache.stats();
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 StoredProcExecutor#jdbcCallOperationsCache}.
* 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.
@@ -571,7 +607,10 @@ public class StoredProcExecutor implements BeanFactoryAware, InitializingBean {
*/
@ManagedMetric
public Map<String, Object> getJdbcCallOperationsCacheStatisticsAsMap() {
final CacheStats cacheStats = this.getJdbcCallOperationsCacheStatistics();
if (!guavaPresent) {
throw new UnsupportedOperationException("The Google Guava library isn't present in the classpath.");
}
final CacheStats cacheStats = (CacheStats) getJdbcCallOperationsCacheStatistics();
final Map<String, Object> cacheStatistics = new HashMap<String, Object>(11);
cacheStatistics.put("averageLoadPenalty", cacheStats.averageLoadPenalty());
cacheStatistics.put("evictionCount", cacheStats.evictionCount());
@@ -590,7 +629,7 @@ public class StoredProcExecutor implements BeanFactoryAware, InitializingBean {
/**
* Defines the maximum number of {@link SimpleJdbcCallOperations}
* ({@link SimpleJdbcCall}) instances to be held by
* {@link StoredProcExecutor#jdbcCallOperationsCache}.
* {@link GuavaCacheWrapper#jdbcCallOperationsCache}.
*
* A value of zero will disable the cache. The default is 10.
*
@@ -611,8 +650,35 @@ public class StoredProcExecutor implements BeanFactoryAware, InitializingBean {
*/
@Override
public void setBeanFactory(BeanFactory beanFactory) {
Assert.notNull(returningResultSetRowMappers, "returningResultSetRowMappers must not be null.");
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 class GuavaCacheWrapper {
private final LoadingCache<String, SimpleJdbcCallOperations> jdbcCallOperationsCache;
private GuavaCacheWrapper(final StoredProcExecutor executor, int size) {
this.jdbcCallOperationsCache = CacheBuilder.newBuilder()
.maximumSize(size)
.recordStats()
.build(new CacheLoader<String, SimpleJdbcCallOperations>() {
@Override
public SimpleJdbcCallOperations load(String key) throws Exception {
return executor.createSimpleJdbcCall(key);
}
});
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@@ -16,7 +16,8 @@
package org.springframework.integration.jdbc;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.util.ArrayList;
import java.util.Collection;
@@ -71,32 +72,38 @@ public class StoredProcJmxManagedBeanTests {
// MessageHandler
final Set<ObjectName> messageHandlerObjectNames = server.queryNames(ObjectName.getInstance("org.springframework.integration.jdbc.test:name=outboundChannelAdapter.adapter.storedProcExecutor,*"), null);
final Set<ObjectName> messageHandlerObjectNames = server.queryNames(
ObjectName.getInstance("org.springframework.integration.jdbc.test:name=outboundChannelAdapter.adapter.storedProcExecutor,*"),
null);
assertEquals(1, messageHandlerObjectNames.size());
ObjectName messageHandlerObjectName = messageHandlerObjectNames.iterator().next();
Map<String, Object> messageHandlerCacheStatistics = (Map<String, Object>) server.getAttribute(messageHandlerObjectName, "JdbcCallOperationsCacheStatisticsAsMap");
Map<String, Object> messageHandlerCacheStatistics =
(Map<String, Object>) server.getAttribute(messageHandlerObjectName, "JdbcCallOperationsCacheStatisticsAsMap");
assertEquals(11, messageHandlerCacheStatistics.size());
assertEquals(Long.valueOf(0), messageHandlerCacheStatistics.get("hitCount"));
assertEquals(Long.valueOf(0), messageHandlerCacheStatistics.get("loadCount"));
assertEquals(Long.valueOf(0), messageHandlerCacheStatistics.get("loadExceptionCount"));
assertEquals(Long.valueOf(0), messageHandlerCacheStatistics.get("loadSuccessCount"));
assertEquals(Long.valueOf(0), messageHandlerCacheStatistics.get("missCount"));
assertEquals(0L, messageHandlerCacheStatistics.get("hitCount"));
assertEquals(0L, messageHandlerCacheStatistics.get("loadCount"));
assertEquals(0L, messageHandlerCacheStatistics.get("loadExceptionCount"));
assertEquals(0L, messageHandlerCacheStatistics.get("loadSuccessCount"));
assertEquals(0L, messageHandlerCacheStatistics.get("missCount"));
// StoredProcOutboundGateway
final Set<ObjectName> storedProcOutboundGatewayObjectNames = server.queryNames(ObjectName.getInstance("org.springframework.integration.jdbc.test:name=my gateway.storedProcExecutor,*"), null);
final Set<ObjectName> storedProcOutboundGatewayObjectNames = server.queryNames(
ObjectName.getInstance("org.springframework.integration.jdbc.test:name=my gateway.storedProcExecutor,*"),
null);
assertEquals(1, storedProcOutboundGatewayObjectNames.size());
ObjectName storedProcOutboundGatewayObjectName = storedProcOutboundGatewayObjectNames.iterator().next();
Map<String, Object> storedProcOutboundGatewayCacheStatistics = (Map<String, Object>) server.getAttribute(storedProcOutboundGatewayObjectName, "JdbcCallOperationsCacheStatisticsAsMap");
Map<String, Object> storedProcOutboundGatewayCacheStatistics =
(Map<String, Object>) server.getAttribute(storedProcOutboundGatewayObjectName, "JdbcCallOperationsCacheStatisticsAsMap");
assertEquals(11, messageHandlerCacheStatistics.size());
assertEquals(Long.valueOf(0), storedProcOutboundGatewayCacheStatistics.get("hitCount"));
assertEquals(Long.valueOf(0), storedProcOutboundGatewayCacheStatistics.get("loadCount"));
assertEquals(Long.valueOf(0), storedProcOutboundGatewayCacheStatistics.get("loadExceptionCount"));
assertEquals(Long.valueOf(0), storedProcOutboundGatewayCacheStatistics.get("loadSuccessCount"));
assertEquals(Long.valueOf(0), storedProcOutboundGatewayCacheStatistics.get("missCount"));
assertEquals(0L, storedProcOutboundGatewayCacheStatistics.get("hitCount"));
assertEquals(0L, storedProcOutboundGatewayCacheStatistics.get("loadCount"));
assertEquals(0L, storedProcOutboundGatewayCacheStatistics.get("loadExceptionCount"));
assertEquals(0L, storedProcOutboundGatewayCacheStatistics.get("loadSuccessCount"));
assertEquals(0L, storedProcOutboundGatewayCacheStatistics.get("missCount"));
// StoredProcPollingChannelAdapter
@@ -107,11 +114,11 @@ public class StoredProcJmxManagedBeanTests {
assertEquals(11, storedProcPollingChannelAdapterCacheStatistics.size());
assertEquals(Long.valueOf(0), storedProcPollingChannelAdapterCacheStatistics.get("hitCount"));
assertEquals(Long.valueOf(0), storedProcPollingChannelAdapterCacheStatistics.get("loadCount"));
assertEquals(Long.valueOf(0), storedProcPollingChannelAdapterCacheStatistics.get("loadExceptionCount"));
assertEquals(Long.valueOf(0), storedProcPollingChannelAdapterCacheStatistics.get("loadSuccessCount"));
assertEquals(Long.valueOf(0), storedProcPollingChannelAdapterCacheStatistics.get("missCount"));
assertEquals(0L, storedProcPollingChannelAdapterCacheStatistics.get("hitCount"));
assertEquals(0L, storedProcPollingChannelAdapterCacheStatistics.get("loadCount"));
assertEquals(0L, storedProcPollingChannelAdapterCacheStatistics.get("loadExceptionCount"));
assertEquals(0L, storedProcPollingChannelAdapterCacheStatistics.get("loadSuccessCount"));
assertEquals(0L, storedProcPollingChannelAdapterCacheStatistics.get("missCount"));
}
@@ -124,18 +131,21 @@ public class StoredProcJmxManagedBeanTests {
final MBeanServer server = servers.iterator().next();
final Set<ObjectName> objectNames = server.queryNames(ObjectName.getInstance("org.springframework.integration.jdbc.test:name=my gateway.storedProcExecutor,*"), null);
final Set<ObjectName> objectNames = server.queryNames(
ObjectName.getInstance("org.springframework.integration.jdbc.test:name=my gateway.storedProcExecutor,*"),
null);
assertEquals(1, objectNames.size());
ObjectName name = objectNames.iterator().next();
Map<String, Object> cacheStatistics = (Map<String, Object>) server.getAttribute(name, "JdbcCallOperationsCacheStatisticsAsMap");
Map<String, Object> cacheStatistics =
(Map<String, Object>) server.getAttribute(name, "JdbcCallOperationsCacheStatisticsAsMap");
assertEquals(11, cacheStatistics.size());
assertEquals(Long.valueOf(0), cacheStatistics.get("hitCount"));
assertEquals(Long.valueOf(0), cacheStatistics.get("loadCount"));
assertEquals(Long.valueOf(0), cacheStatistics.get("loadExceptionCount"));
assertEquals(Long.valueOf(0), cacheStatistics.get("loadSuccessCount"));
assertEquals(Long.valueOf(0), cacheStatistics.get("missCount"));
assertEquals(0L, cacheStatistics.get("hitCount"));
assertEquals(0L, cacheStatistics.get("loadCount"));
assertEquals(0L, cacheStatistics.get("loadExceptionCount"));
assertEquals(0L, cacheStatistics.get("loadSuccessCount"));
assertEquals(0L, cacheStatistics.get("missCount"));
userService.createUser(new User("myUsername", "myPassword", "myEmail"));
@@ -147,17 +157,17 @@ public class StoredProcJmxManagedBeanTests {
assertNotNull(message);
assertNotNull(message.getPayload());
assertNotNull(message.getPayload() instanceof Collection<?>);
Map<String, Object> cacheStatistics2 = (Map<String, Object>) server.getAttribute(name, "JdbcCallOperationsCacheStatisticsAsMap");
Map<String, Object> cacheStatistics2 =
(Map<String, Object>) server.getAttribute(name, "JdbcCallOperationsCacheStatisticsAsMap");
assertEquals(11, cacheStatistics2.size());
assertEquals(Long.valueOf(1), cacheStatistics2.get("hitCount"));
assertEquals(Long.valueOf(0), cacheStatistics2.get("loadCount"));
assertEquals(Long.valueOf(0), cacheStatistics2.get("loadExceptionCount"));
assertEquals(Long.valueOf(0), cacheStatistics2.get("loadSuccessCount"));
assertEquals(Long.valueOf(0), cacheStatistics2.get("missCount"));
assertEquals(0L, cacheStatistics2.get("hitCount"));
assertEquals(1L, cacheStatistics2.get("loadCount"));
assertEquals(0L, cacheStatistics2.get("loadExceptionCount"));
assertEquals(1L, cacheStatistics2.get("loadSuccessCount"));
assertEquals(1L, cacheStatistics2.get("missCount"));
}
@@ -170,8 +180,9 @@ public class StoredProcJmxManagedBeanTests {
//prevent message overload
return null;
}
return Integer.valueOf(count.incrementAndGet());
return count.incrementAndGet();
}
}
@@ -187,5 +198,7 @@ public class StoredProcJmxManagedBeanTests {
Message<Collection<User>> poll(long timeoutInMillis) throws InterruptedException {
return messages.poll(timeoutInMillis, TimeUnit.MILLISECONDS);
}
}
}