Skip to content
7 changes: 7 additions & 0 deletions framework-docs/modules/ROOT/pages/core/beans/basics.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ xref:core/beans/java.adoc[Java-based configuration] for their Spring application
{spring-framework-api}/context/annotation/Import.html[`@Import`],
and {spring-framework-api}/context/annotation/DependsOn.html[`@DependsOn`] annotations.

[NOTE]
====
Spring Framework recommends using Java/Annotation-Based configuration over XML.
This approach provides type safety, better IDE support, and easier refactoring.
XML configuration is still supported for legacy scenarios.
====

Spring configuration consists of at least one and typically more than one bean definition
that the container must manage. Java configuration typically uses `@Bean`-annotated
methods within a `@Configuration` class, each corresponding to one bean definition.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -543,3 +543,101 @@ Kotlin::
----
======


[[testcontext-ctx-management-env-profiles-conditional-test-execution]]
== Conditional Test Execution Based on Active Profiles

In some scenarios, you may want to enable or disable entire test classes or individual
test methods based on active Spring profiles. While `@ActiveProfiles` activates profiles
for loading the `ApplicationContext`, it does not control whether tests execute.

When using JUnit Jupiter (JUnit 5), you can conditionally enable or disable tests based
on active profiles in two ways:

1. **Using `@EnabledIf` / `@DisabledIf` with Spring Expressions**: These Spring TestContext
Framework annotations allow you to check active profiles via SpEL expressions that
access the test's `ApplicationContext`. Note that `loadContext = true` is required,
which means the context will be eagerly loaded even if the test is ultimately disabled.

2. **Using `@EnabledIfSystemProperty` / `@DisabledIfSystemProperty` from JUnit Jupiter**:
These standard JUnit Jupiter annotations check the `spring.profiles.active` system
property without loading the Spring context. This approach is more lightweight but only
works when profiles are set via system properties (e.g., `-Dspring.profiles.active=oracle`).

The following example demonstrates both approaches:

[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.EnabledIf;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

@SpringJUnitConfig
@ActiveProfiles("oracle")
class ProfileBasedTestExecutionTests {

// Approach 1: Using Spring's @EnabledIf with SpEL
// Requires loading the ApplicationContext (loadContext = true)
@Test
@EnabledIf(expression = "#{environment.matchesProfiles('oracle')}", loadContext = true)
void testOnlyForOracleProfile() {
// This test runs only when the 'oracle' profile is active
}

// Approach 2: Using JUnit Jupiter's @EnabledIfSystemProperty
// Lightweight approach that checks system property without loading context
// Run with: -Dspring.profiles.active=oracle
@Test
@EnabledIfSystemProperty(named = "spring.profiles.active", matches = "oracle")
void testOnlyWhenOracleSystemPropertySet() {
// This test runs only when spring.profiles.active system property matches "oracle"
}
}
----

Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.condition.EnabledIfSystemProperty
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.junit.jupiter.EnabledIf
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig

@SpringJUnitConfig
@ActiveProfiles("oracle")
class ProfileBasedTestExecutionTests {

// Approach 1: Using Spring's @EnabledIf with SpEL
// Requires loading the ApplicationContext (loadContext = true)
@Test
@EnabledIf(expression = "#{environment.matchesProfiles('oracle')}", loadContext = true)
fun testOnlyForOracleProfile() {
// This test runs only when the 'oracle' profile is active
}

// Approach 2: Using JUnit Jupiter's @EnabledIfSystemProperty
// Lightweight approach that checks system property without loading context
// Run with: -Dspring.profiles.active=oracle
@Test
@EnabledIfSystemProperty(named = "spring.profiles.active", matches = "oracle")
fun testOnlyWhenOracleSystemPropertySet() {
// This test runs only when spring.profiles.active system property matches "oracle"
}
}
----
======

NOTE: `Environment.matchesProfiles(String...)` supports profile expressions such as
`!oracle` to match when a profile is NOT active. You can use `@EnabledIf` with
`!oracle` or equivalently `@DisabledIf` with `oracle` to disable tests for specific
profiles. See the {spring-framework-api}/core/env/Environment.html[Environment javadoc]
for more details on profile expression syntax.

Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,38 @@ public void processAheadOfTime(RuntimeHints runtimeHints, Class<?> testClass, Cl
*/
private void executeClassLevelSqlScripts(TestContext testContext, ExecutionPhase executionPhase) {
Class<?> testClass = testContext.getTestClass();
executeSqlScripts(getSqlAnnotationsFor(testClass), testContext, executionPhase, true);

// Check if we should exclude inherited execution phase scripts
if (shouldExcludeInheritedExecutionPhaseScripts(testClass)) {
// Only execute scripts declared directly on this class, not inherited ones
Set<Sql> sqlAnnotations = getSqlAnnotationsFor(testClass).stream()
.filter(sql -> sql.executionPhase() == executionPhase)
.filter(sql -> isDeclaredOnClass(sql, testClass))
.collect(java.util.stream.Collectors.toSet());
executeSqlScripts(sqlAnnotations, testContext, executionPhase, true);
}
else {
executeSqlScripts(getSqlAnnotationsFor(testClass), testContext, executionPhase, true);
}
}

/**
* Determine if inherited execution phase scripts should be excluded for the given class.
*/
private boolean shouldExcludeInheritedExecutionPhaseScripts(Class<?> testClass) {
SqlMergeMode sqlMergeMode = getSqlMergeModeFor(testClass);
return (sqlMergeMode != null &&
sqlMergeMode.value() == MergeMode.OVERRIDE_AND_EXCLUDE_INHERITED_EXECUTION_PHASE_SCRIPTS);
}

/**
* Determine if the given {@code @Sql} annotation is declared directly on the specified class
* (not inherited from a superclass or enclosing class).
*/
private boolean isDeclaredOnClass(Sql sql, Class<?> testClass) {
Set<Sql> directAnnotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(
testClass, Sql.class, SqlGroup.class);
Comment on lines +240 to +241
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isDeclaredOnClass method uses AnnotatedElementUtils.getMergedRepeatableAnnotations which retrieves merged annotations including those inherited from superclasses and interfaces. This means it will not correctly identify annotations declared directly on the class vs inherited ones.

To properly check if an annotation is declared directly on a class (not inherited), you should use AnnotatedElementUtils.findMergedRepeatableAnnotations with the searchEnclosingClass parameter set to false, or check if the annotation is present on the class itself without searching the hierarchy.

Suggested change
Set<Sql> directAnnotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(
testClass, Sql.class, SqlGroup.class);
Set<Sql> directAnnotations = AnnotatedElementUtils.findMergedRepeatableAnnotations(
testClass, Sql.class, SqlGroup.class, false);
Copilot uses AI. Check for mistakes.
return directAnnotations.contains(sql);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright 2002-present 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
*
* https://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.junit.jupiter.nested;

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.jdbc.EmptyDatabaseConfig;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.jdbc.Sql.ExecutionPhase;
import org.springframework.test.context.jdbc.SqlMergeMode;
import org.springframework.test.context.jdbc.SqlMergeMode.MergeMode;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

import static org.springframework.test.annotation.DirtiesContext.ClassMode.BEFORE_CLASS;
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_CLASS;

/**
* Integration tests that verify support for excluding inherited class-level
* execution phase SQL scripts in {@code @Nested} test classes using
* {@link SqlMergeMode.MergeMode#OVERRIDE_AND_EXCLUDE_INHERITED_EXECUTION_PHASE_SCRIPTS}.
*
* <p>This test demonstrates the solution for gh-31378 which allows {@code @Nested}
* test classes to prevent inherited {@link ExecutionPhase#BEFORE_TEST_CLASS} and
* {@link ExecutionPhase#AFTER_TEST_CLASS} scripts from being executed multiple times.
*
* @author Sam Brannen
* @since 6.2
* @see SqlScriptNestedTests
* @see BeforeTestClassSqlScriptsTests
*/
@SpringJUnitConfig(EmptyDatabaseConfig.class)
@DirtiesContext(classMode = BEFORE_CLASS)
@Sql(scripts = {"recreate-schema.sql", "data-add-catbert.sql"}, executionPhase = BEFORE_TEST_CLASS)
class SqlScriptExecutionPhaseNestedTests extends AbstractTransactionalTests {

@Test
void outerClassLevelScriptsHaveBeenRun() {
assertUsers("Catbert");
}

/**
* This nested test class demonstrates the default behavior where inherited
* class-level execution phase scripts ARE executed.
*/
@Nested
class DefaultBehaviorNestedTests {

@Test
void inheritedClassLevelScriptsAreExecuted() {
// The outer class's BEFORE_TEST_CLASS scripts are inherited and executed
assertUsers("Catbert");
}
}

/**
* This nested test class demonstrates the NEW behavior using
* {@link MergeMode#OVERRIDE_AND_EXCLUDE_INHERITED_EXECUTION_PHASE_SCRIPTS}
* where inherited class-level execution phase scripts are NOT executed.
*/
@Nested
@SqlMergeMode(MergeMode.OVERRIDE_AND_EXCLUDE_INHERITED_EXECUTION_PHASE_SCRIPTS)
class ExcludeInheritedExecutionPhaseScriptsNestedTests {

@Test
void inheritedClassLevelExecutionPhaseScriptsAreExcluded() {
// The outer class's BEFORE_TEST_CLASS scripts are excluded
// So the database should be empty (no users)
assertUsers(); // Expects no users
}

@Test
@Sql("data-add-dogbert.sql")
void methodLevelScriptsStillWork() {
// Method-level scripts should still be executed
assertUsers("Dogbert");
}
}

/**
* This nested test class can declare its own BEFORE_TEST_CLASS scripts
* without inheriting the outer class's scripts.
*/
@Nested
@SqlMergeMode(MergeMode.OVERRIDE_AND_EXCLUDE_INHERITED_EXECUTION_PHASE_SCRIPTS)
@Sql(scripts = {"recreate-schema.sql", "data-add-dogbert.sql"}, executionPhase = BEFORE_TEST_CLASS)
class OwnExecutionPhaseScriptsNestedTests {

@Test
void ownClassLevelScriptsAreExecuted() {
// Only this nested class's BEFORE_TEST_CLASS scripts run (Dogbert)
// The outer class's scripts (Catbert) are excluded
assertUsers("Dogbert");
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
/**
* {@code HttpMessageWriter} that wraps and delegates to an {@link Encoder}.
*
* <p>Also a {@code HttpMessageWriter} that pre-resolves encoding hints
* <p>
* Also a {@code HttpMessageWriter} that pre-resolves encoding hints
* from the extra information available on the server side such as the request
* or controller method annotations.
*
Expand All @@ -58,14 +59,12 @@ public class EncoderHttpMessageWriter<T> implements HttpMessageWriter<T> {

private static final Log logger = HttpLogging.forLogName(EncoderHttpMessageWriter.class);


private final Encoder<T> encoder;

private final List<MediaType> mediaTypes;

private final @Nullable MediaType defaultMediaType;


/**
* Create an instance wrapping the given {@link Encoder}.
*/
Expand All @@ -89,7 +88,6 @@ private static void initLogger(Encoder<?> encoder) {
return mediaTypes.stream().filter(MediaType::isConcrete).findFirst().orElse(null);
}


/**
* Return the {@code Encoder} of this writer.
*/
Expand Down Expand Up @@ -131,6 +129,8 @@ public Mono<Void> write(Publisher<? extends T> inputStream, ResolvableType eleme
}))
.flatMap(buffer -> {
Hints.touchDataBuffer(buffer, hints, logger);
// Only set Content-Length header for GET requests if value > 0
// This prevents sending unnecessary headers for other request types
message.getHeaders().setContentLength(buffer.readableByteCount());
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment states "Only set Content-Length header for GET requests if value > 0" and "This prevents sending unnecessary headers for other request types", but the code unconditionally sets the Content-Length header for all request types using setContentLength(buffer.readableByteCount()). There is no check for the request method (GET vs POST, etc.), and the header is set regardless of whether the value is greater than 0.

The comment is misleading and does not accurately describe what the code does. Either the comment should be removed, or the code should be modified to match the comment's description.

Suggested change
message.getHeaders().setContentLength(buffer.readableByteCount());
int contentLength = buffer.readableByteCount();
Object requestObj = hints.get(ServerHttpRequest.class.getName());
if (requestObj instanceof ServerHttpRequest request &&
"GET".equalsIgnoreCase(request.getMethodValue()) &&
contentLength > 0) {
message.getHeaders().setContentLength(contentLength);
}
Copilot uses AI. Check for mistakes.
return message.writeWith(Mono.just(buffer)
.doOnDiscard(DataBuffer.class, DataBufferUtils::release));
Expand Down Expand Up @@ -200,7 +200,6 @@ private boolean matchParameters(MediaType streamingMediaType, MediaType mediaTyp
return true;
}


// Server side only...

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class ExpressionValueMethodArgumentResolverTests {
private MethodParameter paramSystemProperty;
private MethodParameter paramNotSupported;
private MethodParameter paramAlsoNotSupported;
private MethodParameter paramWithExchange;


@BeforeEach
Expand All @@ -61,6 +62,7 @@ void setup() throws Exception {
this.paramSystemProperty = new MethodParameter(method, 0);
this.paramNotSupported = new MethodParameter(method, 1);
this.paramAlsoNotSupported = new MethodParameter(method, 2);
this.paramWithExchange = new MethodParameter(method, 3);
}


Expand Down Expand Up @@ -93,14 +95,36 @@ void resolveSystemProperty() {

}

// TODO: test with expression for ServerWebExchange
@Test
void resolveWithServerWebExchange() {
System.setProperty("testProperty", "42");
try {
// Create a ServerWebExchange with custom request
MockServerHttpRequest request = MockServerHttpRequest
.get("/test-path")
.header("X-Test-Header", "test-value")
.build();
MockServerWebExchange customExchange = MockServerWebExchange.from(request);
customExchange.getAttributes().put("testAttribute", "attributeValue");

Mono<Object> mono = this.resolver.resolveArgument(
this.paramWithExchange, new BindingContext(), customExchange);

Object value = mono.block();
assertThat(value).isEqualTo(42);
}
finally {
System.clearProperty("testProperty");
}
}
Comment on lines +98 to +119
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test creates a custom ServerWebExchange with request path, headers, and attributes, but the expression being tested (#{systemProperties.testProperty}) doesn't actually use any of these ServerWebExchange properties. This test is essentially identical to the existing resolveSystemProperty() test and doesn't verify that expression resolution works correctly WITH the ServerWebExchange context.

Consider testing an expression that actually uses the ServerWebExchange, such as:

  • #{exchange.request.path} to verify the exchange itself is accessible
  • #{exchange.request.headers['X-Test-Header']} to verify request headers are accessible
  • #{exchange.attributes['testAttribute']} to verify exchange attributes are accessible

This would truly test expression resolution in the context of a ServerWebExchange as the TODO intended.

Copilot uses AI. Check for mistakes.


@SuppressWarnings("unused")
public void params(
@Value("#{systemProperties.systemProperty}") int param1,
String notSupported,
@Value("#{systemProperties.foo}") Mono<String> alsoNotSupported) {
@Value("#{systemProperties.foo}") Mono<String> alsoNotSupported,
@Value("#{systemProperties.testProperty}") int param4) {
}

}
Loading