From dc345dcb3d06b0acd29099cf1d78600021c42c57 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sat, 18 Apr 2015 18:50:37 +0200 Subject: [PATCH 1/8] Polish TestContextManager --- .../test/context/TestContextManager.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java index 3551c9d03061..9b75bffab341 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java @@ -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. @@ -83,8 +83,6 @@ public class TestContextManager { private final TestContext testContext; - private final TestContextBootstrapper testContextBootstrapper; - private final List testExecutionListeners = new ArrayList(); @@ -99,12 +97,11 @@ public class TestContextManager { public TestContextManager(Class testClass) { CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = new DefaultCacheAwareContextLoaderDelegate(contextCache); BootstrapContext bootstrapContext = new DefaultBootstrapContext(testClass, cacheAwareContextLoaderDelegate); - this.testContextBootstrapper = BootstrapUtils.resolveTestContextBootstrapper(bootstrapContext); - this.testContext = new DefaultTestContext(this.testContextBootstrapper); - registerTestExecutionListeners(this.testContextBootstrapper.getTestExecutionListeners()); + TestContextBootstrapper testContextBootstrapper = BootstrapUtils.resolveTestContextBootstrapper(bootstrapContext); + this.testContext = new DefaultTestContext(testContextBootstrapper); + registerTestExecutionListeners(testContextBootstrapper.getTestExecutionListeners()); } - /** * Get the {@link TestContext} managed by this {@code TestContextManager}. */ From 0e287c1a90513971aea03941a5fbfca48b089f20 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sat, 18 Apr 2015 19:16:59 +0200 Subject: [PATCH 2/8] Introduce resetContextCache() in ContextCacheTestUtils --- .../test/context/ClassLevelDirtiesContextTestNGTests.java | 5 +++-- .../test/context/ClassLevelDirtiesContextTests.java | 5 +++-- .../test/context/ContextCacheTestUtils.java | 7 +++++++ .../test/context/SpringRunnerContextCacheTests.java | 3 +-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/spring-test/src/test/java/org/springframework/test/context/ClassLevelDirtiesContextTestNGTests.java b/spring-test/src/test/java/org/springframework/test/context/ClassLevelDirtiesContextTestNGTests.java index 9009e73229a5..528ef7045030 100644 --- a/spring-test/src/test/java/org/springframework/test/context/ClassLevelDirtiesContextTestNGTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/ClassLevelDirtiesContextTestNGTests.java @@ -79,8 +79,9 @@ private static final void runTestClassAndAssertStats(Class testClass, int exp @BeforeClass public static void verifyInitialCacheState() { - ContextCache contextCache = TestContextManager.contextCache; - contextCache.reset(); + resetContextCache(); + // Reset static counters in case tests are run multiple times in a test suite -- + // for example, via JUnit's @Suite. cacheHits.set(0); cacheMisses.set(0); assertContextCacheStatistics("BeforeClass", 0, cacheHits.get(), cacheMisses.get()); diff --git a/spring-test/src/test/java/org/springframework/test/context/ClassLevelDirtiesContextTests.java b/spring-test/src/test/java/org/springframework/test/context/ClassLevelDirtiesContextTests.java index 1c51ad8072e0..6a73bfa4e0c6 100644 --- a/spring-test/src/test/java/org/springframework/test/context/ClassLevelDirtiesContextTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/ClassLevelDirtiesContextTests.java @@ -74,8 +74,9 @@ private static final void runTestClassAndAssertStats(Class testClass, int exp @BeforeClass public static void verifyInitialCacheState() { - ContextCache contextCache = TestContextManager.contextCache; - contextCache.reset(); + resetContextCache(); + // Reset static counters in case tests are run multiple times in a test suite -- + // for example, via JUnit's @Suite. cacheHits.set(0); cacheMisses.set(0); assertContextCacheStatistics("BeforeClass", 0, cacheHits.get(), cacheMisses.get()); diff --git a/spring-test/src/test/java/org/springframework/test/context/ContextCacheTestUtils.java b/spring-test/src/test/java/org/springframework/test/context/ContextCacheTestUtils.java index dd5efcee1a43..3ffe61fe687b 100644 --- a/spring-test/src/test/java/org/springframework/test/context/ContextCacheTestUtils.java +++ b/spring-test/src/test/java/org/springframework/test/context/ContextCacheTestUtils.java @@ -27,6 +27,13 @@ */ public class ContextCacheTestUtils { + /** + * Reset the state of the context cache. + */ + public static final void resetContextCache() { + TestContextManager.contextCache.reset(); + } + /** * Assert the statistics of the context cache in {@link TestContextManager}. * diff --git a/spring-test/src/test/java/org/springframework/test/context/SpringRunnerContextCacheTests.java b/spring-test/src/test/java/org/springframework/test/context/SpringRunnerContextCacheTests.java index 3e09f2731e90..030bfa9e8826 100644 --- a/spring-test/src/test/java/org/springframework/test/context/SpringRunnerContextCacheTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/SpringRunnerContextCacheTests.java @@ -58,8 +58,7 @@ public class SpringRunnerContextCacheTests { @BeforeClass public static void verifyInitialCacheState() { dirtiedApplicationContext = null; - ContextCache contextCache = TestContextManager.contextCache; - contextCache.reset(); + resetContextCache(); assertContextCacheStatistics("BeforeClass", 0, 0, 0); } From 0392a88c6954bdcd391eea07b513973ebee3ba08 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sat, 18 Apr 2015 20:17:20 +0200 Subject: [PATCH 3/8] Move static ContextCache to DefaultCacheAwareContextLoaderDelegate Issue: SPR-12683 --- ...efaultCacheAwareContextLoaderDelegate.java | 23 ++++++++++++++++++- .../test/context/TestContextManager.java | 12 ++-------- .../test/context/ContextCacheTestUtils.java | 10 ++++---- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/context/DefaultCacheAwareContextLoaderDelegate.java b/spring-test/src/main/java/org/springframework/test/context/DefaultCacheAwareContextLoaderDelegate.java index a9d38dfa08db..a13e347c2c54 100644 --- a/spring-test/src/main/java/org/springframework/test/context/DefaultCacheAwareContextLoaderDelegate.java +++ b/spring-test/src/main/java/org/springframework/test/context/DefaultCacheAwareContextLoaderDelegate.java @@ -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. @@ -40,9 +40,30 @@ class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContextLoaderD private static final Log statsLogger = LogFactory.getLog("org.springframework.test.context.cache"); + /** + * Default cache of Spring application contexts. + * + *

This default cache is static, so that each context can be cached + * and reused for all subsequent tests that declare the same unique + * context configuration within the same JVM process. + */ + static final ContextCache defaultContextCache = new ContextCache(); + private final ContextCache contextCache; + /** + * Construct a new {@code DefaultCacheAwareContextLoaderDelegate} that + * uses the default, static {@code ContextCache}. + */ + DefaultCacheAwareContextLoaderDelegate() { + this(defaultContextCache); + } + + /** + * Construct a new {@code DefaultCacheAwareContextLoaderDelegate} with + * the supplied {@code ContextCache}. + */ DefaultCacheAwareContextLoaderDelegate(ContextCache contextCache) { Assert.notNull(contextCache, "ContextCache must not be null"); this.contextCache = contextCache; diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java index 9b75bffab341..9b7fa9573581 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java @@ -73,14 +73,6 @@ public class TestContextManager { private static final Log logger = LogFactory.getLog(TestContextManager.class); - /** - * Cache of Spring application contexts. - *

This needs to be static, since test instances may be destroyed and - * recreated between invocations of individual test methods, as is the case - * with JUnit. - */ - static final ContextCache contextCache = new ContextCache(); - private final TestContext testContext; private final List testExecutionListeners = new ArrayList(); @@ -88,14 +80,14 @@ public class TestContextManager { /** * Construct a new {@code TestContextManager} for the specified {@linkplain Class test class} - * and automatically {@link #registerTestExecutionListeners register} the + * and automatically {@linkplain #registerTestExecutionListeners register} the * {@link TestExecutionListener TestExecutionListeners} configured for the test class * via the {@link TestExecutionListeners @TestExecutionListeners} annotation. * @param testClass the test class to be managed * @see #registerTestExecutionListeners */ public TestContextManager(Class testClass) { - CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = new DefaultCacheAwareContextLoaderDelegate(contextCache); + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = new DefaultCacheAwareContextLoaderDelegate(); BootstrapContext bootstrapContext = new DefaultBootstrapContext(testClass, cacheAwareContextLoaderDelegate); TestContextBootstrapper testContextBootstrapper = BootstrapUtils.resolveTestContextBootstrapper(bootstrapContext); this.testContext = new DefaultTestContext(testContextBootstrapper); diff --git a/spring-test/src/test/java/org/springframework/test/context/ContextCacheTestUtils.java b/spring-test/src/test/java/org/springframework/test/context/ContextCacheTestUtils.java index 3ffe61fe687b..6291e5a1d2fe 100644 --- a/spring-test/src/test/java/org/springframework/test/context/ContextCacheTestUtils.java +++ b/spring-test/src/test/java/org/springframework/test/context/ContextCacheTestUtils.java @@ -28,14 +28,14 @@ public class ContextCacheTestUtils { /** - * Reset the state of the context cache. + * Reset the state of the static context cache in {@link DefaultCacheAwareContextLoaderDelegate}. */ public static final void resetContextCache() { - TestContextManager.contextCache.reset(); + DefaultCacheAwareContextLoaderDelegate.defaultContextCache.reset(); } /** - * Assert the statistics of the context cache in {@link TestContextManager}. + * Assert the statistics of the static context cache in {@link DefaultCacheAwareContextLoaderDelegate}. * * @param usageScenario the scenario in which the statistics are used * @param expectedSize the expected number of contexts in the cache @@ -44,8 +44,8 @@ public static final void resetContextCache() { */ public static final void assertContextCacheStatistics(String usageScenario, int expectedSize, int expectedHitCount, int expectedMissCount) { - assertContextCacheStatistics(TestContextManager.contextCache, usageScenario, expectedSize, expectedHitCount, - expectedMissCount); + assertContextCacheStatistics(DefaultCacheAwareContextLoaderDelegate.defaultContextCache, usageScenario, + expectedSize, expectedHitCount, expectedMissCount); } /** From 186abcb054303c30620ecf3620be25e7b9cf7e29 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sat, 18 Apr 2015 23:06:57 +0200 Subject: [PATCH 4/8] Introduce buildTestContext() in TestContextBootstrapper This commit moves the responsibility of building a TestContext from the TestContextManager to a TestContextBootstrapper. In addition, DefaultTestContext is now a public class residing in the "support" subpackage. Issue: SPR-12683 --- .../test/context/BootstrapContext.java | 4 +- .../test/context/TestContextBootstrapper.java | 64 +++++++++++-------- .../test/context/TestContextManager.java | 29 ++++++--- .../AbstractTestContextBootstrapper.java | 16 +++++ .../{ => support}/DefaultTestContext.java | 54 ++++++++++------ .../test/context/TestContextTestUtils.java | 8 +-- 6 files changed, 115 insertions(+), 60 deletions(-) rename spring-test/src/main/java/org/springframework/test/context/{ => support}/DefaultTestContext.java (55%) diff --git a/spring-test/src/main/java/org/springframework/test/context/BootstrapContext.java b/spring-test/src/main/java/org/springframework/test/context/BootstrapContext.java index fa12a797e347..13c311fa7974 100644 --- a/spring-test/src/main/java/org/springframework/test/context/BootstrapContext.java +++ b/spring-test/src/main/java/org/springframework/test/context/BootstrapContext.java @@ -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. @@ -28,7 +28,7 @@ public interface BootstrapContext { /** - * Get the {@link Class test class} for this bootstrap context. + * Get the {@linkplain Class test class} for this bootstrap context. * @return the test class (never {@code null}) */ Class getTestClass(); diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContextBootstrapper.java b/spring-test/src/main/java/org/springframework/test/context/TestContextBootstrapper.java index 7252657f42b7..ae81c9ec4a60 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContextBootstrapper.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContextBootstrapper.java @@ -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. @@ -29,15 +29,14 @@ * *

The {@link TestContextManager} uses a {@code TestContextBootstrapper} to * {@linkplain #getTestExecutionListeners get the TestExecutionListeners} for the - * current test and to {@linkplain #buildMergedContextConfiguration build the - * merged context configuration} necessary to create the {@link TestContext} that + * current test and to {@linkplain #buildTestContext build the TestContext} that * it manages. * *

Concrete implementations must provide a {@code public} no-args constructor. * - *

Note: this SPI might potentially change in the future in + *

WARNING: this SPI will likely change in the future in * order to accommodate new requirements. Implementers are therefore strongly encouraged - * not to implement this interface directly but rather to extend + * not to implement this interface directly but rather to extend * {@link org.springframework.test.context.support.AbstractTestContextBootstrapper * AbstractTestContextBootstrapper} or one of its concrete subclasses instead. * @@ -59,28 +58,13 @@ public interface TestContextBootstrapper { BootstrapContext getBootstrapContext(); /** - * Get a list of newly instantiated {@link TestExecutionListener TestExecutionListeners} - * for the test class in the {@link BootstrapContext} associated with this bootstrapper. - *

If {@link TestExecutionListeners @TestExecutionListeners} is not - * present on the test class in the {@code BootstrapContext}, - * default listeners should be returned. Furthermore, default - * listeners must be sorted using - * {@link org.springframework.core.annotation.AnnotationAwareOrderComparator - * AnnotationAwareOrderComparator}. - *

Concrete implementations are free to determine what comprises the - * set of default listeners. However, by default, the Spring TestContext - * Framework will use the - * {@link org.springframework.core.io.support.SpringFactoriesLoader SpringFactoriesLoader} - * mechanism to look up all {@code TestExecutionListener} class names - * configured in all {@code META-INF/spring.factories} files on the classpath. - *

The {@link TestExecutionListeners#inheritListeners() inheritListeners} - * flag of {@link TestExecutionListeners @TestExecutionListeners} must be - * taken into consideration. Specifically, if the {@code inheritListeners} - * flag is set to {@code true}, listeners declared for a given test class must - * be appended to the end of the list of listeners declared in superclasses. - * @return a list of {@code TestExecutionListener} instances + * Build the {@link TestContext} for the {@link BootstrapContext} + * associated with this bootstrapper. + * @return a new {@link TestContext}, never {@code null} + * @since 4.2 + * @see #buildMergedContextConfiguration() */ - List getTestExecutionListeners(); + TestContext buildTestContext(); /** * Build the {@linkplain MergedContextConfiguration merged context configuration} @@ -94,9 +78,12 @@ public interface TestContextBootstrapper { *

  • Active bean definition profiles declared via {@link ActiveProfiles @ActiveProfiles}
  • *
  • {@linkplain org.springframework.context.ApplicationContextInitializer * Context initializers} declared via {@link ContextConfiguration#initializers}
  • + *
  • Test property sources declared via {@link TestPropertySource @TestPropertySource}
  • * *

    Consult the Javadoc for the aforementioned annotations for details on * the required semantics. + *

    Note that the implementation of {@link #buildTestContext()} should + * typically delegate to this method when constructing the {@code TestContext}. *

    When determining which {@link ContextLoader} to use for a given test * class, the following algorithm should be used: *

      @@ -106,7 +93,32 @@ public interface TestContextBootstrapper { * {@code ContextLoader} class to use as as default. *
    * @return the merged context configuration, never {@code null} + * @see #buildTestContext() */ MergedContextConfiguration buildMergedContextConfiguration(); + /** + * Get a list of newly instantiated {@link TestExecutionListener TestExecutionListeners} + * for the test class in the {@link BootstrapContext} associated with this bootstrapper. + *

    If {@link TestExecutionListeners @TestExecutionListeners} is not + * present on the test class in the {@code BootstrapContext}, + * default listeners should be returned. Furthermore, default + * listeners must be sorted using + * {@link org.springframework.core.annotation.AnnotationAwareOrderComparator + * AnnotationAwareOrderComparator}. + *

    Concrete implementations are free to determine what comprises the + * set of default listeners. However, by default, the Spring TestContext + * Framework will use the + * {@link org.springframework.core.io.support.SpringFactoriesLoader SpringFactoriesLoader} + * mechanism to look up all {@code TestExecutionListener} class names + * configured in all {@code META-INF/spring.factories} files on the classpath. + *

    The {@link TestExecutionListeners#inheritListeners() inheritListeners} + * flag of {@link TestExecutionListeners @TestExecutionListeners} must be + * taken into consideration. Specifically, if the {@code inheritListeners} + * flag is set to {@code true}, listeners declared for a given test class must + * be appended to the end of the list of listeners declared in superclasses. + * @return a list of {@code TestExecutionListener} instances + */ + List getTestExecutionListeners(); + } diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java index 9b7fa9573581..b0b93195bbea 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java @@ -28,15 +28,11 @@ /** * {@code TestContextManager} is the main entry point into the Spring - * TestContext Framework, which provides support for loading and accessing - * {@link org.springframework.context.ApplicationContext application contexts}, - * dependency injection of test instances, - * {@link org.springframework.transaction.annotation.Transactional transactional} - * execution of test methods, etc. + * TestContext Framework. * *

    Specifically, a {@code TestContextManager} is responsible for managing a * single {@link TestContext} and signaling events to all registered - * {@link TestExecutionListener TestExecutionListeners} at well defined test + * {@link TestExecutionListener TestExecutionListeners} at the following test * execution points: * *

      @@ -56,6 +52,21 @@ * 4's {@link org.junit.AfterClass @AfterClass}) *
    * + *

    Support for loading and accessing + * {@link org.springframework.context.ApplicationContext application contexts}, + * dependency injection of test instances, + * {@link org.springframework.transaction.annotation.Transactional transactional} + * execution of test methods, etc. is provided by + * {@link SmartContextLoader ContextLoaders} and {@link TestExecutionListener + * TestExecutionListeners}, which are configured via + * {@link ContextConfiguration @ContextConfiguration} and + * {@link TestExecutionListeners @TestExecutionListeners}. + * + *

    Bootstrapping of the {@code TestContext}, the default {@code ContextLoader}, + * default {@code TestExecutionListeners}, and their collaborators is performed + * by a {@link TestContextBootstrapper}, which is configured via + * {@link BootstrapWith @BootstrapWith}. + * * @author Sam Brannen * @author Juergen Hoeller * @since 2.5 @@ -90,7 +101,7 @@ public TestContextManager(Class testClass) { CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = new DefaultCacheAwareContextLoaderDelegate(); BootstrapContext bootstrapContext = new DefaultBootstrapContext(testClass, cacheAwareContextLoaderDelegate); TestContextBootstrapper testContextBootstrapper = BootstrapUtils.resolveTestContextBootstrapper(bootstrapContext); - this.testContext = new DefaultTestContext(testContextBootstrapper); + this.testContext = testContextBootstrapper.buildTestContext(); registerTestExecutionListeners(testContextBootstrapper.getTestExecutionListeners()); } @@ -190,7 +201,7 @@ public void beforeTestClass() throws Exception { * @see #getTestExecutionListeners() */ public void prepareTestInstance(Object testInstance) throws Exception { - Assert.notNull(testInstance, "testInstance must not be null"); + Assert.notNull(testInstance, "Test instance must not be null"); if (logger.isTraceEnabled()) { logger.trace("prepareTestInstance(): instance [" + testInstance + "]"); } @@ -271,7 +282,7 @@ public void beforeTestMethod(Object testInstance, Method testMethod) throws Exce * @see #getTestExecutionListeners() */ public void afterTestMethod(Object testInstance, Method testMethod, Throwable exception) throws Exception { - Assert.notNull(testInstance, "testInstance must not be null"); + Assert.notNull(testInstance, "Test instance must not be null"); if (logger.isTraceEnabled()) { logger.trace("afterTestMethod(): instance [" + testInstance + "], method [" + testMethod + "], exception [" + exception + "]"); diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java index 709c82e972ab..d538777e57c1 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java @@ -44,6 +44,7 @@ import org.springframework.test.context.ContextLoader; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.SmartContextLoader; +import org.springframework.test.context.TestContext; import org.springframework.test.context.TestContextBootstrapper; import org.springframework.test.context.TestExecutionListener; import org.springframework.test.context.TestExecutionListeners; @@ -93,6 +94,21 @@ public BootstrapContext getBootstrapContext() { return this.bootstrapContext; } + /** + * Build a new {@link DefaultTestContext} using the {@linkplain Class test class} + * and {@link CacheAwareContextLoaderDelegate} in the {@link BootstrapContext} + * associated with this bootstrapper and by delegating to + * {@link #buildMergedContextConfiguration()}. + *

    Concrete subclasses may choose to override this method to return a + * custom {@link TestContext} implementation. + * @since 4.2 + */ + @Override + public TestContext buildTestContext() { + return new DefaultTestContext(bootstrapContext.getTestClass(), buildMergedContextConfiguration(), + bootstrapContext.getCacheAwareContextLoaderDelegate()); + } + /** * {@inheritDoc} */ diff --git a/spring-test/src/main/java/org/springframework/test/context/DefaultTestContext.java b/spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContext.java similarity index 55% rename from spring-test/src/main/java/org/springframework/test/context/DefaultTestContext.java rename to spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContext.java index eac358e8e52d..bd730b325465 100644 --- a/spring-test/src/main/java/org/springframework/test/context/DefaultTestContext.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContext.java @@ -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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context; +package org.springframework.test.context.support; import java.lang.reflect.Method; @@ -22,21 +22,19 @@ import org.springframework.core.AttributeAccessorSupport; import org.springframework.core.style.ToStringCreator; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; +import org.springframework.test.context.CacheAwareContextLoaderDelegate; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.TestContext; import org.springframework.util.Assert; /** * Default implementation of the {@link TestContext} interface. * - *

    Although {@code DefaultTestContext} was first introduced in Spring Framework - * 4.0, the initial implementation of this class was extracted from the existing - * code base for {@code TestContext} when {@code TestContext} was converted into - * an interface. - * * @author Sam Brannen * @author Juergen Hoeller * @since 4.0 */ -class DefaultTestContext extends AttributeAccessorSupport implements TestContext { +public class DefaultTestContext extends AttributeAccessorSupport implements TestContext { private static final long serialVersionUID = -5827157174866681233L; @@ -54,24 +52,42 @@ class DefaultTestContext extends AttributeAccessorSupport implements TestContext /** - * Construct a new test context using the supplied {@link TestContextBootstrapper}. - * @param testContextBootstrapper the {@code TestContextBootstrapper} to use - * to construct the test context (must not be {@code null}) + * Construct a new {@code DefaultTestContext} from the supplied arguments. + * @param testClass the test class for this test context; never {@code null} + * @param mergedContextConfiguration the merged application context + * configuration for this test context; never {@code null} + * @param cacheAwareContextLoaderDelegate the delegate to use for loading + * and closing the application context for this test context; never {@code null} */ - DefaultTestContext(TestContextBootstrapper testContextBootstrapper) { - Assert.notNull(testContextBootstrapper, "TestContextBootstrapper must not be null"); - - BootstrapContext bootstrapContext = testContextBootstrapper.getBootstrapContext(); - this.testClass = bootstrapContext.getTestClass(); - this.cacheAwareContextLoaderDelegate = bootstrapContext.getCacheAwareContextLoaderDelegate(); - this.mergedContextConfiguration = testContextBootstrapper.buildMergedContextConfiguration(); + public DefaultTestContext(Class testClass, MergedContextConfiguration mergedContextConfiguration, + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) { + Assert.notNull(testClass, "testClass must not be null"); + Assert.notNull(mergedContextConfiguration, "MergedContextConfiguration must not be null"); + Assert.notNull(cacheAwareContextLoaderDelegate, "CacheAwareContextLoaderDelegate must not be null"); + this.testClass = testClass; + this.mergedContextConfiguration = mergedContextConfiguration; + this.cacheAwareContextLoaderDelegate = cacheAwareContextLoaderDelegate; } - + /** + * Get the {@linkplain ApplicationContext application context} for this + * test context. + *

    The default implementation delegates to the {@link CacheAwareContextLoaderDelegate} + * that was supplied when this {@code TestContext} was constructed. + * @see CacheAwareContextLoaderDelegate#loadContext + */ public ApplicationContext getApplicationContext() { return this.cacheAwareContextLoaderDelegate.loadContext(this.mergedContextConfiguration); } + /** + * Mark the {@linkplain ApplicationContext application context} associated + * with this test context as dirty (i.e., by removing it from the + * context cache and closing it). + *

    The default implementation delegates to the {@link CacheAwareContextLoaderDelegate} + * that was supplied when this {@code TestContext} was constructed. + * @see CacheAwareContextLoaderDelegate#closeContext + */ public void markApplicationContextDirty(HierarchyMode hierarchyMode) { this.cacheAwareContextLoaderDelegate.closeContext(this.mergedContextConfiguration, hierarchyMode); } diff --git a/spring-test/src/test/java/org/springframework/test/context/TestContextTestUtils.java b/spring-test/src/test/java/org/springframework/test/context/TestContextTestUtils.java index 3f1eb86edfbf..da43525bc097 100644 --- a/spring-test/src/test/java/org/springframework/test/context/TestContextTestUtils.java +++ b/spring-test/src/test/java/org/springframework/test/context/TestContextTestUtils.java @@ -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. @@ -28,12 +28,12 @@ public static TestContext buildTestContext(Class testClass, ContextCache cont return buildTestContext(testClass, new DefaultCacheAwareContextLoaderDelegate(contextCache)); } - public static TestContext buildTestContext( - Class testClass, CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) { + public static TestContext buildTestContext(Class testClass, + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) { BootstrapContext bootstrapContext = new DefaultBootstrapContext(testClass, cacheAwareContextLoaderDelegate); TestContextBootstrapper testContextBootstrapper = BootstrapUtils.resolveTestContextBootstrapper(bootstrapContext); - return new DefaultTestContext(testContextBootstrapper); + return testContextBootstrapper.buildTestContext(); } } From 0b054f9fbd1e0fd818e62fcc8de05b2150814ee2 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sat, 18 Apr 2015 23:45:34 +0200 Subject: [PATCH 5/8] Improve Javadoc for TestContextBootstrapper --- .../test/context/TestContextBootstrapper.java | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContextBootstrapper.java b/spring-test/src/main/java/org/springframework/test/context/TestContextBootstrapper.java index ae81c9ec4a60..d33c3db8da4c 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContextBootstrapper.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContextBootstrapper.java @@ -19,19 +19,28 @@ import java.util.List; /** - * {@code TestContextBootstrapper} defines a strategy SPI for bootstrapping the + * {@code TestContextBootstrapper} defines the SPI for bootstrapping the * Spring TestContext Framework. * - *

    A custom bootstrapping strategy can be configured for a test class via - * {@link BootstrapWith @BootstrapWith}, either directly or as a meta-annotation. - * See {@link org.springframework.test.context.web.WebAppConfiguration @WebAppConfiguration} - * for an example. - * - *

    The {@link TestContextManager} uses a {@code TestContextBootstrapper} to + *

    A {@code TestContextBootstrapper} is used by the {@link TestContextManager} to * {@linkplain #getTestExecutionListeners get the TestExecutionListeners} for the * current test and to {@linkplain #buildTestContext build the TestContext} that * it manages. * + *

    Configuration

    + * + *

    A custom bootstrapping strategy can be configured for a test class (or + * test class hierarchy) via {@link BootstrapWith @BootstrapWith}, either + * directly or as a meta-annotation. See + * {@link org.springframework.test.context.web.WebAppConfiguration @WebAppConfiguration} + * for an example. + * + *

    If a bootstrapper is not explicitly configured via {@code @BootstrapWith}, the + * {@link org.springframework.test.context.support.DefaultTestContextBootstrapper DefaultTestContextBootstrapper} + * will be used. + * + *

    Implementation Notes

    + * *

    Concrete implementations must provide a {@code public} no-args constructor. * *

    WARNING: this SPI will likely change in the future in From c9d597f5191f0bc3be69d689baf38e3095845777 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sat, 18 Apr 2015 23:47:11 +0200 Subject: [PATCH 6/8] Introduce createBootstrapContext() extension in TestContextManager Issue: SPR-12683 --- .../test/context/TestContextManager.java | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java index b0b93195bbea..e481092c4e14 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java @@ -90,21 +90,36 @@ public class TestContextManager { /** - * Construct a new {@code TestContextManager} for the specified {@linkplain Class test class} - * and automatically {@linkplain #registerTestExecutionListeners register} the - * {@link TestExecutionListener TestExecutionListeners} configured for the test class - * via the {@link TestExecutionListeners @TestExecutionListeners} annotation. + * Construct a new {@code TestContextManager} for the specified {@linkplain Class test class}, + * automatically {@linkplain #registerTestExecutionListeners registering} the necessary + * {@link TestExecutionListener TestExecutionListeners}. + *

    Delegates to a {@link TestContextBootstrapper} for building the {@code TestContext} + * and retrieving the {@code TestExecutionListeners}. * @param testClass the test class to be managed + * @see TestContextBootstrapper#buildTestContext + * @see TestContextBootstrapper#getTestExecutionListeners * @see #registerTestExecutionListeners */ public TestContextManager(Class testClass) { - CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = new DefaultCacheAwareContextLoaderDelegate(); - BootstrapContext bootstrapContext = new DefaultBootstrapContext(testClass, cacheAwareContextLoaderDelegate); + BootstrapContext bootstrapContext = createBootstrapContext(testClass); TestContextBootstrapper testContextBootstrapper = BootstrapUtils.resolveTestContextBootstrapper(bootstrapContext); this.testContext = testContextBootstrapper.buildTestContext(); registerTestExecutionListeners(testContextBootstrapper.getTestExecutionListeners()); } + /** + * Create the {@code BootstrapContext} for the specified {@linkplain Class test class}. + *

    The default implementation creates a {@link DefaultBootstrapContext} that + * uses the {@link DefaultCacheAwareContextLoaderDelegate}. + *

    Can be overridden by subclasses as necessary. + * @param testClass the test class for which the bootstrap context should be created + * @return a new {@code BootstrapContext}; never {@code null} + */ + protected BootstrapContext createBootstrapContext(Class testClass) { + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = new DefaultCacheAwareContextLoaderDelegate(); + return new DefaultBootstrapContext(testClass, cacheAwareContextLoaderDelegate); + } + /** * Get the {@link TestContext} managed by this {@code TestContextManager}. */ From e6c24f71672a14a27179bef15a592cea029177ee Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sun, 19 Apr 2015 00:59:54 +0200 Subject: [PATCH 7/8] Convert ContextCache to interface with default implementation - ContextCache is now a public interface. - Introduced public DefaultContextCache implementation in the 'support' subpackage. Issue: SPR-12683 --- .../CacheAwareContextLoaderDelegate.java | 8 +- .../test/context/ContextCache.java | 279 +++++------------- ...efaultCacheAwareContextLoaderDelegate.java | 14 +- .../context/support/DefaultContextCache.java | 252 ++++++++++++++++ .../test/context/ContextCacheTests.java | 4 +- 5 files changed, 333 insertions(+), 224 deletions(-) create mode 100644 spring-test/src/main/java/org/springframework/test/context/support/DefaultContextCache.java diff --git a/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java b/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java index 1a59940476bf..5dfd5705b269 100644 --- a/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java +++ b/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java @@ -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. @@ -23,7 +23,7 @@ /** * A {@code CacheAwareContextLoaderDelegate} is responsible for {@linkplain * #loadContext loading} and {@linkplain #closeContext closing} application - * contexts, interacting transparently with a context cache behind + * contexts, interacting transparently with a {@link ContextCache} behind * the scenes. * *

    Note: {@code CacheAwareContextLoaderDelegate} does not extend the @@ -38,7 +38,7 @@ public interface CacheAwareContextLoaderDelegate { * Load the {@linkplain ApplicationContext application context} for the supplied * {@link MergedContextConfiguration} by delegating to the {@link ContextLoader} * configured in the given {@code MergedContextConfiguration}. - *

    If the context is present in the context cache it will simply + *

    If the context is present in the {@code ContextCache} it will simply * be returned; otherwise, it will be loaded, stored in the cache, and returned. * @param mergedContextConfiguration the merged context configuration to use * to load the application context; never {@code null} @@ -50,7 +50,7 @@ public interface CacheAwareContextLoaderDelegate { /** * Remove the {@linkplain ApplicationContext application context} for the - * supplied {@link MergedContextConfiguration} from the context cache + * supplied {@link MergedContextConfiguration} from the {@code ContextCache} * and {@linkplain ConfigurableApplicationContext#close() close} it if it is * an instance of {@link ConfigurableApplicationContext}. *

    The semantics of the supplied {@code HierarchyMode} must be honored when diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextCache.java b/spring-test/src/main/java/org/springframework/test/context/ContextCache.java index 5f18399d80da..6dafc4a4cd8b 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ContextCache.java +++ b/spring-test/src/main/java/org/springframework/test/context/ContextCache.java @@ -16,145 +16,48 @@ package org.springframework.test.context; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; - import org.springframework.context.ApplicationContext; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.style.ToStringCreator; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; -import org.springframework.util.Assert; -import org.springframework.util.ConcurrentReferenceHashMap; /** - * Cache for Spring {@link ApplicationContext ApplicationContexts} in a test - * environment. + * {@code ContextCache} defines the public API for caching Spring + * {@link ApplicationContext ApplicationContexts} within the Spring + * TestContext Framework. * - *

    Rationale

    - *

    Caching has significant performance benefits if initializing the context - * takes a considerable about of time. Although initializing a Spring context - * itself is very quick, some beans in a context, such as a - * {@code LocalSessionFactoryBean} for working with Hibernate, may take some - * time to initialize. Hence it often makes sense to perform that initialization - * only once per test suite. + *

    A {@code ContextCache} maintains a cache of {@code ApplicationContexts} + * keyed by {@link MergedContextConfiguration} instances. * - *

    Implementation Details

    - *

    {@code ContextCache} maintains a cache of {@code ApplicationContexts} - * keyed by {@link MergedContextConfiguration} instances. Behind the scenes, - * Spring's {@link ConcurrentReferenceHashMap} is used to store - * {@linkplain java.lang.ref.SoftReference soft references} to cached contexts - * and {@code MergedContextConfiguration} instances. + *

    Rationale

    + *

    Context caching can have significant performance benefits if context + * initialization is complex. So, although initializing a Spring context itself + * is typically very quick, some beans in a context — for example, an + * in-memory database or a {@code LocalSessionFactoryBean} for working with + * Hibernate — may take several seconds to initialize. Hence it often + * makes sense to perform that initialization only once per test suite. * * @author Sam Brannen * @author Juergen Hoeller - * @since 2.5 - * @see ConcurrentReferenceHashMap + * @since 4.2 */ -class ContextCache { - - /** - * Map of context keys to Spring {@code ApplicationContext} instances. - */ - private final Map contextMap = - new ConcurrentReferenceHashMap(64); - - /** - * Map of parent keys to sets of children keys, representing a top-down tree - * of context hierarchies. This information is used for determining which subtrees - * need to be recursively removed and closed when removing a context that is a parent - * of other contexts. - */ - private final Map> hierarchyMap = - new ConcurrentReferenceHashMap>(64); - - private final AtomicInteger hitCount = new AtomicInteger(); - - private final AtomicInteger missCount = new AtomicInteger(); - - /** - * Reset all state maintained by this cache. - * @see #clear() - * @see #clearStatistics() - */ - public void reset() { - synchronized (contextMap) { - clear(); - clearStatistics(); - } - } - - /** - * Clear all contexts from the cache and clear context hierarchy information as well. - */ - public void clear() { - synchronized (contextMap) { - this.contextMap.clear(); - this.hierarchyMap.clear(); - } - } - - /** - * Clear hit and miss count statistics for the cache (i.e., reset counters to zero). - */ - public void clearStatistics() { - synchronized (contextMap) { - this.hitCount.set(0); - this.missCount.set(0); - } - } +public interface ContextCache { /** * Determine whether there is a cached context for the given key. * @param key the context key (never {@code null}) * @return {@code true} if the cache contains a context with the given key */ - public boolean contains(MergedContextConfiguration key) { - Assert.notNull(key, "Key must not be null"); - return this.contextMap.containsKey(key); - } + boolean contains(MergedContextConfiguration key); /** * Obtain a cached {@code ApplicationContext} for the given key. - *

    The {@link #getHitCount() hit} and {@link #getMissCount() miss} counts will - * be updated accordingly. + *

    The {@link #getHitCount() hit} and {@link #getMissCount() miss} counts + * must be updated accordingly. * @param key the context key (never {@code null}) * @return the corresponding {@code ApplicationContext} instance, or {@code null} * if not found in the cache * @see #remove */ - public ApplicationContext get(MergedContextConfiguration key) { - Assert.notNull(key, "Key must not be null"); - ApplicationContext context = this.contextMap.get(key); - if (context == null) { - this.missCount.incrementAndGet(); - } - else { - this.hitCount.incrementAndGet(); - } - return context; - } - - /** - * Get the overall hit count for this cache. - *

    A hit is any access to the cache that returns a non-null - * context for the queried key. - */ - public int getHitCount() { - return this.hitCount.get(); - } - - /** - * Get the overall miss count for this cache. - *

    A miss is any access to the cache that returns a {@code null} - * context for the queried key. - */ - public int getMissCount() { - return this.missCount.get(); - } + ApplicationContext get(MergedContextConfiguration key); /** * Explicitly add an {@code ApplicationContext} instance to the cache @@ -162,122 +65,80 @@ public int getMissCount() { * @param key the context key (never {@code null}) * @param context the {@code ApplicationContext} instance (never {@code null}) */ - public void put(MergedContextConfiguration key, ApplicationContext context) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(context, "ApplicationContext must not be null"); - - this.contextMap.put(key, context); - MergedContextConfiguration child = key; - MergedContextConfiguration parent = child.getParent(); - while (parent != null) { - Set list = this.hierarchyMap.get(parent); - if (list == null) { - list = new HashSet(); - this.hierarchyMap.put(parent, list); - } - list.add(child); - child = parent; - parent = child.getParent(); - } - } + void put(MergedContextConfiguration key, ApplicationContext context); /** * Remove the context with the given key from the cache and explicitly - * {@linkplain ConfigurableApplicationContext#close() close} it if it is an - * instance of {@link ConfigurableApplicationContext}. - *

    Generally speaking, you would only call this method if you change the - * state of a singleton bean, potentially affecting future interaction with - * the context. - *

    In addition, the semantics of the supplied {@code HierarchyMode} will + * {@linkplain org.springframework.context.ConfigurableApplicationContext#close() close} + * it if it is an instance of {@code ConfigurableApplicationContext}. + *

    Generally speaking, this method should be called if the state of + * a singleton bean has been modified, potentially affecting future + * interaction with the context. + *

    In addition, the semantics of the supplied {@code HierarchyMode} must * be honored. See the Javadoc for {@link HierarchyMode} for details. * @param key the context key; never {@code null} * @param hierarchyMode the hierarchy mode; may be {@code null} if the context * is not part of a hierarchy */ - public void remove(MergedContextConfiguration key, HierarchyMode hierarchyMode) { - Assert.notNull(key, "Key must not be null"); + void remove(MergedContextConfiguration key, HierarchyMode hierarchyMode); - // startKey is the level at which to begin clearing the cache, depending - // on the configured hierarchy mode. - MergedContextConfiguration startKey = key; - if (hierarchyMode == HierarchyMode.EXHAUSTIVE) { - while (startKey.getParent() != null) { - startKey = startKey.getParent(); - } - } - - List removedContexts = new ArrayList(); - remove(removedContexts, startKey); - - // Remove all remaining references to any removed contexts from the - // hierarchy map. - for (MergedContextConfiguration currentKey : removedContexts) { - for (Set children : this.hierarchyMap.values()) { - children.remove(currentKey); - } - } + /** + * Determine the number of contexts currently stored in the cache. + *

    If the cache contains more than {@code Integer.MAX_VALUE} elements, + * this method must return {@code Integer.MAX_VALUE}. + */ + int size(); - // Remove empty entries from the hierarchy map. - for (MergedContextConfiguration currentKey : this.hierarchyMap.keySet()) { - if (this.hierarchyMap.get(currentKey).isEmpty()) { - this.hierarchyMap.remove(currentKey); - } - } - } + /** + * Determine the number of parent contexts currently tracked within the cache. + */ + int getParentContextCount(); - private void remove(List removedContexts, MergedContextConfiguration key) { - Assert.notNull(key, "Key must not be null"); + /** + * Get the overall hit count for this cache. + *

    A hit is any access to the cache that returns a non-null + * context for the queried key. + */ + int getHitCount(); - Set children = this.hierarchyMap.get(key); - if (children != null) { - for (MergedContextConfiguration child : children) { - // Recurse through lower levels - remove(removedContexts, child); - } - // Remove the set of children for the current context from the hierarchy map. - this.hierarchyMap.remove(key); - } + /** + * Get the overall miss count for this cache. + *

    A miss is any access to the cache that returns a {@code null} + * context for the queried key. + */ + int getMissCount(); - // Physically remove and close leaf nodes first (i.e., on the way back up the - // stack as opposed to prior to the recursive call). - ApplicationContext context = this.contextMap.remove(key); - if (context instanceof ConfigurableApplicationContext) { - ((ConfigurableApplicationContext) context).close(); - } - removedContexts.add(key); - } + /** + * Reset all state maintained by this cache including statistics. + * @see #clear() + * @see #clearStatistics() + */ + void reset(); /** - * Determine the number of contexts currently stored in the cache. - *

    If the cache contains more than {@code Integer.MAX_VALUE} elements, - * this method returns {@code Integer.MAX_VALUE}. + * Clear all contexts from the cache, clearing context hierarchy information as well. */ - public int size() { - return this.contextMap.size(); - } + void clear(); /** - * Determine the number of parent contexts currently tracked within the cache. + * Clear hit and miss count statistics for the cache (i.e., reset counters to zero). */ - public int getParentContextCount() { - return this.hierarchyMap.size(); - } + void clearStatistics(); /** - * Generate a text string containing the statistics for this cache. - *

    Specifically, the returned string contains the {@linkplain #size}, - * {@linkplain #getHitCount() hit count}, {@linkplain #getMissCount() miss count}, - * and {@linkplain #getParentContextCount() parent context count}. - * @return the statistics for this cache + * Generate a text string containing the implementation type of this + * cache and its statistics. + *

    The value returned by this method will be used primarily for + * logging purposes. + *

    Specifically, the returned string should contain the name of the + * concrete {@code ContextCache} implementation, the {@linkplain #size}, + * {@linkplain #getParentContextCount() parent context count}, + * {@linkplain #getHitCount() hit count}, {@linkplain #getMissCount() + * miss count}, and any other information useful in monitoring the + * state of this cache. + * @return a string representation of this cache, including statistics */ @Override - public String toString() { - return new ToStringCreator(this) - .append("size", size()) - .append("hitCount", getHitCount()) - .append("missCount", getMissCount()) - .append("parentContextCount", getParentContextCount()) - .toString(); - } + abstract String toString(); } diff --git a/spring-test/src/main/java/org/springframework/test/context/DefaultCacheAwareContextLoaderDelegate.java b/spring-test/src/main/java/org/springframework/test/context/DefaultCacheAwareContextLoaderDelegate.java index a13e347c2c54..cfa482c32123 100644 --- a/spring-test/src/main/java/org/springframework/test/context/DefaultCacheAwareContextLoaderDelegate.java +++ b/spring-test/src/main/java/org/springframework/test/context/DefaultCacheAwareContextLoaderDelegate.java @@ -21,16 +21,12 @@ import org.springframework.context.ApplicationContext; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; +import org.springframework.test.context.support.DefaultContextCache; import org.springframework.util.Assert; /** * Default implementation of the {@link CacheAwareContextLoaderDelegate} interface. * - *

    Although {@code DefaultCacheAwareContextLoaderDelegate} was first introduced - * in Spring Framework 4.1, the initial implementation of this class was extracted - * from the existing code base for {@code CacheAwareContextLoaderDelegate} when - * {@code CacheAwareContextLoaderDelegate} was converted into an interface. - * * @author Sam Brannen * @since 4.1 */ @@ -47,21 +43,21 @@ class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContextLoaderD * and reused for all subsequent tests that declare the same unique * context configuration within the same JVM process. */ - static final ContextCache defaultContextCache = new ContextCache(); + static final ContextCache defaultContextCache = new DefaultContextCache(); private final ContextCache contextCache; /** - * Construct a new {@code DefaultCacheAwareContextLoaderDelegate} that - * uses the default, static {@code ContextCache}. + * Construct a new {@code DefaultCacheAwareContextLoaderDelegate} using + * a static {@code DefaultContextCache}. */ DefaultCacheAwareContextLoaderDelegate() { this(defaultContextCache); } /** - * Construct a new {@code DefaultCacheAwareContextLoaderDelegate} with + * Construct a new {@code DefaultCacheAwareContextLoaderDelegate} using * the supplied {@code ContextCache}. */ DefaultCacheAwareContextLoaderDelegate(ContextCache contextCache) { diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DefaultContextCache.java b/spring-test/src/main/java/org/springframework/test/context/support/DefaultContextCache.java new file mode 100644 index 000000000000..17ebcb82b1d1 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/support/DefaultContextCache.java @@ -0,0 +1,252 @@ +/* + * 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. + * 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.test.context.support; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.style.ToStringCreator; +import org.springframework.test.annotation.DirtiesContext.HierarchyMode; +import org.springframework.test.context.ContextCache; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.util.Assert; +import org.springframework.util.ConcurrentReferenceHashMap; + +/** + * Default implementation of the {@link ContextCache} API. + * + *

    Uses Spring's {@link ConcurrentReferenceHashMap} to store + * {@linkplain java.lang.ref.SoftReference soft references} to cached + * contexts and {@code MergedContextConfiguration} instances. + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + * @see ConcurrentReferenceHashMap + */ +public class DefaultContextCache implements ContextCache { + + /** + * Map of context keys to Spring {@code ApplicationContext} instances. + */ + private final Map contextMap = + new ConcurrentReferenceHashMap(64); + + /** + * Map of parent keys to sets of children keys, representing a top-down tree + * of context hierarchies. This information is used for determining which subtrees + * need to be recursively removed and closed when removing a context that is a parent + * of other contexts. + */ + private final Map> hierarchyMap = + new ConcurrentReferenceHashMap>(64); + + private final AtomicInteger hitCount = new AtomicInteger(); + + private final AtomicInteger missCount = new AtomicInteger(); + + + /** + * {@inheritDoc} + */ + @Override + public boolean contains(MergedContextConfiguration key) { + Assert.notNull(key, "Key must not be null"); + return this.contextMap.containsKey(key); + } + + /** + * {@inheritDoc} + */ + @Override + public ApplicationContext get(MergedContextConfiguration key) { + Assert.notNull(key, "Key must not be null"); + ApplicationContext context = this.contextMap.get(key); + if (context == null) { + this.missCount.incrementAndGet(); + } + else { + this.hitCount.incrementAndGet(); + } + return context; + } + + /** + * {@inheritDoc} + */ + @Override + public void put(MergedContextConfiguration key, ApplicationContext context) { + Assert.notNull(key, "Key must not be null"); + Assert.notNull(context, "ApplicationContext must not be null"); + + this.contextMap.put(key, context); + MergedContextConfiguration child = key; + MergedContextConfiguration parent = child.getParent(); + while (parent != null) { + Set list = this.hierarchyMap.get(parent); + if (list == null) { + list = new HashSet(); + this.hierarchyMap.put(parent, list); + } + list.add(child); + child = parent; + parent = child.getParent(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void remove(MergedContextConfiguration key, HierarchyMode hierarchyMode) { + Assert.notNull(key, "Key must not be null"); + + // startKey is the level at which to begin clearing the cache, depending + // on the configured hierarchy mode. + MergedContextConfiguration startKey = key; + if (hierarchyMode == HierarchyMode.EXHAUSTIVE) { + while (startKey.getParent() != null) { + startKey = startKey.getParent(); + } + } + + List removedContexts = new ArrayList(); + remove(removedContexts, startKey); + + // Remove all remaining references to any removed contexts from the + // hierarchy map. + for (MergedContextConfiguration currentKey : removedContexts) { + for (Set children : this.hierarchyMap.values()) { + children.remove(currentKey); + } + } + + // Remove empty entries from the hierarchy map. + for (MergedContextConfiguration currentKey : this.hierarchyMap.keySet()) { + if (this.hierarchyMap.get(currentKey).isEmpty()) { + this.hierarchyMap.remove(currentKey); + } + } + } + + private void remove(List removedContexts, MergedContextConfiguration key) { + Assert.notNull(key, "Key must not be null"); + + Set children = this.hierarchyMap.get(key); + if (children != null) { + for (MergedContextConfiguration child : children) { + // Recurse through lower levels + remove(removedContexts, child); + } + // Remove the set of children for the current context from the hierarchy map. + this.hierarchyMap.remove(key); + } + + // Physically remove and close leaf nodes first (i.e., on the way back up the + // stack as opposed to prior to the recursive call). + ApplicationContext context = this.contextMap.remove(key); + if (context instanceof ConfigurableApplicationContext) { + ((ConfigurableApplicationContext) context).close(); + } + removedContexts.add(key); + } + + /** + * {@inheritDoc} + */ + @Override + public int size() { + return this.contextMap.size(); + } + + /** + * {@inheritDoc} + */ + @Override + public int getParentContextCount() { + return this.hierarchyMap.size(); + } + + /** + * {@inheritDoc} + */ + @Override + public int getHitCount() { + return this.hitCount.get(); + } + + /** + * {@inheritDoc} + */ + @Override + public int getMissCount() { + return this.missCount.get(); + } + + /** + * {@inheritDoc} + */ + @Override + public void reset() { + synchronized (contextMap) { + clear(); + clearStatistics(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void clear() { + synchronized (contextMap) { + this.contextMap.clear(); + this.hierarchyMap.clear(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void clearStatistics() { + synchronized (contextMap) { + this.hitCount.set(0); + this.missCount.set(0); + } + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return new ToStringCreator(this) + .append("size", size()) + .append("parentContextCount", getParentContextCount()) + .append("hitCount", getHitCount()) + .append("missCount", getMissCount()) + .toString(); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/ContextCacheTests.java b/spring-test/src/test/java/org/springframework/test/context/ContextCacheTests.java index 61dc9beaa321..abd36808f85e 100644 --- a/spring-test/src/test/java/org/springframework/test/context/ContextCacheTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/ContextCacheTests.java @@ -18,11 +18,11 @@ import org.junit.Before; import org.junit.Test; - import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; import org.springframework.test.context.support.AnnotationConfigContextLoader; +import org.springframework.test.context.support.DefaultContextCache; import org.springframework.test.util.ReflectionTestUtils; import static org.junit.Assert.*; @@ -39,7 +39,7 @@ */ public class ContextCacheTests { - private ContextCache contextCache = new ContextCache(); + private ContextCache contextCache = new DefaultContextCache(); @Before From 129488cb4fba9592cfa3f0091ef2ecb25881b72a Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sun, 19 Apr 2015 18:14:11 +0200 Subject: [PATCH 8/8] Improve extensibility of TestContext bootstrapping & context caching - DefaultBootstrapContext and DefaultCacheAwareContextLoaderDelegate are now public classes in the 'support' subpackage. - Introduced getCacheAwareContextLoaderDelegate() in AbstractTestContextBootstrapper as an extension point for configuring custom ContextCache support. - Introduced reflection-based createBootstrapContext() utility method in BootstrapUtils; TestContextManager now delegates to BootstrapUtils in order to avoid package cycles. - Introduced logStatistics() method in the ContextCache API and defined statistics logging category as a constant. - DefaultCacheAwareContextLoaderDelegate now delegates to ContextCache.logStatistics(). Issue: SPR-12683 --- .../test/context/BootstrapContext.java | 3 +- .../test/context/BootstrapUtils.java | 57 ++++++++++++++++++- .../CacheAwareContextLoaderDelegate.java | 2 + .../test/context/ContextCache.java | 32 ++++++----- .../test/context/TestContextManager.java | 15 ++--- .../AbstractTestContextBootstrapper.java | 30 ++++++++-- .../DefaultBootstrapContext.java | 16 ++++-- ...efaultCacheAwareContextLoaderDelegate.java | 48 ++++++++++------ .../context/support/DefaultContextCache.java | 18 ++++++ .../test/context/BootstrapTestUtils.java | 2 + .../ClassLevelDirtiesContextTestNGTests.java | 2 +- .../ClassLevelDirtiesContextTests.java | 2 +- .../test/context/ContextCacheTests.java | 3 +- .../SpringRunnerContextCacheTests.java | 2 +- .../test/context/TestContextTestUtils.java | 3 + .../{ => support}/ContextCacheTestUtils.java | 4 +- 16 files changed, 184 insertions(+), 55 deletions(-) rename spring-test/src/main/java/org/springframework/test/context/{ => support}/DefaultBootstrapContext.java (70%) rename spring-test/src/main/java/org/springframework/test/context/{ => support}/DefaultCacheAwareContextLoaderDelegate.java (72%) rename spring-test/src/test/java/org/springframework/test/context/{ => support}/ContextCacheTestUtils.java (96%) diff --git a/spring-test/src/main/java/org/springframework/test/context/BootstrapContext.java b/spring-test/src/main/java/org/springframework/test/context/BootstrapContext.java index 13c311fa7974..19a7e89acecf 100644 --- a/spring-test/src/main/java/org/springframework/test/context/BootstrapContext.java +++ b/spring-test/src/main/java/org/springframework/test/context/BootstrapContext.java @@ -35,7 +35,8 @@ public interface BootstrapContext { /** * Get the {@link CacheAwareContextLoaderDelegate} to use for transparent - * interaction with the context cache. + * interaction with the {@code ContextCache}. + * @return the context loader delegate (never {@code null}) */ CacheAwareContextLoaderDelegate getCacheAwareContextLoaderDelegate(); diff --git a/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java b/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java index d0f9c07e8991..bf84a9a80e17 100644 --- a/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java @@ -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,6 +16,8 @@ package org.springframework.test.context; +import java.lang.reflect.Constructor; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -36,6 +38,10 @@ */ abstract class BootstrapUtils { + private static final String DEFAULT_BOOTSTRAP_CONTEXT_CLASS_NAME = "org.springframework.test.context.support.DefaultBootstrapContext"; + + private static final String DEFAULT_CACHE_AWARE_CONTEXT_LOADER_DELEGATE_CLASS_NAME = "org.springframework.test.context.support.DefaultCacheAwareContextLoaderDelegate"; + private static final String DEFAULT_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME = "org.springframework.test.context.support.DefaultTestContextBootstrapper"; private static final Log logger = LogFactory.getLog(BootstrapUtils.class); @@ -45,6 +51,55 @@ private BootstrapUtils() { /* no-op */ } + /** + * Create the {@code BootstrapContext} for the specified {@linkplain Class test class}. + * + *

    Uses reflection to create a {@link org.springframework.test.context.support.DefaultBootstrapContext} + * that uses a {@link org.springframework.test.context.support.DefaultCacheAwareContextLoaderDelegate}. + * + * @param testClass the test class for which the bootstrap context should be created + * @return a new {@code BootstrapContext}; never {@code null} + */ + @SuppressWarnings("unchecked") + static BootstrapContext createBootstrapContext(Class testClass) { + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = createCacheAwareContextLoaderDelegate(); + + Class clazz = null; + try { + clazz = (Class) ClassUtils.forName(DEFAULT_BOOTSTRAP_CONTEXT_CLASS_NAME, + BootstrapUtils.class.getClassLoader()); + + Constructor constructor = clazz.getConstructor(Class.class, + CacheAwareContextLoaderDelegate.class); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("Instantiating BootstrapContext using constructor [%s]", constructor)); + } + return instantiateClass(constructor, testClass, cacheAwareContextLoaderDelegate); + } + catch (Throwable t) { + throw new IllegalStateException("Could not load BootstrapContext [" + clazz + "]", t); + } + } + + @SuppressWarnings("unchecked") + private static CacheAwareContextLoaderDelegate createCacheAwareContextLoaderDelegate() { + Class clazz = null; + try { + clazz = (Class) ClassUtils.forName( + DEFAULT_CACHE_AWARE_CONTEXT_LOADER_DELEGATE_CLASS_NAME, BootstrapUtils.class.getClassLoader()); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("Instantiating CacheAwareContextLoaderDelegate from class [%s]", + clazz.getName())); + } + return instantiateClass(clazz, CacheAwareContextLoaderDelegate.class); + } + catch (Throwable t) { + throw new IllegalStateException("Could not load CacheAwareContextLoaderDelegate [" + clazz + "]", t); + } + } + /** * Resolve the {@link TestContextBootstrapper} type for the test class in the * supplied {@link BootstrapContext}, instantiate it, and provide it a reference diff --git a/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java b/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java index 5dfd5705b269..459c5a5c9c96 100644 --- a/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java +++ b/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java @@ -40,6 +40,8 @@ public interface CacheAwareContextLoaderDelegate { * configured in the given {@code MergedContextConfiguration}. *

    If the context is present in the {@code ContextCache} it will simply * be returned; otherwise, it will be loaded, stored in the cache, and returned. + *

    The cache statistics should be logged by invoking + * {@link ContextCache#logStatistics()}. * @param mergedContextConfiguration the merged context configuration to use * to load the application context; never {@code null} * @return the application context diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextCache.java b/spring-test/src/main/java/org/springframework/test/context/ContextCache.java index 6dafc4a4cd8b..cbf13e432a57 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ContextCache.java +++ b/spring-test/src/main/java/org/springframework/test/context/ContextCache.java @@ -41,6 +41,13 @@ */ public interface ContextCache { + /** + * The name of the logging category used for reporting {@code ContextCache} + * statistics. + */ + public static final String CONTEXT_CACHE_LOGGING_CATEGORY = "org.springframework.test.context.cache"; + + /** * Determine whether there is a cached context for the given key. * @param key the context key (never {@code null}) @@ -126,19 +133,18 @@ public interface ContextCache { void clearStatistics(); /** - * Generate a text string containing the implementation type of this - * cache and its statistics. - *

    The value returned by this method will be used primarily for - * logging purposes. - *

    Specifically, the returned string should contain the name of the - * concrete {@code ContextCache} implementation, the {@linkplain #size}, - * {@linkplain #getParentContextCount() parent context count}, - * {@linkplain #getHitCount() hit count}, {@linkplain #getMissCount() - * miss count}, and any other information useful in monitoring the - * state of this cache. - * @return a string representation of this cache, including statistics + * Log the statistics for this {@code ContextCache} at {@code DEBUG} level + * using the {@value #CONTEXT_CACHE_LOGGING_CATEGORY} logging category. + *

    The following information should be logged. + *

      + *
    • name of the concrete {@code ContextCache} implementation
    • + *
    • {@linkplain #size}
    • + *
    • {@linkplain #getParentContextCount() parent context count}
    • + *
    • {@linkplain #getHitCount() hit count}
    • + *
    • {@linkplain #getMissCount() miss count}
    • + *
    • any other information useful for monitoring the state of this cache
    • + *
    */ - @Override - abstract String toString(); + void logStatistics(); } diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java index e481092c4e14..f21717472156 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java @@ -102,22 +102,23 @@ public class TestContextManager { */ public TestContextManager(Class testClass) { BootstrapContext bootstrapContext = createBootstrapContext(testClass); - TestContextBootstrapper testContextBootstrapper = BootstrapUtils.resolveTestContextBootstrapper(bootstrapContext); - this.testContext = testContextBootstrapper.buildTestContext(); - registerTestExecutionListeners(testContextBootstrapper.getTestExecutionListeners()); + TestContextBootstrapper bootstrapper = BootstrapUtils.resolveTestContextBootstrapper(bootstrapContext); + this.testContext = bootstrapper.buildTestContext(); + registerTestExecutionListeners(bootstrapper.getTestExecutionListeners()); } /** * Create the {@code BootstrapContext} for the specified {@linkplain Class test class}. - *

    The default implementation creates a {@link DefaultBootstrapContext} that - * uses the {@link DefaultCacheAwareContextLoaderDelegate}. + *

    The default implementation creates a + * {@link org.springframework.test.context.support.DefaultBootstrapContext DefaultBootstrapContext} + * that uses a + * {@link org.springframework.test.context.support.DefaultCacheAwareContextLoaderDelegate DefaultCacheAwareContextLoaderDelegate}. *

    Can be overridden by subclasses as necessary. * @param testClass the test class for which the bootstrap context should be created * @return a new {@code BootstrapContext}; never {@code null} */ protected BootstrapContext createBootstrapContext(Class testClass) { - CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = new DefaultCacheAwareContextLoaderDelegate(); - return new DefaultBootstrapContext(testClass, cacheAwareContextLoaderDelegate); + return BootstrapUtils.createBootstrapContext(testClass); } /** diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java index d538777e57c1..c6b1b0b15c53 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java @@ -38,6 +38,7 @@ import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.test.context.BootstrapContext; import org.springframework.test.context.CacheAwareContextLoaderDelegate; +import org.springframework.test.context.ContextCache; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextHierarchy; @@ -67,6 +68,9 @@ *

  • {@link #processMergedContextConfiguration} * * + *

    To plug in custom {@link ContextCache} support, override + * {@link #getCacheAwareContextLoaderDelegate()}. + * * @author Sam Brannen * @author Juergen Hoeller * @since 4.1 @@ -96,17 +100,17 @@ public BootstrapContext getBootstrapContext() { /** * Build a new {@link DefaultTestContext} using the {@linkplain Class test class} - * and {@link CacheAwareContextLoaderDelegate} in the {@link BootstrapContext} - * associated with this bootstrapper and by delegating to - * {@link #buildMergedContextConfiguration()}. + * in the {@link BootstrapContext} associated with this bootstrapper and + * by delegating to {@link #buildMergedContextConfiguration()} and + * {@link #getCacheAwareContextLoaderDelegate()}. *

    Concrete subclasses may choose to override this method to return a * custom {@link TestContext} implementation. * @since 4.2 */ @Override public TestContext buildTestContext() { - return new DefaultTestContext(bootstrapContext.getTestClass(), buildMergedContextConfiguration(), - bootstrapContext.getCacheAwareContextLoaderDelegate()); + return new DefaultTestContext(getBootstrapContext().getTestClass(), buildMergedContextConfiguration(), + getCacheAwareContextLoaderDelegate()); } /** @@ -282,7 +286,7 @@ protected List getDefaultTestExecutionListenerClassNames() { @Override public final MergedContextConfiguration buildMergedContextConfiguration() { Class testClass = getBootstrapContext().getTestClass(); - CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = getBootstrapContext().getCacheAwareContextLoaderDelegate(); + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = getCacheAwareContextLoaderDelegate(); if (MetaAnnotationUtils.findAnnotationDescriptorForTypes(testClass, ContextConfiguration.class, ContextHierarchy.class) == null) { @@ -471,6 +475,20 @@ protected Class resolveExplicitContextLoaderClass( return null; } + /** + * Get the {@link CacheAwareContextLoaderDelegate} to use for transparent + * interaction with the {@code ContextCache}. + *

    The default implementation simply delegates to + * {@code getBootstrapContext().getCacheAwareContextLoaderDelegate()}. + *

    Concrete subclasses may choose to override this method to return a + * custom {@code CacheAwareContextLoaderDelegate} implementation with + * custom {@link ContextCache} support. + * @return the context loader delegate (never {@code null}) + */ + protected CacheAwareContextLoaderDelegate getCacheAwareContextLoaderDelegate() { + return getBootstrapContext().getCacheAwareContextLoaderDelegate(); + } + /** * Determine the default {@link ContextLoader} {@linkplain Class class} * to use for the supplied test class. diff --git a/spring-test/src/main/java/org/springframework/test/context/DefaultBootstrapContext.java b/spring-test/src/main/java/org/springframework/test/context/support/DefaultBootstrapContext.java similarity index 70% rename from spring-test/src/main/java/org/springframework/test/context/DefaultBootstrapContext.java rename to spring-test/src/main/java/org/springframework/test/context/support/DefaultBootstrapContext.java index e40802a7be10..d541c29b0d3f 100644 --- a/spring-test/src/main/java/org/springframework/test/context/DefaultBootstrapContext.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DefaultBootstrapContext.java @@ -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. @@ -14,9 +14,11 @@ * limitations under the License. */ -package org.springframework.test.context; +package org.springframework.test.context.support; import org.springframework.core.style.ToStringCreator; +import org.springframework.test.context.BootstrapContext; +import org.springframework.test.context.CacheAwareContextLoaderDelegate; import org.springframework.util.Assert; /** @@ -25,13 +27,19 @@ * @author Sam Brannen * @since 4.1 */ -class DefaultBootstrapContext implements BootstrapContext { +public class DefaultBootstrapContext implements BootstrapContext { private final Class testClass; private final CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate; - DefaultBootstrapContext(Class testClass, CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) { + /** + * Construct a new {@code DefaultBootstrapContext} from the supplied arguments. + * @param testClass the test class for this bootstrap context; never {@code null} + * @param cacheAwareContextLoaderDelegate the context loader delegate to use for + * transparent interaction with the {@code ContextCache}; never {@code null} + */ + public DefaultBootstrapContext(Class testClass, CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) { Assert.notNull(testClass, "Test class must not be null"); Assert.notNull(cacheAwareContextLoaderDelegate, "CacheAwareContextLoaderDelegate must not be null"); this.testClass = testClass; diff --git a/spring-test/src/main/java/org/springframework/test/context/DefaultCacheAwareContextLoaderDelegate.java b/spring-test/src/main/java/org/springframework/test/context/support/DefaultCacheAwareContextLoaderDelegate.java similarity index 72% rename from spring-test/src/main/java/org/springframework/test/context/DefaultCacheAwareContextLoaderDelegate.java rename to spring-test/src/main/java/org/springframework/test/context/support/DefaultCacheAwareContextLoaderDelegate.java index cfa482c32123..7598fa9eb91f 100644 --- a/spring-test/src/main/java/org/springframework/test/context/DefaultCacheAwareContextLoaderDelegate.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DefaultCacheAwareContextLoaderDelegate.java @@ -14,34 +14,37 @@ * limitations under the License. */ -package org.springframework.test.context; +package org.springframework.test.context.support; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.context.ApplicationContext; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; -import org.springframework.test.context.support.DefaultContextCache; +import org.springframework.test.context.CacheAwareContextLoaderDelegate; +import org.springframework.test.context.ContextCache; +import org.springframework.test.context.ContextLoader; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.SmartContextLoader; import org.springframework.util.Assert; /** * Default implementation of the {@link CacheAwareContextLoaderDelegate} interface. * + *

    To use a static {@code DefaultContextCache}, invoke the + * {@link #DefaultCacheAwareContextLoaderDelegate()} constructor; otherwise, + * invoke the {@link #DefaultCacheAwareContextLoaderDelegate(ContextCache)} + * and provide a custom {@link ContextCache} implementation. + * * @author Sam Brannen * @since 4.1 */ -class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContextLoaderDelegate { +public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContextLoaderDelegate { private static final Log logger = LogFactory.getLog(DefaultCacheAwareContextLoaderDelegate.class); - private static final Log statsLogger = LogFactory.getLog("org.springframework.test.context.cache"); - /** - * Default cache of Spring application contexts. - * - *

    This default cache is static, so that each context can be cached - * and reused for all subsequent tests that declare the same unique - * context configuration within the same JVM process. + * Default static cache of Spring application contexts. */ static final ContextCache defaultContextCache = new DefaultContextCache(); @@ -50,28 +53,39 @@ class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContextLoaderD /** * Construct a new {@code DefaultCacheAwareContextLoaderDelegate} using - * a static {@code DefaultContextCache}. + * a static {@link DefaultContextCache}. + *

    This default cache is static so that each context can be cached + * and reused for all subsequent tests that declare the same unique + * context configuration within the same JVM process. + * @see #DefaultCacheAwareContextLoaderDelegate(ContextCache) */ - DefaultCacheAwareContextLoaderDelegate() { + public DefaultCacheAwareContextLoaderDelegate() { this(defaultContextCache); } /** * Construct a new {@code DefaultCacheAwareContextLoaderDelegate} using - * the supplied {@code ContextCache}. + * the supplied {@link ContextCache}. + * @see #DefaultCacheAwareContextLoaderDelegate() */ - DefaultCacheAwareContextLoaderDelegate(ContextCache contextCache) { + public DefaultCacheAwareContextLoaderDelegate(ContextCache contextCache) { Assert.notNull(contextCache, "ContextCache must not be null"); this.contextCache = contextCache; } + /** + * Get the {@link ContextCache} used by this context loader delegate. + */ + protected ContextCache getContextCache() { + return this.contextCache; + } /** * Load the {@code ApplicationContext} for the supplied merged context configuration. *

    Supports both the {@link SmartContextLoader} and {@link ContextLoader} SPIs. * @throws Exception if an error occurs while loading the application context */ - private ApplicationContext loadContextInternal(MergedContextConfiguration mergedContextConfiguration) + protected ApplicationContext loadContextInternal(MergedContextConfiguration mergedContextConfiguration) throws Exception { ContextLoader contextLoader = mergedContextConfiguration.getContextLoader(); @@ -118,9 +132,7 @@ public ApplicationContext loadContext(MergedContextConfiguration mergedContextCo } } - if (statsLogger.isDebugEnabled()) { - statsLogger.debug("Spring test ApplicationContext cache statistics: " + this.contextCache); - } + this.contextCache.logStatistics(); return context; } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DefaultContextCache.java b/spring-test/src/main/java/org/springframework/test/context/support/DefaultContextCache.java index 17ebcb82b1d1..bcb5c21df45d 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/DefaultContextCache.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DefaultContextCache.java @@ -23,6 +23,8 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.style.ToStringCreator; @@ -46,6 +48,8 @@ */ public class DefaultContextCache implements ContextCache { + private static final Log statsLogger = LogFactory.getLog(CONTEXT_CACHE_LOGGING_CATEGORY); + /** * Map of context keys to Spring {@code ApplicationContext} instances. */ @@ -240,6 +244,20 @@ public void clearStatistics() { * {@inheritDoc} */ @Override + public void logStatistics() { + if (statsLogger.isDebugEnabled()) { + statsLogger.debug("Spring test ApplicationContext cache statistics: " + this); + } + } + + /** + * Generate a text string containing the implementation type of this + * cache and its statistics. + *

    The string returned by this method contains all information + * required for compliance with the contract for {@link #logStatistics()}. + * @return a string representation of this cache, including statistics + */ + @Override public String toString() { return new ToStringCreator(this) .append("size", size()) diff --git a/spring-test/src/test/java/org/springframework/test/context/BootstrapTestUtils.java b/spring-test/src/test/java/org/springframework/test/context/BootstrapTestUtils.java index b3b4fa5fb404..af6c4f7f5eaa 100644 --- a/spring-test/src/test/java/org/springframework/test/context/BootstrapTestUtils.java +++ b/spring-test/src/test/java/org/springframework/test/context/BootstrapTestUtils.java @@ -16,6 +16,8 @@ package org.springframework.test.context; +import org.springframework.test.context.support.DefaultBootstrapContext; + /** * Collection of test-related utility methods for working with {@link BootstrapContext * BootstrapContexts} and {@link TestContextBootstrapper TestContextBootstrappers}. diff --git a/spring-test/src/test/java/org/springframework/test/context/ClassLevelDirtiesContextTestNGTests.java b/spring-test/src/test/java/org/springframework/test/context/ClassLevelDirtiesContextTestNGTests.java index 528ef7045030..3b45f158d6b8 100644 --- a/spring-test/src/test/java/org/springframework/test/context/ClassLevelDirtiesContextTestNGTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/ClassLevelDirtiesContextTestNGTests.java @@ -37,7 +37,7 @@ import org.testng.TestNG; import static org.junit.Assert.*; -import static org.springframework.test.context.ContextCacheTestUtils.*; +import static org.springframework.test.context.support.ContextCacheTestUtils.*; /** * JUnit 4 based integration test which verifies correct {@linkplain ContextCache diff --git a/spring-test/src/test/java/org/springframework/test/context/ClassLevelDirtiesContextTests.java b/spring-test/src/test/java/org/springframework/test/context/ClassLevelDirtiesContextTests.java index 6a73bfa4e0c6..64dced85b857 100644 --- a/spring-test/src/test/java/org/springframework/test/context/ClassLevelDirtiesContextTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/ClassLevelDirtiesContextTests.java @@ -36,7 +36,7 @@ import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import static org.junit.Assert.*; -import static org.springframework.test.context.ContextCacheTestUtils.*; +import static org.springframework.test.context.support.ContextCacheTestUtils.*; /** * JUnit 4 based integration test which verifies correct {@linkplain ContextCache diff --git a/spring-test/src/test/java/org/springframework/test/context/ContextCacheTests.java b/spring-test/src/test/java/org/springframework/test/context/ContextCacheTests.java index abd36808f85e..e8a4c3e48fde 100644 --- a/spring-test/src/test/java/org/springframework/test/context/ContextCacheTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/ContextCacheTests.java @@ -18,6 +18,7 @@ import org.junit.Before; import org.junit.Test; + import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; @@ -26,7 +27,7 @@ import org.springframework.test.util.ReflectionTestUtils; import static org.junit.Assert.*; -import static org.springframework.test.context.ContextCacheTestUtils.*; +import static org.springframework.test.context.support.ContextCacheTestUtils.*; /** * Integration tests for verifying proper behavior of the {@link ContextCache} in diff --git a/spring-test/src/test/java/org/springframework/test/context/SpringRunnerContextCacheTests.java b/spring-test/src/test/java/org/springframework/test/context/SpringRunnerContextCacheTests.java index 030bfa9e8826..622d25f88dfd 100644 --- a/spring-test/src/test/java/org/springframework/test/context/SpringRunnerContextCacheTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/SpringRunnerContextCacheTests.java @@ -31,7 +31,7 @@ import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import static org.junit.Assert.*; -import static org.springframework.test.context.ContextCacheTestUtils.*; +import static org.springframework.test.context.support.ContextCacheTestUtils.*; /** * JUnit 4 based unit test which verifies correct {@link ContextCache diff --git a/spring-test/src/test/java/org/springframework/test/context/TestContextTestUtils.java b/spring-test/src/test/java/org/springframework/test/context/TestContextTestUtils.java index da43525bc097..47ec34ebd589 100644 --- a/spring-test/src/test/java/org/springframework/test/context/TestContextTestUtils.java +++ b/spring-test/src/test/java/org/springframework/test/context/TestContextTestUtils.java @@ -16,6 +16,9 @@ package org.springframework.test.context; +import org.springframework.test.context.support.DefaultBootstrapContext; +import org.springframework.test.context.support.DefaultCacheAwareContextLoaderDelegate; + /** * Collection of test-related utility methods for working with {@link TestContext TestContexts}. * diff --git a/spring-test/src/test/java/org/springframework/test/context/ContextCacheTestUtils.java b/spring-test/src/test/java/org/springframework/test/context/support/ContextCacheTestUtils.java similarity index 96% rename from spring-test/src/test/java/org/springframework/test/context/ContextCacheTestUtils.java rename to spring-test/src/test/java/org/springframework/test/context/support/ContextCacheTestUtils.java index 6291e5a1d2fe..c6127b47c74b 100644 --- a/spring-test/src/test/java/org/springframework/test/context/ContextCacheTestUtils.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/ContextCacheTestUtils.java @@ -14,7 +14,9 @@ * limitations under the License. */ -package org.springframework.test.context; +package org.springframework.test.context.support; + +import org.springframework.test.context.ContextCache; import static org.junit.Assert.*;