Introduce ActiveProfilesResolver in the TCF

Prior to this commit, the active bean definition profiles to use when
loading an ApplicationContext for tests could only be configured
declaratively (i.e., via hard-coded values supplied to the 'value' or
'profiles' attribute of @ActiveProfiles).

This commit makes it possible to programmatically configure active bean
definition profiles in tests via a new ActiveProfileResolver interface.
Custom resolvers can be registered via a new 'resolver' attribute
introduced in @ActiveProfiles.

Overview of changes:

 - Introduced a new ActiveProfilesResolver API.
 - Added a 'resolver' attribute to @ActiveProfiles.
 - Updated ContextLoaderUtils.resolveActiveProfiles() to support
   ActiveProfilesResolvers.
 - Documented these new features in the reference manual.
 - Added new content to the reference manual regarding the
   'inheritProfiles' attribute of @ActiveProfiles
 - Removed the use of <lineannotation> Docbook markup in the testing
   chapter of the reference manual for Java code examples in order to
   allow comments to have proper syntax highlighting in the generated
   HTML and PDF.

Issue: SPR-10338
This commit is contained in:
Sam Brannen
2013-06-16 00:01:34 +02:00
parent d4dcf4e4ec
commit 044f51283b
14 changed files with 731 additions and 137 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2013 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.
@@ -34,6 +34,7 @@ import java.lang.annotation.Target;
* @see SmartContextLoader
* @see MergedContextConfiguration
* @see ContextConfiguration
* @see ActiveProfilesResolver
* @see org.springframework.context.ApplicationContext
* @see org.springframework.context.annotation.Profile
*/
@@ -47,8 +48,8 @@ public @interface ActiveProfiles {
* Alias for {@link #profiles}.
*
* <p>This attribute may <strong>not</strong> be used in conjunction
* with {@link #profiles}, but it may be used <em>instead</em> of
* {@link #profiles}.
* with {@link #profiles} or {@link #resolver}, but it may be used
* <em>instead</em> of them.
*/
String[] value() default {};
@@ -56,11 +57,24 @@ public @interface ActiveProfiles {
* The bean definition profiles to activate.
*
* <p>This attribute may <strong>not</strong> be used in conjunction
* with {@link #value}, but it may be used <em>instead</em> of
* {@link #value}.
* with {@link #value} or {@link #resolver}, but it may be used
* <em>instead</em> of them.
*/
String[] profiles() default {};
/**
* The type of {@link ActiveProfilesResolver} to use for resolving the active
* bean definition profiles programmatically.
*
* <p>This attribute may <strong>not</strong> be used in conjunction
* with {@link #profiles} or {@link #value}, but it may be used <em>instead</em>
* of them in order to resolve the active profiles programmatically.
*
* @since 4.0
* @see ActiveProfilesResolver
*/
Class<? extends ActiveProfilesResolver> resolver() default ActiveProfilesResolver.class;
/**
* Whether or not bean definition profiles from superclasses should be
* <em>inherited</em>.

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2002-2013 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;
/**
* Strategy interface for programmatically resolving which <em>active bean
* definition profiles</em> should be used when loading an
* {@link org.springframework.context.ApplicationContext ApplicationContext}
* for a test class.
*
* <p>A custom {@code ActiveProfilesResolver} can be registered via the
* {@link ActiveProfiles#resolver resolver} attribute of {@code @ActiveProfiles}.
*
* <p>Concrete implementations must provide a {@code public} no-args constructor.
*
* @author Sam Brannen
* @author Michail Nikolaev
* @since 4.0
* @see ActiveProfiles
*/
public interface ActiveProfilesResolver {
/**
* Resolve the <em>bean definition profiles</em> to use when loading an
* {@code ApplicationContext} for the given {@linkplain Class test class}.
*
* @param testClass the test class for which the profiles should be resolved;
* never {@code null}
* @return the list of bean definition profiles to use when loading the
* {@code ApplicationContext}; never {@code null}
* @see ActiveProfiles#resolver
* @see ActiveProfiles#inheritProfiles
*/
String[] resolve(Class<?> testClass);
}

View File

@@ -16,11 +16,6 @@
package org.springframework.test.context;
import static org.springframework.beans.BeanUtils.instantiateClass;
import static org.springframework.core.annotation.AnnotationUtils.findAnnotationDeclaringClass;
import static org.springframework.core.annotation.AnnotationUtils.findAnnotationDeclaringClassForTypes;
import static org.springframework.core.annotation.AnnotationUtils.isAnnotationDeclaredLocally;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
@@ -42,6 +37,9 @@ import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import static org.springframework.beans.BeanUtils.*;
import static org.springframework.core.annotation.AnnotationUtils.*;
/**
* Utility methods for working with {@link ContextLoader ContextLoaders} and
* {@link SmartContextLoader SmartContextLoaders} and resolving resource locations,
@@ -49,12 +47,14 @@ import org.springframework.util.StringUtils;
* initializers.
*
* @author Sam Brannen
* @author Michail Nikolaev
* @since 3.1
* @see ContextLoader
* @see SmartContextLoader
* @see ContextConfiguration
* @see ContextConfigurationAttributes
* @see ActiveProfiles
* @see ActiveProfilesResolver
* @see ApplicationContextInitializer
* @see ContextHierarchy
* @see MergedContextConfiguration
@@ -477,24 +477,43 @@ abstract class ContextLoaderUtils {
while (declaringClass != null) {
ActiveProfiles annotation = declaringClass.getAnnotation(annotationType);
if (logger.isTraceEnabled()) {
logger.trace(String.format("Retrieved @ActiveProfiles [%s] for declaring class [%s].", annotation,
declaringClass.getName()));
}
validateActiveProfilesConfiguration(declaringClass, annotation);
String[] profiles = annotation.profiles();
String[] valueProfiles = annotation.value();
Class<? extends ActiveProfilesResolver> resolverClass = annotation.resolver();
if (!ObjectUtils.isEmpty(valueProfiles) && !ObjectUtils.isEmpty(profiles)) {
String msg = String.format("Test class [%s] has been configured with @ActiveProfiles' 'value' [%s] "
+ "and 'profiles' [%s] attributes. Only one declaration of active bean "
+ "definition profiles is permitted per @ActiveProfiles annotation.", declaringClass.getName(),
ObjectUtils.nullSafeToString(valueProfiles), ObjectUtils.nullSafeToString(profiles));
logger.error(msg);
throw new IllegalStateException(msg);
boolean resolverDeclared = !ActiveProfilesResolver.class.equals(resolverClass);
boolean valueDeclared = !ObjectUtils.isEmpty(valueProfiles);
if (resolverDeclared) {
ActiveProfilesResolver resolver = null;
try {
resolver = instantiateClass(resolverClass, ActiveProfilesResolver.class);
}
catch (Exception e) {
String msg = String.format("Could not instantiate ActiveProfilesResolver of "
+ "type [%s] for test class [%s].", resolverClass.getName(), declaringClass.getName());
logger.error(msg);
throw new IllegalStateException(msg, e);
}
if (resolver != null) {
profiles = resolver.resolve(declaringClass);
if (profiles == null) {
String msg = String.format(
"ActiveProfilesResolver [%s] returned a null array of bean definition profiles.",
resolverClass.getName());
logger.error(msg);
throw new IllegalStateException(msg);
}
}
}
else if (!ObjectUtils.isEmpty(valueProfiles)) {
else if (valueDeclared) {
profiles = valueProfiles;
}
@@ -511,6 +530,43 @@ abstract class ContextLoaderUtils {
return StringUtils.toStringArray(activeProfiles);
}
private static void validateActiveProfilesConfiguration(Class<?> declaringClass, ActiveProfiles annotation) {
String[] valueProfiles = annotation.value();
String[] profiles = annotation.profiles();
Class<? extends ActiveProfilesResolver> resolverClass = annotation.resolver();
boolean valueDeclared = !ObjectUtils.isEmpty(valueProfiles);
boolean profilesDeclared = !ObjectUtils.isEmpty(profiles);
boolean resolverDeclared = !ActiveProfilesResolver.class.equals(resolverClass);
String msg = null;
if (valueDeclared && profilesDeclared) {
msg = String.format("Test class [%s] has been configured with @ActiveProfiles' 'value' [%s] "
+ "and 'profiles' [%s] attributes. Only one declaration of active bean "
+ "definition profiles is permitted per @ActiveProfiles annotation.", declaringClass.getName(),
ObjectUtils.nullSafeToString(valueProfiles), ObjectUtils.nullSafeToString(profiles));
}
else if (valueDeclared && resolverDeclared) {
msg = String.format("Test class [%s] has been configured with @ActiveProfiles' 'value' [%s] "
+ "and 'resolver' [%s] attributes. Only one source of active bean "
+ "definition profiles is permitted per @ActiveProfiles annotation, "
+ "either declaritively or programmatically.", declaringClass.getName(),
ObjectUtils.nullSafeToString(valueProfiles), resolverClass.getName());
}
else if (profilesDeclared && resolverDeclared) {
msg = String.format("Test class [%s] has been configured with @ActiveProfiles' 'profiles' [%s] "
+ "and 'resolver' [%s] attributes. Only one source of active bean "
+ "definition profiles is permitted per @ActiveProfiles annotation, "
+ "either declaritively or programmatically.", declaringClass.getName(),
ObjectUtils.nullSafeToString(profiles), resolverClass.getName());
}
if (msg != null) {
logger.error(msg);
throw new IllegalStateException(msg);
}
}
/**
* Build the {@link MergedContextConfiguration merged context configuration} for
* the supplied {@link Class testClass} and {@code defaultContextLoaderClassName},