Compare commits

...

35 Commits

Author SHA1 Message Date
Spring Builds 14fa75c98a Release v6.0.18 2024-03-14 09:18:22 +00:00
rstoyanchev f2fd2f1226 Extract reusable checkSchemeAndPort method
Closes gh-32440
2024-03-14 08:48:42 +00:00
Juergen Hoeller 072ebb6ffc Additional unit tests for operations on empty UriTemplate
See gh-32432

(cherry picked from commit 54a6d89da7)
2024-03-13 18:18:52 +01:00
Kasper Bisgaard 08e7f7efa4 Allow UriTemplate to be built with an empty template
Closes gh-32437
2024-03-13 17:32:48 +01:00
Juergen Hoeller b1fafbf7e1 Upgrade to Reactor 2022.0.17
Includes Groovy 4.0.19, OpenPDF 1.3.42, Mockito 5.11

Closes gh-32421
2024-03-12 20:26:44 +01:00
Juergen Hoeller a3647a8c5e Polishing
(cherry picked from commit 723c94e5ac)
2024-03-12 20:26:24 +01:00
ZeroCyan 5c8d9cd0b2 Fix order of sections in Validation chapter of reference manual
Closes gh-32408
2024-03-10 17:31:30 +01:00
Juergen Hoeller 40d5196243 Polishing 2024-03-08 19:40:45 +01:00
Brian Clozel ec2c9b5d0e Set error on observation in WebClient instrumentation
Prior to this commit, error signals flowing from the client response
publisher in `WebClient` would be set on the `Observation.Context`. This
is enough for the observation convention to collect data about the error
but observation handlers are not notified of this error.

This commit sets the error instead on the observation directly to fix
this issue.

Fixes gh-32399
2024-03-08 11:36:20 +01:00
rstoyanchev 36539bdaa9 Use wrapped response in HandlerFunctionAdapter
webmvc.fn now also uses the StandardServletAsyncWebRequest wrapped response
to enforce lifecycle rules from Servlet spec (section 2.3.3.4).

See gh-32341
2024-03-07 14:51:01 +00:00
rstoyanchev 67ba7dd1da DisconnectedClientHelper recognizes AsyncRequestNotUsableException
See gh-32341
2024-03-06 18:20:31 +00:00
rstoyanchev 7b3fcf2647 Fix Javadoc error 2024-03-05 13:06:12 +00:00
rstoyanchev fd76c33589 Fix Javadoc error 2024-03-05 12:40:32 +00:00
rstoyanchev 1a7a6f421f Backport tests for wrapping of response for async requests
This is a backport of commits 4b96cd and ef0717.

Closes gh-32341
2024-03-05 12:03:48 +00:00
Juergen Hoeller 9ac1feceb5 Restore ability to return original method for proxy-derived method
Closes gh-32365
2024-03-04 23:32:27 +01:00
Juergen Hoeller 7029042e44 Polishing
(cherry picked from commit e9110c0729)
2024-03-04 23:31:51 +01:00
rstoyanchev 1a5661d426 Improve concurrent handling of result in WebAsyncManager
1. Use state transitions
2. Increase synchronized scope in setConcurrentResultAndDispatch

See gh-32341
2024-03-03 20:32:00 +00:00
rstoyanchev b208c63414 Add state and response wrapping to StandardServletAsyncWebRequest
The wrapped response prevents use after AsyncListener onError or completion
to ensure compliance with Servlet Spec 2.3.3.4.

The wrapped response is applied in RequestMappingHandlerAdapter.

The wrapped response raises AsyncRequestNotUsableException that is now
handled in DefaultHandlerExceptionResolver.

See gh-32341
2024-03-03 20:31:45 +00:00
rstoyanchev 814c003b43 Align 5.3.x with 6.1.x
In preparation for a larger update, start by aligning with
6.1.x, which includes changes for gh-32042 and gh-30232.

See gh-32341
2024-02-29 17:46:18 +00:00
Juergen Hoeller 5c34e1d11a Upgrade to Undertow 2.3.12, OpenPDF 1.3.41, JRuby 9.4.6 2024-02-28 21:38:57 +01:00
Juergen Hoeller d57775bbb2 Polishing 2024-02-28 21:38:38 +01:00
Juergen Hoeller b598ad3f33 Polishing 2024-02-28 19:26:11 +01:00
Juergen Hoeller b44ef70bc3 Direct reference to PushBuilder API on Servlet 5.0 baseline
See gh-29435
2024-02-28 19:17:13 +01:00
Sam Brannen d1b3107398 Do not cache Content-Type in ContentCachingResponseWrapper
Based on feedback from several members of the community, we have
decided to revert the caching of the Content-Type header that was
introduced in ContentCachingResponseWrapper in 375e0e6827.

This commit therefore completely removes Content-Type caching in
ContentCachingResponseWrapper and updates the existing tests
accordingly.

To provide guards against future regressions in this area, this commit
also introduces explicit tests for the 6 ways to set the content length
in ContentCachingResponseWrapper and modifies a test in
ShallowEtagHeaderFilterTests to ensure that a Content-Type header set
directly on ContentCachingResponseWrapper is propagated to the
underlying response even if content caching is disabled for the
ShallowEtagHeaderFilter.

See gh-32039
See gh-32317
Closes gh-32321
2024-02-28 10:51:48 +01:00
Sam Brannen 629c560316 Polish ShallowEtagHeaderFilterTests 2024-02-28 10:49:13 +01:00
Sébastien Deleuze 7bf07ef393 Refine *HttpMessageConverter#getContentLength null safety
Closes gh-32333
2024-02-27 15:48:36 +01:00
Sam Brannen ca602ef874 Honor Content-[Type|Length] headers from wrapped response again
Commit 375e0e6827 introduced a regression in
ContentCachingResponseWrapper (CCRW). Specifically, CCRW no longer
honors Content-Type and Content-Length headers that have been set in
the wrapped response and now incorrectly returns null for those header
values if they have not been set directly in the CCRW.

This commit fixes this regression as follows.

- The Content-Type and Content-Length headers set in the wrapped
  response are honored in getContentType(), containsHeader(),
  getHeader(), and getHeaders() unless those headers have been set
  directly in the CCRW.

- In copyBodyToResponse(), the Content-Type in the wrapped response is
  only overridden if the Content-Type has been set directly in the CCRW.

Furthermore, prior to this commit, getHeaderNames() returned duplicates
for the Content-Type and Content-Length headers if they were set in the
wrapped response as well as in CCRW.

This commit fixes that by returning a unique set from getHeaderNames().

This commit also updates ContentCachingResponseWrapperTests to verify
the expected behavior for Content-Type and Content-Length headers that
are set in the wrapped response as well as in CCRW.

See gh-32039
See gh-32317
Closes gh-32321
2024-02-25 17:35:54 +01:00
Sam Brannen e9bf5f5569 Polish ContentCachingResponseWrapper[Tests] 2024-02-25 17:35:54 +01:00
Juergen Hoeller 9a6f636e17 Consistent nullability for internal field access 2024-02-24 08:31:56 +01:00
Sébastien Deleuze 7686f5467e Adapt Hibernate native support for HHH-17643
This commit adapts Hibernate native support to handle
the changes performed as part of HHH-17643 which impacts
Hibernate versions 6.4.3+ and 6.2.21+.

It ignores the BytecodeProvider services loaded by the
service loader feature in order to default to the
"no-op" provider with native.

gh-32314 is expected to remove the need for such
substitutions which are not great for maintainability
by design.

Closes gh-32312
2024-02-22 17:06:53 +01:00
Juergen Hoeller 429c477f6a Polishing 2024-02-22 16:59:53 +01:00
Juergen Hoeller 5187281b50 Polishing 2024-02-21 22:56:21 +01:00
Juergen Hoeller 24a4487050 Add test for cleanup after configuration class creation failure
See gh-23343
2024-02-21 22:47:23 +01:00
Juergen Hoeller 8fba4a448a Polishing 2024-02-16 22:41:50 +01:00
Spring Builds 8f286de773 Next development version (v6.0.18-SNAPSHOT) 2024-02-15 12:34:07 +00:00
81 changed files with 2201 additions and 670 deletions
+2 -2
View File
@@ -39,8 +39,8 @@
** xref:core/resources.adoc[]
** xref:core/validation.adoc[]
*** xref:core/validation/validator.adoc[]
*** xref:core/validation/conversion.adoc[]
*** xref:core/validation/beans-beans.adoc[]
*** xref:core/validation/conversion.adoc[]
*** xref:core/validation/convert.adoc[]
*** xref:core/validation/format.adoc[]
*** xref:core/validation/format-configuring-formatting-globaldatetimeformat.adoc[]
@@ -431,4 +431,4 @@
** xref:languages/groovy.adoc[]
** xref:languages/dynamic.adoc[]
* xref:appendix.adoc[]
* https://github.com/spring-projects/spring-framework/wiki[Wiki]
* https://github.com/spring-projects/spring-framework/wiki[Wiki]
@@ -268,8 +268,8 @@ The actual JPA provider bootstrapping is handed off to the specified executor an
running in parallel, to the application bootstrap thread. The exposed `EntityManagerFactory`
proxy can be injected into other application components and is even able to respond to
`EntityManagerFactoryInfo` configuration inspection. However, once the actual JPA provider
is being accessed by other components (for example, calling `createEntityManager`), those calls
block until the background bootstrapping has completed. In particular, when you use
is being accessed by other components (for example, calling `createEntityManager`), those
calls block until the background bootstrapping has completed. In particular, when you use
Spring Data JPA, make sure to set up deferred bootstrapping for its repositories as well.
@@ -284,9 +284,9 @@ to a newly created `EntityManager` per operation, in effect making its usage thr
It is possible to write code against the plain JPA without any Spring dependencies, by
using an injected `EntityManagerFactory` or `EntityManager`. Spring can understand the
`@PersistenceUnit` and `@PersistenceContext` annotations both at the field and the method level
if a `PersistenceAnnotationBeanPostProcessor` is enabled. The following example shows a plain
JPA DAO implementation that uses the `@PersistenceUnit` annotation:
`@PersistenceUnit` and `@PersistenceContext` annotations both at the field and the method
level if a `PersistenceAnnotationBeanPostProcessor` is enabled. The following example
shows a plain JPA DAO implementation that uses the `@PersistenceUnit` annotation:
[tabs]
======
+8 -8
View File
@@ -11,21 +11,21 @@ dependencies {
api(platform("io.micrometer:micrometer-bom:1.10.13"))
api(platform("io.netty:netty-bom:4.1.107.Final"))
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
api(platform("io.projectreactor:reactor-bom:2022.0.16"))
api(platform("io.projectreactor:reactor-bom:2022.0.17"))
api(platform("io.rsocket:rsocket-bom:1.1.3"))
api(platform("org.apache.groovy:groovy-bom:4.0.18"))
api(platform("org.apache.groovy:groovy-bom:4.0.19"))
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
api(platform("org.eclipse.jetty:jetty-bom:11.0.20"))
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4"))
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.4.0"))
api(platform("org.junit:junit-bom:5.9.3"))
api(platform("org.mockito:mockito-bom:5.10.0"))
api(platform("org.mockito:mockito-bom:5.11.0"))
constraints {
api("com.fasterxml:aalto-xml:1.3.2")
api("com.fasterxml.woodstox:woodstox-core:6.5.1")
api("com.github.ben-manes.caffeine:caffeine:3.1.8")
api("com.github.librepdf:openpdf:1.3.40")
api("com.github.librepdf:openpdf:1.3.42")
api("com.google.code.findbugs:findbugs:3.0.1")
api("com.google.code.findbugs:jsr305:3.0.2")
api("com.google.code.gson:gson:2.10.1")
@@ -54,9 +54,9 @@ dependencies {
api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE")
api("io.reactivex.rxjava3:rxjava:3.1.8")
api("io.smallrye.reactive:mutiny:1.10.0")
api("io.undertow:undertow-core:2.3.11.Final")
api("io.undertow:undertow-servlet:2.3.11.Final")
api("io.undertow:undertow-websockets-jsr:2.3.11.Final")
api("io.undertow:undertow-core:2.3.12.Final")
api("io.undertow:undertow-servlet:2.3.12.Final")
api("io.undertow:undertow-websockets-jsr:2.3.12.Final")
api("io.vavr:vavr:0.10.4")
api("jakarta.activation:jakarta.activation-api:2.0.1")
api("jakarta.annotation:jakarta.annotation-api:2.0.0")
@@ -126,7 +126,7 @@ dependencies {
api("org.hibernate:hibernate-validator:7.0.5.Final")
api("org.hsqldb:hsqldb:2.7.2")
api("org.javamoney:moneta:1.4.2")
api("org.jruby:jruby:9.4.5.0")
api("org.jruby:jruby:9.4.6.0")
api("org.junit.support:testng-engine:1.0.4")
api("org.mozilla:rhino:1.7.14")
api("org.ogce:xpp3:1.1.6")
+1 -1
View File
@@ -1,4 +1,4 @@
version=6.0.17-SNAPSHOT
version=6.0.18
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 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.
@@ -186,10 +186,10 @@ public abstract class AopUtils {
* this method resolves bridge methods in order to retrieve attributes from
* the <i>original</i> method definition.
* @param method the method to be invoked, which may come from an interface
* @param targetClass the target class for the current invocation.
* May be {@code null} or may not even implement the method.
* @param targetClass the target class for the current invocation
* (can be {@code null} or may not even implement the method)
* @return the specific target method, or the original method if the
* {@code targetClass} doesn't implement it or is {@code null}
* {@code targetClass} does not implement it
* @see org.springframework.util.ClassUtils#getMostSpecificMethod
*/
public static Method getMostSpecificMethod(Method method, @Nullable Class<?> targetClass) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2024 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.
@@ -17,29 +17,35 @@
package org.springframework.aop.support;
import java.lang.reflect.Method;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.MethodMatcher;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.interceptor.ExposeInvocationInterceptor;
import org.springframework.aop.target.EmptyTargetSource;
import org.springframework.aop.testfixture.interceptor.NopInterceptor;
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.core.ResolvableType;
import org.springframework.core.testfixture.io.SerializationTestUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Rod Johnson
* @author Chris Beams
* @author Sebastien Deleuze
* @author Juergen Hoeller
*/
public class AopUtilsTests {
class AopUtilsTests {
@Test
public void testPointcutCanNeverApply() {
void testPointcutCanNeverApply() {
class TestPointcut extends StaticMethodMatcherPointcut {
@Override
public boolean matches(Method method, @Nullable Class<?> clazzy) {
@@ -52,13 +58,13 @@ public class AopUtilsTests {
}
@Test
public void testPointcutAlwaysApplies() {
void testPointcutAlwaysApplies() {
assertThat(AopUtils.canApply(new DefaultPointcutAdvisor(new NopInterceptor()), Object.class)).isTrue();
assertThat(AopUtils.canApply(new DefaultPointcutAdvisor(new NopInterceptor()), TestBean.class)).isTrue();
}
@Test
public void testPointcutAppliesToOneMethodOnObject() {
void testPointcutAppliesToOneMethodOnObject() {
class TestPointcut extends StaticMethodMatcherPointcut {
@Override
public boolean matches(Method method, @Nullable Class<?> clazz) {
@@ -78,7 +84,7 @@ public class AopUtilsTests {
* that's subverted the singleton construction limitation.
*/
@Test
public void testCanonicalFrameworkClassesStillCanonicalOnDeserialization() throws Exception {
void testCanonicalFrameworkClassesStillCanonicalOnDeserialization() throws Exception {
assertThat(SerializationTestUtils.serializeAndDeserialize(MethodMatcher.TRUE)).isSameAs(MethodMatcher.TRUE);
assertThat(SerializationTestUtils.serializeAndDeserialize(ClassFilter.TRUE)).isSameAs(ClassFilter.TRUE);
assertThat(SerializationTestUtils.serializeAndDeserialize(Pointcut.TRUE)).isSameAs(Pointcut.TRUE);
@@ -88,4 +94,45 @@ public class AopUtilsTests {
assertThat(SerializationTestUtils.serializeAndDeserialize(ExposeInvocationInterceptor.INSTANCE)).isSameAs(ExposeInvocationInterceptor.INSTANCE);
}
@Test
void testInvokeJoinpointUsingReflection() throws Throwable {
String name = "foo";
TestBean testBean = new TestBean(name);
Method method = ReflectionUtils.findMethod(TestBean.class, "getName");
Object result = AopUtils.invokeJoinpointUsingReflection(testBean, method, new Object[0]);
assertThat(result).isEqualTo(name);
}
@Test // gh-32365
void mostSpecificMethodBetweenJdkProxyAndTarget() throws Exception {
Class<?> proxyClass = new ProxyFactory(new WithInterface()).getProxyClass(getClass().getClassLoader());
Method specificMethod = AopUtils.getMostSpecificMethod(proxyClass.getMethod("handle", List.class), WithInterface.class);
assertThat(ResolvableType.forMethodParameter(specificMethod, 0).getGeneric().toClass()).isEqualTo(String.class);
}
@Test // gh-32365
void mostSpecificMethodBetweenCglibProxyAndTarget() throws Exception {
Class<?> proxyClass = new ProxyFactory(new WithoutInterface()).getProxyClass(getClass().getClassLoader());
Method specificMethod = AopUtils.getMostSpecificMethod(proxyClass.getMethod("handle", List.class), WithoutInterface.class);
assertThat(ResolvableType.forMethodParameter(specificMethod, 0).getGeneric().toClass()).isEqualTo(String.class);
}
interface ProxyInterface {
void handle(List<String> list);
}
static class WithInterface implements ProxyInterface {
public void handle(List<String> list) {
}
}
static class WithoutInterface {
public void handle(List<String> list) {
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -161,6 +161,9 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
/** Map from scope identifier String to corresponding Scope. */
private final Map<String, Scope> scopes = new LinkedHashMap<>(8);
/** Application startup metrics. **/
private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT;
/** Map from bean name to merged RootBeanDefinition. */
private final Map<String, RootBeanDefinition> mergedBeanDefinitions = new ConcurrentHashMap<>(256);
@@ -171,8 +174,6 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
private final ThreadLocal<Object> prototypesCurrentlyInCreation =
new NamedThreadLocal<>("Prototype beans currently in creation");
/** Application startup metrics. **/
private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT;
/**
* Create a new AbstractBeanFactory.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -609,13 +609,10 @@ class ConstructorResolver {
String argDesc = StringUtils.collectionToCommaDelimitedString(argTypes);
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"No matching factory method found on class [" + factoryClass.getName() + "]: " +
(mbd.getFactoryBeanName() != null ?
"factory bean '" + mbd.getFactoryBeanName() + "'; " : "") +
(mbd.getFactoryBeanName() != null ? "factory bean '" + mbd.getFactoryBeanName() + "'; " : "") +
"factory method '" + mbd.getFactoryMethodName() + "(" + argDesc + ")'. " +
"Check that a method with the specified name " +
(minNrOfArgs > 0 ? "and arguments " : "") +
"exists and that it is " +
(isStatic ? "static" : "non-static") + ".");
"Check that a method with the specified name " + (minNrOfArgs > 0 ? "and arguments " : "") +
"exists and that it is " + (isStatic ? "static" : "non-static") + ".");
}
else if (void.class == factoryMethodToUse.getReturnType()) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -567,16 +567,16 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
*/
protected void destroyBean(String beanName, @Nullable DisposableBean bean) {
// Trigger destruction of dependent beans first...
Set<String> dependencies;
Set<String> dependentBeanNames;
synchronized (this.dependentBeanMap) {
// Within full synchronization in order to guarantee a disconnected Set
dependencies = this.dependentBeanMap.remove(beanName);
dependentBeanNames = this.dependentBeanMap.remove(beanName);
}
if (dependencies != null) {
if (dependentBeanNames != null) {
if (logger.isTraceEnabled()) {
logger.trace("Retrieved dependent beans for bean '" + beanName + "': " + dependencies);
logger.trace("Retrieved dependent beans for bean '" + beanName + "': " + dependentBeanNames);
}
for (String dependentBeanName : dependencies) {
for (String dependentBeanName : dependentBeanNames) {
destroySingleton(dependentBeanName);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 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,12 +29,10 @@ import org.springframework.core.MethodClassKey;
import org.springframework.lang.Nullable;
/**
* Abstract implementation of {@link JCacheOperationSource} that caches attributes
* Abstract implementation of {@link JCacheOperationSource} that caches operations
* for methods and implements a fallback policy: 1. specific target method;
* 2. declaring method.
*
* <p>This implementation caches attributes by method after they are first used.
*
* @author Stephane Nicoll
* @author Juergen Hoeller
* @since 4.1
@@ -43,24 +41,25 @@ import org.springframework.lang.Nullable;
public abstract class AbstractFallbackJCacheOperationSource implements JCacheOperationSource {
/**
* Canonical value held in cache to indicate no caching attribute was
* found for this method and we don't need to look again.
* Canonical value held in cache to indicate no cache operation was
* found for this method, and we don't need to look again.
*/
private static final Object NULL_CACHING_ATTRIBUTE = new Object();
private static final Object NULL_CACHING_MARKER = new Object();
protected final Log logger = LogFactory.getLog(getClass());
private final Map<MethodClassKey, Object> cache = new ConcurrentHashMap<>(1024);
private final Map<MethodClassKey, Object> operationCache = new ConcurrentHashMap<>(1024);
@Override
@Nullable
public JCacheOperation<?> getCacheOperation(Method method, @Nullable Class<?> targetClass) {
MethodClassKey cacheKey = new MethodClassKey(method, targetClass);
Object cached = this.cache.get(cacheKey);
Object cached = this.operationCache.get(cacheKey);
if (cached != null) {
return (cached != NULL_CACHING_ATTRIBUTE ? (JCacheOperation<?>) cached : null);
return (cached != NULL_CACHING_MARKER ? (JCacheOperation<?>) cached : null);
}
else {
JCacheOperation<?> operation = computeCacheOperation(method, targetClass);
@@ -68,10 +67,10 @@ public abstract class AbstractFallbackJCacheOperationSource implements JCacheOpe
if (logger.isDebugEnabled()) {
logger.debug("Adding cacheable method '" + method.getName() + "' with operation: " + operation);
}
this.cache.put(cacheKey, operation);
this.operationCache.put(cacheKey, operation);
}
else {
this.cache.put(cacheKey, NULL_CACHING_ATTRIBUTE);
this.operationCache.put(cacheKey, NULL_CACHING_MARKER);
}
return operation;
}
@@ -84,7 +83,7 @@ public abstract class AbstractFallbackJCacheOperationSource implements JCacheOpe
return null;
}
// The method may be on an interface, but we need attributes from the target class.
// The method may be on an interface, but we need metadata from the target class.
// If the target class is null, the method will be unchanged.
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2024 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.
@@ -212,10 +212,8 @@ public abstract class AnnotationJCacheOperationSource extends AbstractFallbackJC
for (Class<?> parameterType : parameterTypes) {
parameters.add(parameterType.getName());
}
return method.getDeclaringClass().getName()
+ '.' + method.getName()
+ '(' + StringUtils.collectionToCommaDelimitedString(parameters) + ')';
return method.getDeclaringClass().getName() + '.' + method.getName() +
'(' + StringUtils.collectionToCommaDelimitedString(parameters) + ')';
}
private int countNonNull(Object... instances) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -46,6 +46,7 @@ public class BeanFactoryJCacheOperationSourceAdvisor extends AbstractBeanFactory
* Set the cache operation attribute source which is used to find cache
* attributes. This should usually be identical to the source reference
* set on the cache interceptor itself.
* @see JCacheInterceptor#setCacheOperationSource
*/
public void setCacheOperationSource(JCacheOperationSource cacheOperationSource) {
this.pointcut.setCacheOperationSource(cacheOperationSource);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2024 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,7 +34,7 @@ public interface JCacheOperationSource {
* Return the cache operations for this method, or {@code null}
* if the method contains no <em>JSR-107</em> related metadata.
* @param method the method to introspect
* @param targetClass the target class (may be {@code null}, in which case
* @param targetClass the target class (can be {@code null}, in which case
* the declaring class of the method must be used)
* @return the cache operation for this method, or {@code null} if none found
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -24,7 +24,7 @@ import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
/**
* A Pointcut that matches if the underlying {@link JCacheOperationSource}
* A {@code Pointcut} that matches if the underlying {@link JCacheOperationSource}
* has an operation for a given method.
*
* @author Stephane Nicoll
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 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.
@@ -32,20 +32,16 @@ import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
/**
* Abstract implementation of {@link CacheOperation} that caches attributes
* Abstract implementation of {@link CacheOperationSource} that caches operations
* for methods and implements a fallback policy: 1. specific target method;
* 2. target class; 3. declaring method; 4. declaring class/interface.
*
* <p>Defaults to using the target class's caching attribute if none is
* associated with the target method. Any caching attribute associated with
* the target method completely overrides a class caching attribute.
* <p>Defaults to using the target class's declared cache operations if none are
* associated with the target method. Any cache operations associated with
* the target method completely override any class-level declarations.
* If none found on the target class, the interface that the invoked method
* has been called through (in case of a JDK proxy) will be checked.
*
* <p>This implementation caches attributes by method after they are first
* used. If it is ever desirable to allow dynamic changing of cacheable
* attributes (which is very unlikely), caching could be made configurable.
*
* @author Costin Leau
* @author Juergen Hoeller
* @since 3.1
@@ -53,10 +49,10 @@ import org.springframework.util.ClassUtils;
public abstract class AbstractFallbackCacheOperationSource implements CacheOperationSource {
/**
* Canonical value held in cache to indicate no caching attribute was
* found for this method and we don't need to look again.
* Canonical value held in cache to indicate no cache operation was
* found for this method, and we don't need to look again.
*/
private static final Collection<CacheOperation> NULL_CACHING_ATTRIBUTE = Collections.emptyList();
private static final Collection<CacheOperation> NULL_CACHING_MARKER = Collections.emptyList();
/**
@@ -71,14 +67,14 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera
* <p>As this base class is not marked Serializable, the cache will be recreated
* after serialization - provided that the concrete subclass is Serializable.
*/
private final Map<Object, Collection<CacheOperation>> attributeCache = new ConcurrentHashMap<>(1024);
private final Map<Object, Collection<CacheOperation>> operationCache = new ConcurrentHashMap<>(1024);
/**
* Determine the caching attribute for this method invocation.
* <p>Defaults to the class's caching attribute if no method attribute is found.
* Determine the cache operations for this method invocation.
* <p>Defaults to class-declared metadata if no method-level metadata is found.
* @param method the method for the current invocation (never {@code null})
* @param targetClass the target class for this invocation (may be {@code null})
* @param targetClass the target class for this invocation (can be {@code null})
* @return {@link CacheOperation} for this method, or {@code null} if the method
* is not cacheable
*/
@@ -90,21 +86,21 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera
}
Object cacheKey = getCacheKey(method, targetClass);
Collection<CacheOperation> cached = this.attributeCache.get(cacheKey);
Collection<CacheOperation> cached = this.operationCache.get(cacheKey);
if (cached != null) {
return (cached != NULL_CACHING_ATTRIBUTE ? cached : null);
return (cached != NULL_CACHING_MARKER ? cached : null);
}
else {
Collection<CacheOperation> cacheOps = computeCacheOperations(method, targetClass);
if (cacheOps != null) {
if (logger.isTraceEnabled()) {
logger.trace("Adding cacheable method '" + method.getName() + "' with attribute: " + cacheOps);
logger.trace("Adding cacheable method '" + method.getName() + "' with operations: " + cacheOps);
}
this.attributeCache.put(cacheKey, cacheOps);
this.operationCache.put(cacheKey, cacheOps);
}
else {
this.attributeCache.put(cacheKey, NULL_CACHING_ATTRIBUTE);
this.operationCache.put(cacheKey, NULL_CACHING_MARKER);
}
return cacheOps;
}
@@ -129,7 +125,7 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera
return null;
}
// The method may be on an interface, but we need attributes from the target class.
// The method may be on an interface, but we need metadata from the target class.
// If the target class is null, the method will be unchanged.
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
@@ -163,19 +159,19 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera
/**
* Subclasses need to implement this to return the caching attribute for the
* Subclasses need to implement this to return the cache operations for the
* given class, if any.
* @param clazz the class to retrieve the attribute for
* @return all caching attribute associated with this class, or {@code null} if none
* @param clazz the class to retrieve the cache operations for
* @return all cache operations associated with this class, or {@code null} if none
*/
@Nullable
protected abstract Collection<CacheOperation> findCacheOperations(Class<?> clazz);
/**
* Subclasses need to implement this to return the caching attribute for the
* Subclasses need to implement this to return the cache operations for the
* given method, if any.
* @param method the method to retrieve the attribute for
* @return all caching attribute associated with this method, or {@code null} if none
* @param method the method to retrieve the cache operations for
* @return all cache operations associated with this method, or {@code null} if none
*/
@Nullable
protected abstract Collection<CacheOperation> findCacheOperations(Method method);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2024 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.
@@ -54,7 +54,7 @@ public interface CacheOperationSource {
* Return the collection of cache operations for this method,
* or {@code null} if the method contains no <em>cacheable</em> annotations.
* @param method the method to introspect
* @param targetClass the target class (may be {@code null}, in which case
* @param targetClass the target class (can be {@code null}, in which case
* the declaring class of the method must be used)
* @return all cache operations for this method, or {@code null} if none found
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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 @@ import org.springframework.util.ObjectUtils;
/**
* A {@code Pointcut} that matches if the underlying {@link CacheOperationSource}
* has an attribute for a given method.
* has an operation for a given method.
*
* @author Costin Leau
* @author Juergen Hoeller
@@ -36,7 +36,7 @@ import org.springframework.util.ObjectUtils;
* @since 3.1
*/
@SuppressWarnings("serial")
class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable {
final class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable {
@Nullable
private CacheOperationSource cacheOperationSource;
@@ -78,7 +78,7 @@ class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implement
* {@link ClassFilter} that delegates to {@link CacheOperationSource#isCandidateClass}
* for filtering classes whose methods are not worth searching to begin with.
*/
private class CacheOperationSourceClassFilter implements ClassFilter {
private final class CacheOperationSourceClassFilter implements ClassFilter {
@Override
public boolean matches(Class<?> clazz) {
@@ -88,6 +88,7 @@ class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implement
return (cacheOperationSource == null || cacheOperationSource.isCandidateClass(clazz));
}
@Nullable
private CacheOperationSource getCacheOperationSource() {
return cacheOperationSource;
}
@@ -95,7 +96,7 @@ class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implement
@Override
public boolean equals(@Nullable Object other) {
return (this == other || (other instanceof CacheOperationSourceClassFilter that &&
ObjectUtils.nullSafeEquals(cacheOperationSource, that.getCacheOperationSource())));
ObjectUtils.nullSafeEquals(getCacheOperationSource(), that.getCacheOperationSource())));
}
@Override
@@ -105,9 +106,8 @@ class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implement
@Override
public String toString() {
return CacheOperationSourceClassFilter.class.getName() + ": " + cacheOperationSource;
return CacheOperationSourceClassFilter.class.getName() + ": " + getCacheOperationSource();
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -73,7 +73,6 @@ final class ConfigurationClass {
* Create a new {@link ConfigurationClass} with the given name.
* @param metadataReader reader used to parse the underlying {@link Class}
* @param beanName must not be {@code null}
* @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass)
*/
ConfigurationClass(MetadataReader metadataReader, String beanName) {
Assert.notNull(beanName, "Bean name must not be null");
@@ -87,10 +86,10 @@ final class ConfigurationClass {
* using the {@link Import} annotation or automatically processed as a nested
* configuration class (if importedBy is not {@code null}).
* @param metadataReader reader used to parse the underlying {@link Class}
* @param importedBy the configuration class importing this one or {@code null}
* @param importedBy the configuration class importing this one
* @since 3.1.1
*/
ConfigurationClass(MetadataReader metadataReader, @Nullable ConfigurationClass importedBy) {
ConfigurationClass(MetadataReader metadataReader, ConfigurationClass importedBy) {
this.metadata = metadataReader.getAnnotationMetadata();
this.resource = metadataReader.getResource();
this.importedBy.add(importedBy);
@@ -100,7 +99,6 @@ final class ConfigurationClass {
* Create a new {@link ConfigurationClass} with the given name.
* @param clazz the underlying {@link Class} to represent
* @param beanName name of the {@code @Configuration} class bean
* @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass)
*/
ConfigurationClass(Class<?> clazz, String beanName) {
Assert.notNull(beanName, "Bean name must not be null");
@@ -114,10 +112,10 @@ final class ConfigurationClass {
* using the {@link Import} annotation or automatically processed as a nested
* configuration class (if imported is {@code true}).
* @param clazz the underlying {@link Class} to represent
* @param importedBy the configuration class importing this one (or {@code null})
* @param importedBy the configuration class importing this one
* @since 3.1.1
*/
ConfigurationClass(Class<?> clazz, @Nullable ConfigurationClass importedBy) {
ConfigurationClass(Class<?> clazz, ConfigurationClass importedBy) {
this.metadata = AnnotationMetadata.introspect(clazz);
this.resource = new DescriptiveResource(clazz.getName());
this.importedBy.add(importedBy);
@@ -127,7 +125,6 @@ final class ConfigurationClass {
* Create a new {@link ConfigurationClass} with the given name.
* @param metadata the metadata for the underlying class to represent
* @param beanName name of the {@code @Configuration} class bean
* @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass)
*/
ConfigurationClass(AnnotationMetadata metadata, String beanName) {
Assert.notNull(beanName, "Bean name must not be null");
@@ -149,12 +146,12 @@ final class ConfigurationClass {
return ClassUtils.getShortName(getMetadata().getClassName());
}
void setBeanName(String beanName) {
void setBeanName(@Nullable String beanName) {
this.beanName = beanName;
}
@Nullable
public String getBeanName() {
String getBeanName() {
return this.beanName;
}
@@ -164,7 +161,7 @@ final class ConfigurationClass {
* @since 3.1.1
* @see #getImportedBy()
*/
public boolean isImported() {
boolean isImported() {
return !this.importedBy.isEmpty();
}
@@ -198,6 +195,10 @@ final class ConfigurationClass {
this.importedResources.put(importedResource, readerClass);
}
Map<String, Class<? extends BeanDefinitionReader>> getImportedResources() {
return this.importedResources;
}
void addImportBeanDefinitionRegistrar(ImportBeanDefinitionRegistrar registrar, AnnotationMetadata importingClassMetadata) {
this.importBeanDefinitionRegistrars.put(registrar, importingClassMetadata);
}
@@ -206,10 +207,6 @@ final class ConfigurationClass {
return this.importBeanDefinitionRegistrars;
}
Map<String, Class<? extends BeanDefinitionReader>> getImportedResources() {
return this.importedResources;
}
void validate(ProblemReporter problemReporter) {
Map<String, Object> attributes = this.metadata.getAnnotationAttributes(Configuration.class.getName());
@@ -135,17 +135,6 @@ import org.springframework.util.ReflectionUtils;
public abstract class AbstractApplicationContext extends DefaultResourceLoader
implements ConfigurableApplicationContext {
/**
* The name of the {@link LifecycleProcessor} bean in the context.
* If none is supplied, a {@link DefaultLifecycleProcessor} is used.
* @since 3.0
* @see org.springframework.context.LifecycleProcessor
* @see org.springframework.context.support.DefaultLifecycleProcessor
* @see #start()
* @see #stop()
*/
public static final String LIFECYCLE_PROCESSOR_BEAN_NAME = "lifecycleProcessor";
/**
* The name of the {@link MessageSource} bean in the context.
* If none is supplied, message resolution is delegated to the parent.
@@ -166,6 +155,17 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
*/
public static final String APPLICATION_EVENT_MULTICASTER_BEAN_NAME = "applicationEventMulticaster";
/**
* The name of the {@link LifecycleProcessor} bean in the context.
* If none is supplied, a {@link DefaultLifecycleProcessor} is used.
* @since 3.0
* @see org.springframework.context.LifecycleProcessor
* @see org.springframework.context.support.DefaultLifecycleProcessor
* @see #start()
* @see #stop()
*/
public static final String LIFECYCLE_PROCESSOR_BEAN_NAME = "lifecycleProcessor";
static {
// Eagerly load the ContextClosedEvent class to avoid weird classloader issues
@@ -796,8 +796,9 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
}
/**
* Initialize the MessageSource.
* Use parent's if none defined in this context.
* Initialize the {@link MessageSource}.
* <p>Uses parent's {@code MessageSource} if none defined in this context.
* @see #MESSAGE_SOURCE_BEAN_NAME
*/
protected void initMessageSource() {
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
@@ -827,8 +828,9 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
}
/**
* Initialize the ApplicationEventMulticaster.
* Uses SimpleApplicationEventMulticaster if none defined in the context.
* Initialize the {@link ApplicationEventMulticaster}.
* <p>Uses {@link SimpleApplicationEventMulticaster} if none defined in the context.
* @see #APPLICATION_EVENT_MULTICASTER_BEAN_NAME
* @see org.springframework.context.event.SimpleApplicationEventMulticaster
*/
protected void initApplicationEventMulticaster() {
@@ -851,15 +853,16 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
}
/**
* Initialize the LifecycleProcessor.
* Uses DefaultLifecycleProcessor if none defined in the context.
* Initialize the {@link LifecycleProcessor}.
* <p>Uses {@link DefaultLifecycleProcessor} if none defined in the context.
* @since 3.0
* @see #LIFECYCLE_PROCESSOR_BEAN_NAME
* @see org.springframework.context.support.DefaultLifecycleProcessor
*/
protected void initLifecycleProcessor() {
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
if (beanFactory.containsLocalBean(LIFECYCLE_PROCESSOR_BEAN_NAME)) {
this.lifecycleProcessor =
beanFactory.getBean(LIFECYCLE_PROCESSOR_BEAN_NAME, LifecycleProcessor.class);
this.lifecycleProcessor = beanFactory.getBean(LIFECYCLE_PROCESSOR_BEAN_NAME, LifecycleProcessor.class);
if (logger.isTraceEnabled()) {
logger.trace("Using LifecycleProcessor [" + this.lifecycleProcessor + "]");
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 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.
@@ -98,8 +98,8 @@ public class AsyncAnnotationBeanPostProcessor extends AbstractBeanFactoryAwareAd
* applying the corresponding default if a supplier is not resolvable.
* @since 5.1
*/
public void configure(
@Nullable Supplier<Executor> executor, @Nullable Supplier<AsyncUncaughtExceptionHandler> exceptionHandler) {
public void configure(@Nullable Supplier<Executor> executor,
@Nullable Supplier<AsyncUncaughtExceptionHandler> exceptionHandler) {
this.executor = executor;
this.exceptionHandler = exceptionHandler;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -268,6 +268,7 @@ public class ClassPathBeanDefinitionScannerTests {
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context);
scanner.setIncludeAnnotationConfig(false);
scanner.scan("org.springframework.context.annotation2");
assertThatIllegalStateException().isThrownBy(() -> scanner.scan(BASE_PACKAGE))
.withMessageContaining("myNamedDao")
.withMessageContaining(NamedStubDao.class.getName())
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 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.
@@ -35,6 +35,8 @@ import org.springframework.aop.support.AopUtils;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.ObjectProvider;
@@ -321,8 +323,8 @@ class ConfigurationClassPostProcessorTests {
ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor();
pp.setEnvironment(new StandardEnvironment());
pp.postProcessBeanFactory(beanFactory);
assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() ->
beanFactory.getBean(SimpleComponent.class));
assertThatExceptionOfType(NoSuchBeanDefinitionException.class)
.isThrownBy(() -> beanFactory.getBean(SimpleComponent.class));
}
@Test
@@ -372,11 +374,11 @@ class ConfigurationClassPostProcessorTests {
beanFactory.registerBeanDefinition("config", new RootBeanDefinition(SingletonBeanConfig.class));
beanFactory.setAllowBeanDefinitionOverriding(false);
ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor();
assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() ->
pp.postProcessBeanFactory(beanFactory))
.withMessageContaining("bar")
.withMessageContaining("SingletonBeanConfig")
.withMessageContaining(TestBean.class.getName());
assertThatExceptionOfType(BeanDefinitionStoreException.class)
.isThrownBy(() -> pp.postProcessBeanFactory(beanFactory))
.withMessageContaining("bar")
.withMessageContaining("SingletonBeanConfig")
.withMessageContaining(TestBean.class.getName());
}
@Test // gh-25430
@@ -429,12 +431,12 @@ class ConfigurationClassPostProcessorTests {
ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor();
pp.postProcessBeanFactory(beanFactory);
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() ->
beanFactory.getBean(Bar.class))
.withMessageContaining("OverridingSingletonBeanConfig.foo")
.withMessageContaining(ExtendedFoo.class.getName())
.withMessageContaining(Foo.class.getName())
.withMessageContaining("InvalidOverridingSingletonBeanConfig");
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> beanFactory.getBean(Bar.class))
.withMessageContaining("OverridingSingletonBeanConfig.foo")
.withMessageContaining(ExtendedFoo.class.getName())
.withMessageContaining(Foo.class.getName())
.withMessageContaining("InvalidOverridingSingletonBeanConfig");
}
@Test // SPR-15384
@@ -944,8 +946,8 @@ class ConfigurationClassPostProcessorTests {
beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(ConcreteConfig.class));
beanFactory.registerBeanDefinition("serviceBeanProvider", new RootBeanDefinition(ServiceBeanProvider.class));
new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory);
beanFactory.preInstantiateSingletons();
beanFactory.preInstantiateSingletons();
beanFactory.getBean(ServiceBean.class);
}
@@ -958,8 +960,8 @@ class ConfigurationClassPostProcessorTests {
beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(ConcreteConfigWithDefaultMethods.class));
beanFactory.registerBeanDefinition("serviceBeanProvider", new RootBeanDefinition(ServiceBeanProvider.class));
new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory);
beanFactory.preInstantiateSingletons();
beanFactory.preInstantiateSingletons();
beanFactory.getBean(ServiceBean.class);
}
@@ -972,11 +974,25 @@ class ConfigurationClassPostProcessorTests {
beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(ConcreteConfigWithDefaultMethods.class.getName()));
beanFactory.registerBeanDefinition("serviceBeanProvider", new RootBeanDefinition(ServiceBeanProvider.class.getName()));
new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory);
beanFactory.preInstantiateSingletons();
beanFactory.preInstantiateSingletons();
beanFactory.getBean(ServiceBean.class);
}
@Test
void testConfigWithFailingInit() { // gh-23343
AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor();
bpp.setBeanFactory(beanFactory);
beanFactory.addBeanPostProcessor(bpp);
beanFactory.addBeanPostProcessor(new CommonAnnotationBeanPostProcessor());
beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(ConcreteConfigWithFailingInit.class));
new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory);
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(beanFactory::preInstantiateSingletons);
assertThat(beanFactory.containsSingleton("configClass")).isFalse();
assertThat(beanFactory.containsSingleton("provider")).isFalse();
}
@Test
void testCircularDependency() {
AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor();
@@ -985,16 +1001,17 @@ class ConfigurationClassPostProcessorTests {
beanFactory.registerBeanDefinition("configClass1", new RootBeanDefinition(A.class));
beanFactory.registerBeanDefinition("configClass2", new RootBeanDefinition(AStrich.class));
new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory);
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(
beanFactory::preInstantiateSingletons)
.withMessageContaining("Circular reference");
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(beanFactory::preInstantiateSingletons)
.withMessageContaining("Circular reference");
}
@Test
void testCircularDependencyWithApplicationContext() {
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() ->
new AnnotationConfigApplicationContext(A.class, AStrich.class))
.withMessageContaining("Circular reference");
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> new AnnotationConfigApplicationContext(A.class, AStrich.class))
.withMessageContaining("Circular reference");
}
@Test
@@ -1048,9 +1065,7 @@ class ConfigurationClassPostProcessorTests {
void testCollectionArgumentOnBeanMethod() {
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CollectionArgumentConfiguration.class, TestBean.class);
CollectionArgumentConfiguration bean = ctx.getBean(CollectionArgumentConfiguration.class);
assertThat(bean.testBeans).isNotNull();
assertThat(bean.testBeans).hasSize(1);
assertThat(bean.testBeans.get(0)).isSameAs(ctx.getBean(TestBean.class));
assertThat(bean.testBeans).containsExactly(ctx.getBean(TestBean.class));
ctx.close();
}
@@ -1058,8 +1073,7 @@ class ConfigurationClassPostProcessorTests {
void testEmptyCollectionArgumentOnBeanMethod() {
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CollectionArgumentConfiguration.class);
CollectionArgumentConfiguration bean = ctx.getBean(CollectionArgumentConfiguration.class);
assertThat(bean.testBeans).isNotNull();
assertThat(bean.testBeans.isEmpty()).isTrue();
assertThat(bean.testBeans).isEmpty();
ctx.close();
}
@@ -1067,9 +1081,7 @@ class ConfigurationClassPostProcessorTests {
void testMapArgumentOnBeanMethod() {
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(MapArgumentConfiguration.class, DummyRunnable.class);
MapArgumentConfiguration bean = ctx.getBean(MapArgumentConfiguration.class);
assertThat(bean.testBeans).isNotNull();
assertThat(bean.testBeans).hasSize(1);
assertThat(bean.testBeans.values().iterator().next()).isSameAs(ctx.getBean(Runnable.class));
assertThat(bean.testBeans).hasSize(1).containsValue(ctx.getBean(Runnable.class));
ctx.close();
}
@@ -1077,8 +1089,7 @@ class ConfigurationClassPostProcessorTests {
void testEmptyMapArgumentOnBeanMethod() {
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(MapArgumentConfiguration.class);
MapArgumentConfiguration bean = ctx.getBean(MapArgumentConfiguration.class);
assertThat(bean.testBeans).isNotNull();
assertThat(bean.testBeans.isEmpty()).isTrue();
assertThat(bean.testBeans).isEmpty();
ctx.close();
}
@@ -1086,9 +1097,7 @@ class ConfigurationClassPostProcessorTests {
void testCollectionInjectionFromSameConfigurationClass() {
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CollectionInjectionConfiguration.class);
CollectionInjectionConfiguration bean = ctx.getBean(CollectionInjectionConfiguration.class);
assertThat(bean.testBeans).isNotNull();
assertThat(bean.testBeans).hasSize(1);
assertThat(bean.testBeans.get(0)).isSameAs(ctx.getBean(TestBean.class));
assertThat(bean.testBeans).containsExactly(ctx.getBean(TestBean.class));
ctx.close();
}
@@ -1096,25 +1105,21 @@ class ConfigurationClassPostProcessorTests {
void testMapInjectionFromSameConfigurationClass() {
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(MapInjectionConfiguration.class);
MapInjectionConfiguration bean = ctx.getBean(MapInjectionConfiguration.class);
assertThat(bean.testBeans).isNotNull();
assertThat(bean.testBeans).hasSize(1);
assertThat(bean.testBeans.get("testBean")).isSameAs(ctx.getBean(Runnable.class));
assertThat(bean.testBeans).containsOnly(Map.entry("testBean", ctx.getBean(Runnable.class)));
ctx.close();
}
@Test
void testBeanLookupFromSameConfigurationClass() {
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(BeanLookupConfiguration.class);
BeanLookupConfiguration bean = ctx.getBean(BeanLookupConfiguration.class);
assertThat(bean.getTestBean()).isNotNull();
assertThat(bean.getTestBean()).isSameAs(ctx.getBean(TestBean.class));
assertThat(ctx.getBean(BeanLookupConfiguration.class).getTestBean()).isSameAs(ctx.getBean(TestBean.class));
ctx.close();
}
@Test
void testNameClashBetweenConfigurationClassAndBean() {
assertThatExceptionOfType(BeanDefinitionStoreException.class)
.isThrownBy(() -> new AnnotationConfigApplicationContext(MyTestBean.class).getBean("myTestBean", TestBean.class));
.isThrownBy(() -> new AnnotationConfigApplicationContext(MyTestBean.class).getBean("myTestBean", TestBean.class));
}
@Test
@@ -1131,11 +1136,11 @@ class ConfigurationClassPostProcessorTests {
@Order(1)
static class SingletonBeanConfig {
public @Bean Foo foo() {
@Bean public Foo foo() {
return new Foo();
}
public @Bean Bar bar() {
@Bean public Bar bar() {
return new Bar(foo());
}
}
@@ -1143,11 +1148,11 @@ class ConfigurationClassPostProcessorTests {
@Configuration(proxyBeanMethods = false)
static class NonEnhancedSingletonBeanConfig {
public @Bean Foo foo() {
@Bean public Foo foo() {
return new Foo();
}
public @Bean Bar bar() {
@Bean public Bar bar() {
return new Bar(foo());
}
}
@@ -1155,11 +1160,13 @@ class ConfigurationClassPostProcessorTests {
@Configuration
static class StaticSingletonBeanConfig {
public static @Bean Foo foo() {
@Bean
public static Foo foo() {
return new Foo();
}
public static @Bean Bar bar() {
@Bean
public static Bar bar() {
return new Bar(foo());
}
}
@@ -1168,11 +1175,11 @@ class ConfigurationClassPostProcessorTests {
@Order(2)
static class OverridingSingletonBeanConfig {
public @Bean ExtendedFoo foo() {
@Bean public ExtendedFoo foo() {
return new ExtendedFoo();
}
public @Bean Bar bar() {
@Bean public Bar bar() {
return new Bar(foo());
}
}
@@ -1180,7 +1187,7 @@ class ConfigurationClassPostProcessorTests {
@Configuration
static class OverridingAgainSingletonBeanConfig {
public @Bean ExtendedAgainFoo foo() {
@Bean public ExtendedAgainFoo foo() {
return new ExtendedAgainFoo();
}
}
@@ -1188,7 +1195,7 @@ class ConfigurationClassPostProcessorTests {
@Configuration
static class InvalidOverridingSingletonBeanConfig {
public @Bean Foo foo() {
@Bean public Foo foo() {
return new Foo();
}
}
@@ -1200,11 +1207,11 @@ class ConfigurationClassPostProcessorTests {
@Order(1)
static class SingletonBeanConfig {
public @Bean Foo foo() {
@Bean public Foo foo() {
return new Foo();
}
public @Bean Bar bar() {
@Bean public Bar bar() {
return new Bar(foo());
}
}
@@ -1213,11 +1220,11 @@ class ConfigurationClassPostProcessorTests {
@Order(2)
static class OverridingSingletonBeanConfig {
public @Bean ExtendedFoo foo() {
@Bean public ExtendedFoo foo() {
return new ExtendedFoo();
}
public @Bean Bar bar() {
@Bean public Bar bar() {
return new Bar(foo());
}
}
@@ -1233,11 +1240,11 @@ class ConfigurationClassPostProcessorTests {
public SingletonBeanConfig(ConfigWithOrderedInnerClasses other) {
}
public @Bean Foo foo() {
@Bean public Foo foo() {
return new Foo();
}
public @Bean Bar bar() {
@Bean public Bar bar() {
return new Bar(foo());
}
}
@@ -1250,11 +1257,11 @@ class ConfigurationClassPostProcessorTests {
other.getObject();
}
public @Bean ExtendedFoo foo() {
@Bean public ExtendedFoo foo() {
return new ExtendedFoo();
}
public @Bean Bar bar() {
@Bean public Bar bar() {
return new Bar(foo());
}
}
@@ -1281,7 +1288,7 @@ class ConfigurationClassPostProcessorTests {
@Configuration
static class UnloadedConfig {
public @Bean Foo foo() {
@Bean public Foo foo() {
return new Foo();
}
}
@@ -1289,7 +1296,7 @@ class ConfigurationClassPostProcessorTests {
@Configuration
static class LoadedConfig {
public @Bean Bar bar() {
@Bean public Bar bar() {
return new Bar(new Foo());
}
}
@@ -1598,7 +1605,7 @@ class ConfigurationClassPostProcessorTests {
public static class WildcardWithGenericExtendsConfiguration {
@Bean
public Repository<? extends Object> genericRepo() {
public Repository<?> genericRepo() {
return new Repository<String>();
}
@@ -1707,7 +1714,7 @@ class ConfigurationClassPostProcessorTests {
}
@Configuration
public static abstract class AbstractConfig {
public abstract static class AbstractConfig {
@Bean
public ServiceBean serviceBean() {
@@ -1758,7 +1765,6 @@ class ConfigurationClassPostProcessorTests {
}
public interface DefaultMethodsConfig extends BaseDefaultMethods {
}
@Configuration
@@ -1779,6 +1785,29 @@ class ConfigurationClassPostProcessorTests {
}
}
@Configuration
public static class ConcreteConfigWithFailingInit implements DefaultMethodsConfig, BeanFactoryAware {
private BeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@Bean
@Override
public ServiceBeanProvider provider() {
return new ServiceBeanProvider();
}
@PostConstruct
public void validate() {
beanFactory.getBean("provider");
throw new IllegalStateException();
}
}
@Primary
public static class ServiceBeanProvider {
@@ -1891,7 +1920,7 @@ class ConfigurationClassPostProcessorTests {
}
}
static abstract class FooFactory {
abstract static class FooFactory {
abstract DependingFoo createFoo(BarArgument bar);
}
@@ -2010,7 +2039,7 @@ class ConfigurationClassPostProcessorTests {
}
@Configuration
static abstract class BeanLookupConfiguration {
abstract static class BeanLookupConfiguration {
@Bean
public TestBean thing() {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 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.
@@ -204,7 +204,7 @@ class ConfigurationClassProcessingTests {
BeanFactory factory = initBeanFactory(ConfigWithNullReference.class);
TestBean foo = factory.getBean("foo", TestBean.class);
assertThat(factory.getBean("bar").equals(null)).isTrue();
assertThat(factory.getBean("bar")).isEqualTo(null);
assertThat(foo.getSpouse()).isNull();
}
@@ -426,7 +426,7 @@ class ConfigurationClassProcessingTests {
@Configuration
static class ConfigWithFinalBean {
public final @Bean TestBean testBean() {
@Bean public final TestBean testBean() {
return new TestBean();
}
}
@@ -435,7 +435,7 @@ class ConfigurationClassProcessingTests {
@Configuration
static class SimplestPossibleConfig {
public @Bean String stringBean() {
@Bean public String stringBean() {
return "foo";
}
}
@@ -444,11 +444,11 @@ class ConfigurationClassProcessingTests {
@Configuration
static class ConfigWithNonSpecificReturnTypes {
public @Bean Object stringBean() {
@Bean public Object stringBean() {
return "foo";
}
public @Bean FactoryBean<?> factoryBean() {
@Bean public FactoryBean<?> factoryBean() {
ListFactoryBean fb = new ListFactoryBean();
fb.setSourceList(Arrays.asList("element1", "element2"));
return fb;
@@ -459,13 +459,13 @@ class ConfigurationClassProcessingTests {
@Configuration
static class ConfigWithPrototypeBean {
public @Bean TestBean foo() {
@Bean public TestBean foo() {
TestBean foo = new SpousyTestBean("foo");
foo.setSpouse(bar());
return foo;
}
public @Bean TestBean bar() {
@Bean public TestBean bar() {
TestBean bar = new SpousyTestBean("bar");
bar.setSpouse(baz());
return bar;
@@ -605,15 +605,15 @@ class ConfigurationClassProcessingTests {
void register(GenericApplicationContext ctx) {
ctx.registerBean("spouse", TestBean.class,
() -> new TestBean("functional"));
Supplier<TestBean> testBeanSupplier = () -> new TestBean(ctx.getBean("spouse", TestBean.class));
ctx.registerBean(TestBean.class,
testBeanSupplier,
Supplier<TestBean> testBeanSupplier =
() -> new TestBean(ctx.getBean("spouse", TestBean.class));
ctx.registerBean(TestBean.class, testBeanSupplier,
bd -> bd.setPrimary(true));
}
@Bean
public NestedTestBean nestedTestBean(TestBean testBean) {
return new NestedTestBean(testBean.getSpouse().getName());
public NestedTestBean nestedTestBean(TestBean spouse) {
return new NestedTestBean(spouse.getSpouse().getName());
}
}
@@ -1984,7 +1984,7 @@ class DataBinderTests {
.withMessageContaining("DataBinder is already initialized - call setAutoGrowCollectionLimit before other configuration methods");
}
@Test // SPR-15009
@Test // SPR-15009
void setCustomMessageCodesResolverBeforeInitializeBindingResultForBeanPropertyAccess() {
TestBean testBean = new TestBean();
DataBinder binder = new DataBinder(testBean, "testBean");
@@ -2001,7 +2001,7 @@ class DataBinderTests {
assertThat(((BeanWrapper) binder.getInternalBindingResult().getPropertyAccessor()).getAutoGrowCollectionLimit()).isEqualTo(512);
}
@Test // SPR-15009
@Test // SPR-15009
void setCustomMessageCodesResolverBeforeInitializeBindingResultForDirectFieldAccess() {
TestBean testBean = new TestBean();
DataBinder binder = new DataBinder(testBean, "testBean");
@@ -2055,7 +2055,7 @@ class DataBinderTests {
.withMessageContaining("DataBinder is already initialized with MessageCodesResolver");
}
@Test // gh-24347
@Test // gh-24347
void overrideBindingResultType() {
TestBean testBean = new TestBean();
DataBinder binder = new DataBinder(testBean, "testBean");
@@ -78,8 +78,7 @@ public class MethodValidationTests {
ac.close();
}
@Test // gh-29782
@SuppressWarnings("unchecked")
@Test // gh-29782
public void testMethodValidationPostProcessorForInterfaceOnlyProxy() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(MethodValidationPostProcessor.class);
@@ -647,7 +647,7 @@ public class ResolvableType implements Serializable {
* @param nestingLevel the required nesting level, indexed from 1 for the
* current type, 2 for the first nested generic, 3 for the second and so on
* @param typeIndexesPerLevel a map containing the generic index for a given
* nesting level (may be {@code null})
* nesting level (can be {@code null})
* @return a {@code ResolvableType} for the nested level, or {@link #NONE}
*/
public ResolvableType getNested(int nestingLevel, @Nullable Map<Integer, Integer> typeIndexesPerLevel) {
@@ -679,7 +679,7 @@ public class ResolvableType implements Serializable {
* generic is returned.
* <p>If no generic is available at the specified indexes {@link #NONE} is returned.
* @param indexes the indexes that refer to the generic parameter
* (may be omitted to return the first generic)
* (can be omitted to return the first generic)
* @return a {@code ResolvableType} for the specified generic, or {@link #NONE}
* @see #hasGenerics()
* @see #getGenerics()
@@ -782,7 +782,7 @@ public class ResolvableType implements Serializable {
* Convenience method that will {@link #getGeneric(int...) get} and
* {@link #resolve() resolve} a specific generic parameter.
* @param indexes the indexes that refer to the generic parameter
* (may be omitted to return the first generic)
* (can be omitted to return the first generic)
* @return a resolved {@link Class} or {@code null}
* @see #getGeneric(int...)
* @see #resolve()
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -38,7 +38,7 @@ public abstract class OrderUtils {
/** Cache marker for a non-annotated Class. */
private static final Object NOT_ANNOTATED = new Object();
private static final String JAVAX_PRIORITY_ANNOTATION = "jakarta.annotation.Priority";
private static final String JAKARTA_PRIORITY_ANNOTATION = "jakarta.annotation.Priority";
/** Cache for @Order value (or NOT_ANNOTATED marker) per Class. */
static final Map<AnnotatedElement, Object> orderCache = new ConcurrentReferenceHashMap<>(64);
@@ -124,7 +124,7 @@ public abstract class OrderUtils {
if (orderAnnotation.isPresent()) {
return orderAnnotation.getInt(MergedAnnotation.VALUE);
}
MergedAnnotation<?> priorityAnnotation = annotations.get(JAVAX_PRIORITY_ANNOTATION);
MergedAnnotation<?> priorityAnnotation = annotations.get(JAKARTA_PRIORITY_ANNOTATION);
if (priorityAnnotation.isPresent()) {
return priorityAnnotation.getInt(MergedAnnotation.VALUE);
}
@@ -139,7 +139,7 @@ public abstract class OrderUtils {
*/
@Nullable
public static Integer getPriority(Class<?> type) {
return MergedAnnotations.from(type, SearchStrategy.TYPE_HIERARCHY).get(JAVAX_PRIORITY_ANNOTATION)
return MergedAnnotations.from(type, SearchStrategy.TYPE_HIERARCHY).get(JAKARTA_PRIORITY_ANNOTATION)
.getValue(MergedAnnotation.VALUE, Integer.class).orElse(null);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -557,7 +557,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
String rootDirPath = determineRootDir(locationPattern);
String subPattern = locationPattern.substring(rootDirPath.length());
Resource[] rootDirResources = getResources(rootDirPath);
Set<Resource> result = new LinkedHashSet<>(16);
Set<Resource> result = new LinkedHashSet<>(64);
for (Resource rootDirResource : rootDirResources) {
rootDirResource = resolveRootDirResource(rootDirResource);
URL rootDirUrl = rootDirResource.getURL();
@@ -706,7 +706,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
// The Sun JRE does not return a slash here, but BEA JRockit does.
rootEntryPath = rootEntryPath + "/";
}
Set<Resource> result = new LinkedHashSet<>(8);
Set<Resource> result = new LinkedHashSet<>(64);
for (Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements();) {
JarEntry entry = entries.nextElement();
String entryPath = entry.getName();
@@ -756,7 +756,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, String subPattern)
throws IOException {
Set<Resource> result = new LinkedHashSet<>();
Set<Resource> result = new LinkedHashSet<>(64);
URI rootDirUri;
try {
rootDirUri = rootDirResource.getURI();
@@ -865,7 +865,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
* @see PathMatcher#match(String, String)
*/
protected Set<Resource> findAllModulePathResources(String locationPattern) throws IOException {
Set<Resource> result = new LinkedHashSet<>(16);
Set<Resource> result = new LinkedHashSet<>(64);
// Skip scanning the module path when running in a native image.
if (NativeDetector.inNativeImage()) {
@@ -966,7 +966,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
private final String rootPath;
private final Set<Resource> resources = new LinkedHashSet<>();
private final Set<Resource> resources = new LinkedHashSet<>(64);
public PatternVirtualFileVisitor(String rootPath, String subPattern, PathMatcher pathMatcher) {
this.subPattern = subPattern;
@@ -997,7 +997,6 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
else if ("toString".equals(methodName)) {
return toString();
}
throw new IllegalStateException("Unexpected method invocation: " + method);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -243,7 +243,7 @@ public abstract class ClassUtils {
* style (e.g. "java.lang.Thread.State" instead of "java.lang.Thread$State").
* @param name the name of the Class
* @param classLoader the class loader to use
* (may be {@code null}, which indicates the default class loader)
* (can be {@code null}, which indicates the default class loader)
* @return a class instance for the supplied name
* @throws ClassNotFoundException if the class was not found
* @throws LinkageError if the class file could not be loaded
@@ -314,7 +314,7 @@ public abstract class ClassUtils {
* the exceptions thrown in case of class loading failure.
* @param className the name of the Class
* @param classLoader the class loader to use
* (may be {@code null}, which indicates the default class loader)
* (can be {@code null}, which indicates the default class loader)
* @return a class instance for the supplied name
* @throws IllegalArgumentException if the class name was not resolvable
* (that is, the class could not be found or the class file could not be loaded)
@@ -348,7 +348,7 @@ public abstract class ClassUtils {
* one of its dependencies is not present or cannot be loaded.
* @param className the name of the class to check
* @param classLoader the class loader to use
* (may be {@code null} which indicates the default class loader)
* (can be {@code null} which indicates the default class loader)
* @return whether the specified class is present (including all of its
* superclasses and interfaces)
* @throws IllegalStateException if the corresponding class is resolvable but
@@ -375,7 +375,7 @@ public abstract class ClassUtils {
* Check whether the given class is visible in the given ClassLoader.
* @param clazz the class to check (typically an interface)
* @param classLoader the ClassLoader to check against
* (may be {@code null} in which case this method will always return {@code true})
* (can be {@code null} in which case this method will always return {@code true})
*/
public static boolean isVisible(Class<?> clazz, @Nullable ClassLoader classLoader) {
if (classLoader == null) {
@@ -399,7 +399,7 @@ public abstract class ClassUtils {
* i.e. whether it is loaded by the given ClassLoader or a parent of it.
* @param clazz the class to analyze
* @param classLoader the ClassLoader to potentially cache metadata in
* (may be {@code null} which indicates the system class loader)
* (can be {@code null} which indicates the system class loader)
*/
public static boolean isCacheSafe(Class<?> clazz, @Nullable ClassLoader classLoader) {
Assert.notNull(clazz, "Class must not be null");
@@ -663,7 +663,7 @@ public abstract class ClassUtils {
* in the given collection.
* <p>Basically like {@code AbstractCollection.toString()}, but stripping
* the "class "/"interface " prefix before every class name.
* @param classes a Collection of Class objects (may be {@code null})
* @param classes a Collection of Class objects (can be {@code null})
* @return a String of form "[com.foo.Bar, com.foo.Baz]"
* @see java.util.AbstractCollection#toString()
*/
@@ -718,7 +718,7 @@ public abstract class ClassUtils {
* <p>If the class itself is an interface, it gets returned as sole interface.
* @param clazz the class to analyze for interfaces
* @param classLoader the ClassLoader that the interfaces need to be visible in
* (may be {@code null} when accepting all declared interfaces)
* (can be {@code null} when accepting all declared interfaces)
* @return all interfaces that the given object implements as an array
*/
public static Class<?>[] getAllInterfacesForClass(Class<?> clazz, @Nullable ClassLoader classLoader) {
@@ -753,7 +753,7 @@ public abstract class ClassUtils {
* <p>If the class itself is an interface, it gets returned as sole interface.
* @param clazz the class to analyze for interfaces
* @param classLoader the ClassLoader that the interfaces need to be visible in
* (may be {@code null} when accepting all declared interfaces)
* (can be {@code null} when accepting all declared interfaces)
* @return all interfaces that the given object implements as a Set
*/
public static Set<Class<?>> getAllInterfacesForClassAsSet(Class<?> clazz, @Nullable ClassLoader classLoader) {
@@ -1082,7 +1082,7 @@ public abstract class ClassUtils {
* fully qualified interface/class name + "." + method name.
* @param method the method
* @param clazz the clazz that the method is being invoked on
* (may be {@code null} to indicate the method's declaring class)
* (can be {@code null} to indicate the method's declaring class)
* @return the qualified name of the method
* @since 4.3.4
*/
@@ -1163,7 +1163,7 @@ public abstract class ClassUtils {
* @param clazz the clazz to analyze
* @param methodName the name of the method
* @param paramTypes the parameter types of the method
* (may be {@code null} to indicate any signature)
* (can be {@code null} to indicate any signature)
* @return the method (never {@code null})
* @throws IllegalStateException if the method has not been found
* @see Class#getMethod
@@ -1202,7 +1202,7 @@ public abstract class ClassUtils {
* @param clazz the clazz to analyze
* @param methodName the name of the method
* @param paramTypes the parameter types of the method
* (may be {@code null} to indicate any signature)
* (can be {@code null} to indicate any signature)
* @return the method, or {@code null} if not found
* @see Class#getMethod
*/
@@ -1291,13 +1291,14 @@ public abstract class ClassUtils {
* implementation will fall back to returning the originally provided method.
* @param method the method to be invoked, which may come from an interface
* @param targetClass the target class for the current invocation
* (may be {@code null} or may not even implement the method)
* (can be {@code null} or may not even implement the method)
* @return the specific target method, or the original method if the
* {@code targetClass} does not implement it
* @see #getInterfaceMethodIfPossible(Method, Class)
*/
public static Method getMostSpecificMethod(Method method, @Nullable Class<?> targetClass) {
if (targetClass != null && targetClass != method.getDeclaringClass() && isOverridable(method, targetClass)) {
if (targetClass != null && targetClass != method.getDeclaringClass() &&
(isOverridable(method, targetClass) || !method.getDeclaringClass().isAssignableFrom(targetClass))) {
try {
if (Modifier.isPublic(method.getModifiers())) {
try {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -51,14 +51,14 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
*/
class PathMatchingResourcePatternResolverTests {
private static final String[] CLASSES_IN_CORE_IO_SUPPORT = { "EncodedResource.class",
private static final String[] CLASSES_IN_CORE_IO_SUPPORT = {"EncodedResource.class",
"LocalizedResourceHelper.class", "PathMatchingResourcePatternResolver.class", "PropertiesLoaderSupport.class",
"PropertiesLoaderUtils.class", "ResourceArrayPropertyEditor.class", "ResourcePatternResolver.class",
"ResourcePatternUtils.class", "SpringFactoriesLoader.class" };
"ResourcePatternUtils.class", "SpringFactoriesLoader.class"};
private static final String[] TEST_CLASSES_IN_CORE_IO_SUPPORT = { "PathMatchingResourcePatternResolverTests.class" };
private static final String[] TEST_CLASSES_IN_CORE_IO_SUPPORT = {"PathMatchingResourcePatternResolverTests.class"};
private static final String[] CLASSES_IN_REACTOR_UTIL_ANNOTATION = { "NonNull.class", "NonNullApi.class", "Nullable.class" };
private static final String[] CLASSES_IN_REACTOR_UTIL_ANNOTATION = {"NonNull.class", "NonNullApi.class", "Nullable.class"};
private PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
@@ -200,7 +200,7 @@ class PathMatchingResourcePatternResolverTests {
}
@Test
void usingFileProtocolWithoutWildcardInPatternAndEndingInSlashStarStar() {
void usingFileProtocolWithoutWildcardInPatternAndEndingInSlashStarStar() {
Path testResourcesDir = Paths.get("src/test/resources").toAbsolutePath();
String pattern = String.format("file:%s/scanned-resources/**", testResourcesDir);
String pathPrefix = ".+?resources/";
@@ -329,8 +329,8 @@ class PathMatchingResourcePatternResolverTests {
}
private String getPath(Resource resource) {
// Tests fail if we use resouce.getURL().getPath(). They would also fail on Mac OS when
// using resouce.getURI().getPath() if the resource paths are not Unicode normalized.
// Tests fail if we use resource.getURL().getPath(). They would also fail on macOS when
// using resource.getURI().getPath() if the resource paths are not Unicode normalized.
//
// On the JVM, all tests should pass when using resouce.getFile().getPath(); however,
// we use FileSystemResource#getPath since this test class is sometimes run within a
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2024 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.
@@ -20,7 +20,7 @@ import org.springframework.dao.DataRetrievalFailureException;
/**
* Data access exception thrown when a result set did not have the correct column count,
* for example when expecting a single column but getting 0 or more than 1 columns.
* for example when expecting a single column but getting 0 or more than 1 column.
*
* @author Juergen Hoeller
* @since 2.0
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2024 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.
@@ -20,7 +20,7 @@ import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
/**
* Exception thrown when a JDBC update affects an unexpected number of rows.
* Typically we expect an update to affect a single row, meaning it's an
* Typically, we expect an update to affect a single row, meaning it is an
* error if it affects multiple rows.
*
* @author Rod Johnson
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2024 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.
@@ -22,7 +22,7 @@ package org.springframework.jdbc.core;
*
* <p>This interface allows you to signal the end of a batch rather than
* having to determine the exact batch size upfront. Batch size is still
* being honored but it is now the maximum size of the batch.
* being honored, but it is now the maximum size of the batch.
*
* <p>The {@link #isBatchExhausted} method is called after each call to
* {@link #setValues} to determine whether there were some values added,
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2024 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.
@@ -128,8 +128,8 @@ public abstract class RdbmsOperation implements InitializingBean {
/**
* Set the maximum number of rows for this RDBMS operation. This is important
* for processing subsets of large result sets, avoiding to read and hold
* the entire result set in the database or in the JDBC driver.
* for processing subsets of large result sets, in order to avoid reading and
* holding the entire result set in the database or in the JDBC driver.
* <p>Default is -1, indicating to use the driver's default.
* @see org.springframework.jdbc.core.JdbcTemplate#setMaxRows
*/
@@ -175,7 +175,7 @@ public abstract class RdbmsOperation implements InitializingBean {
public void setUpdatableResults(boolean updatableResults) {
if (isCompiled()) {
throw new InvalidDataAccessApiUsageException(
"The updateableResults flag must be set before the operation is compiled");
"The updatableResults flag must be set before the operation is compiled");
}
this.updatableResults = updatableResults;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 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,7 +29,7 @@ import org.springframework.jdbc.core.SqlParameter;
/**
* Superclass for object abstractions of RDBMS stored procedures.
* This class is abstract and it is intended that subclasses will provide a typed
* This class is abstract, and it is intended that subclasses will provide a typed
* method for invocation that delegates to the supplied {@link #execute} method.
*
* <p>The inherited {@link #setSql sql} property is the name of the stored procedure
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2024 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.
@@ -25,9 +25,9 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.lang.Nullable;
/**
* Registry for custom {@link org.springframework.jdbc.support.SQLExceptionTranslator} instances associated with
* specific databases allowing for overriding translation based on values contained in the configuration file
* named "sql-error-codes.xml".
* Registry for custom {@link SQLExceptionTranslator} instances associated with
* specific databases allowing for overriding translation based on values
* contained in the configuration file named "sql-error-codes.xml".
*
* @author Thomas Risberg
* @since 3.1.1
@@ -38,7 +38,7 @@ public final class CustomSQLExceptionTranslatorRegistry {
private static final Log logger = LogFactory.getLog(CustomSQLExceptionTranslatorRegistry.class);
/**
* Keep track of a single instance so we can return it to classes that request it.
* Keep track of a single instance, so we can return it to classes that request it.
*/
private static final CustomSQLExceptionTranslatorRegistry instance = new CustomSQLExceptionTranslatorRegistry();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 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.
@@ -219,7 +219,7 @@ public abstract class JdbcUtils {
return NumberUtils.convertNumberToTargetClass(number, Integer.class);
}
else {
// e.g. on Postgres: getObject returns a PGObject but we need a String
// e.g. on Postgres: getObject returns a PGObject, but we need a String
return rs.getString(index);
}
}
@@ -415,14 +415,14 @@ public abstract class JdbcUtils {
}
/**
* Return whether the given JDBC driver supports JDBC 2.0 batch updates.
* Return whether the given JDBC driver supports JDBC batch updates.
* <p>Typically invoked right before execution of a given set of statements:
* to decide whether the set of SQL statements should be executed through
* the JDBC 2.0 batch mechanism or simply in a traditional one-by-one fashion.
* the JDBC batch mechanism or simply in a traditional one-by-one fashion.
* <p>Logs a warning if the "supportsBatchUpdates" methods throws an exception
* and simply returns {@code false} in that case.
* @param con the Connection to check
* @return whether JDBC 2.0 batch updates are supported
* @return whether JDBC batch updates are supported
* @see java.sql.DatabaseMetaData#supportsBatchUpdates()
*/
public static boolean supportsBatchUpdates(Connection con) {
@@ -492,8 +492,8 @@ public abstract class JdbcUtils {
/**
* Determine the column name to use. The column name is determined based on a
* lookup using ResultSetMetaData.
* <p>This method implementation takes into account recent clarifications
* expressed in the JDBC 4.0 specification:
* <p>This method's implementation takes into account clarifications expressed
* in the JDBC 4.0 specification:
* <p><i>columnLabel - the label for the column specified with the SQL AS clause.
* If the SQL AS clause was not specified, then the label is the name of the column</i>.
* @param resultSetMetaData the current meta-data to use
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 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.
@@ -67,7 +67,7 @@ public class SQLErrorCodesFactory {
private static final Log logger = LogFactory.getLog(SQLErrorCodesFactory.class);
/**
* Keep track of a single instance so we can return it to classes that request it.
* Keep track of a single instance, so we can return it to classes that request it.
* Lazily initialized in order to avoid making {@code SQLErrorCodesFactory} constructor
* reachable on native images when not needed.
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -284,6 +284,7 @@ public class DefaultMessageListenerContainer extends AbstractPollingMessageListe
* @see #CACHE_CONNECTION
* @see #CACHE_SESSION
* @see #CACHE_CONSUMER
* @see #CACHE_AUTO
* @see #setCacheLevelName
* @see #setTransactionManager
*/
@@ -570,8 +571,7 @@ public class DefaultMessageListenerContainer extends AbstractPollingMessageListe
if (this.taskExecutor == null) {
this.taskExecutor = createDefaultTaskExecutor();
}
else if (this.taskExecutor instanceof SchedulingTaskExecutor ste &&
ste.prefersShortLivedTasks() &&
else if (this.taskExecutor instanceof SchedulingTaskExecutor ste && ste.prefersShortLivedTasks() &&
this.maxMessagesPerTask == Integer.MIN_VALUE) {
// TaskExecutor indicated a preference for short-lived tasks. According to
// setMaxMessagesPerTask javadoc, we'll use 10 message per task in this case
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 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.
@@ -19,7 +19,6 @@ package org.springframework.jms.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import jakarta.jms.JMSException;
import jakarta.jms.MessageListener;
import org.junit.jupiter.api.Test;
@@ -103,24 +102,20 @@ class EnableJmsTests extends AbstractJmsAnnotationDrivenTests {
}
@Test
@SuppressWarnings("resource")
void containerAreStartedByDefault() {
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(
EnableJmsDefaultContainerFactoryConfig.class, DefaultBean.class);
JmsListenerContainerTestFactory factory =
context.getBean(JmsListenerContainerTestFactory.class);
JmsListenerContainerTestFactory factory = context.getBean(JmsListenerContainerTestFactory.class);
MessageListenerTestContainer container = factory.getListenerContainers().get(0);
assertThat(container.isAutoStartup()).isTrue();
assertThat(container.isStarted()).isTrue();
}
@Test
@SuppressWarnings("resource")
void containerCanBeStarterViaTheRegistry() {
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(
EnableJmsAutoStartupFalseConfig.class, DefaultBean.class);
JmsListenerContainerTestFactory factory =
context.getBean(JmsListenerContainerTestFactory.class);
JmsListenerContainerTestFactory factory = context.getBean(JmsListenerContainerTestFactory.class);
MessageListenerTestContainer container = factory.getListenerContainers().get(0);
assertThat(container.isAutoStartup()).isFalse();
assertThat(container.isStarted()).isFalse();
@@ -131,13 +126,13 @@ class EnableJmsTests extends AbstractJmsAnnotationDrivenTests {
@Override
@Test
void jmsHandlerMethodFactoryConfiguration() throws JMSException {
void jmsHandlerMethodFactoryConfiguration() {
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(
EnableJmsHandlerMethodFactoryConfig.class, ValidationBean.class);
assertThatExceptionOfType(ListenerExecutionFailedException.class).isThrownBy(() ->
testJmsHandlerMethodFactoryConfiguration(context))
.withCauseInstanceOf(MethodArgumentNotValidException.class);
assertThatExceptionOfType(ListenerExecutionFailedException.class)
.isThrownBy(() -> testJmsHandlerMethodFactoryConfiguration(context))
.withCauseInstanceOf(MethodArgumentNotValidException.class);
}
@Override
@@ -159,19 +154,20 @@ class EnableJmsTests extends AbstractJmsAnnotationDrivenTests {
@Test
void composedJmsListeners() {
try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(
EnableJmsDefaultContainerFactoryConfig.class, ComposedJmsListenersBean.class)) {
JmsListenerContainerTestFactory simpleFactory = context.getBean("jmsListenerContainerFactory",
JmsListenerContainerTestFactory.class);
EnableJmsDefaultContainerFactoryConfig.class, ComposedJmsListenersBean.class)) {
JmsListenerContainerTestFactory simpleFactory =
context.getBean("jmsListenerContainerFactory", JmsListenerContainerTestFactory.class);
assertThat(simpleFactory.getListenerContainers()).hasSize(2);
MethodJmsListenerEndpoint first = (MethodJmsListenerEndpoint) simpleFactory.getListenerContainer(
"first").getEndpoint();
MethodJmsListenerEndpoint first = (MethodJmsListenerEndpoint)
simpleFactory.getListenerContainer("first").getEndpoint();
assertThat(first.getId()).isEqualTo("first");
assertThat(first.getDestination()).isEqualTo("orderQueue");
assertThat(first.getConcurrency()).isNull();
MethodJmsListenerEndpoint second = (MethodJmsListenerEndpoint) simpleFactory.getListenerContainer(
"second").getEndpoint();
MethodJmsListenerEndpoint second = (MethodJmsListenerEndpoint)
simpleFactory.getListenerContainer("second").getEndpoint();
assertThat(second.getId()).isEqualTo("second");
assertThat(second.getDestination()).isEqualTo("billingQueue");
assertThat(second.getConcurrency()).isEqualTo("2-10");
@@ -179,12 +175,11 @@ class EnableJmsTests extends AbstractJmsAnnotationDrivenTests {
}
@Test
@SuppressWarnings("resource")
void unknownFactory() {
// not found
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() ->
new AnnotationConfigApplicationContext(EnableJmsSampleConfig.class, CustomBean.class))
.withMessageContaining("customFactory");
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> new AnnotationConfigApplicationContext(EnableJmsSampleConfig.class, CustomBean.class))
.withMessageContaining("customFactory");
}
@Test
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2024 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,15 +28,13 @@ public class JmsListenerContainerTestFactory implements JmsListenerContainerFact
private boolean autoStartup = true;
private final Map<String, MessageListenerTestContainer> listenerContainers =
new LinkedHashMap<>();
private final Map<String, MessageListenerTestContainer> listenerContainers = new LinkedHashMap<>();
public void setAutoStartup(boolean autoStartup) {
this.autoStartup = autoStartup;
}
public List<MessageListenerTestContainer> getListenerContainers() {
return new ArrayList<>(this.listenerContainers.values());
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -264,8 +264,9 @@ public class LocalContainerEntityManagerFactoryBean extends AbstractEntityManage
* @see jakarta.persistence.spi.PersistenceUnitInfo#getNonJtaDataSource()
* @see #setPersistenceUnitManager
*/
public void setDataSource(DataSource dataSource) {
this.internalPersistenceUnitManager.setDataSourceLookup(new SingleDataSourceLookup(dataSource));
public void setDataSource(@Nullable DataSource dataSource) {
this.internalPersistenceUnitManager.setDataSourceLookup(
dataSource != null ? new SingleDataSourceLookup(dataSource) : null);
this.internalPersistenceUnitManager.setDefaultDataSource(dataSource);
}
@@ -281,8 +282,9 @@ public class LocalContainerEntityManagerFactoryBean extends AbstractEntityManage
* @see jakarta.persistence.spi.PersistenceUnitInfo#getJtaDataSource()
* @see #setPersistenceUnitManager
*/
public void setJtaDataSource(DataSource jtaDataSource) {
this.internalPersistenceUnitManager.setDataSourceLookup(new SingleDataSourceLookup(jtaDataSource));
public void setJtaDataSource(@Nullable DataSource jtaDataSource) {
this.internalPersistenceUnitManager.setDataSourceLookup(
jtaDataSource != null ? new SingleDataSourceLookup(jtaDataSource) : null);
this.internalPersistenceUnitManager.setDefaultJtaDataSource(jtaDataSource);
}
@@ -427,6 +429,7 @@ public class LocalContainerEntityManagerFactoryBean extends AbstractEntityManage
}
@Override
@Nullable
public DataSource getDataSource() {
if (this.persistenceUnitInfo != null) {
return (this.persistenceUnitInfo.getJtaDataSource() != null ?
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 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,28 +28,18 @@ import jakarta.persistence.spi.PersistenceProvider;
* shared JPA EntityManagerFactory in a Spring application context; the
* EntityManagerFactory can then be passed to JPA-based DAOs via
* dependency injection. Note that switching to a JNDI lookup or to a
* {@link LocalContainerEntityManagerFactoryBean}
* definition is just a matter of configuration!
* {@link LocalContainerEntityManagerFactoryBean} definition based on the
* JPA container contract is just a matter of configuration!
*
* <p>Configuration settings are usually read from a {@code META-INF/persistence.xml}
* config file, residing in the class path, according to the JPA standalone bootstrap
* contract. Additionally, most JPA providers will require a special VM agent
* (specified on JVM startup) that allows them to instrument application classes.
* See the Java Persistence API specification and your provider documentation
* for setup details.
*
* <p>This EntityManagerFactory bootstrap is appropriate for standalone applications
* which solely use JPA for data access. If you want to set up your persistence
* provider for an external DataSource and/or for global transactions which span
* multiple resources, you will need to either deploy it into a full Jakarta EE
* application server and access the deployed EntityManagerFactory via JNDI,
* or use Spring's {@link LocalContainerEntityManagerFactoryBean} with appropriate
* configuration for local setup according to JPA's container contract.
* contract. See the Java Persistence API specification and your persistence provider
* documentation for setup details. Additionally, JPA properties can also be added
* on this FactoryBean via {@link #setJpaProperties}/{@link #setJpaPropertyMap}.
*
* <p><b>Note:</b> This FactoryBean has limited configuration power in terms of
* what configuration it is able to pass to the JPA provider. If you need more
* flexible configuration, for example passing a Spring-managed JDBC DataSource
* to the JPA provider, consider using Spring's more powerful
* the configuration that it is able to pass to the JPA provider. If you need
* more flexible configuration options, consider using Spring's more powerful
* {@link LocalContainerEntityManagerFactoryBean} instead.
*
* @author Juergen Hoeller
@@ -0,0 +1 @@
Args = -H:ServiceLoaderFeatureExcludeServices=org.hibernate.bytecode.spi.BytecodeProvider
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -70,13 +70,15 @@ public class SingleConnectionFactory extends DelegatingConnectionFactory
private boolean suppressClose;
/** Override auto-commit state?. */
private @Nullable Boolean autoCommit;
@Nullable
private Boolean autoCommit;
/** Wrapped Connection. */
private final AtomicReference<Connection> target = new AtomicReference<>();
/** Proxy Connection. */
private @Nullable Connection connection;
@Nullable
private Connection connection;
private final Mono<? extends Connection> connectionEmitter;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -292,6 +292,11 @@ public class MockHttpServletResponse implements HttpServletResponse {
doAddHeaderValue(HttpHeaders.CONTENT_LENGTH, contentLength, true);
}
/**
* Get the length of the content body from the HTTP Content-Length header.
* @return the value of the Content-Length header
* @see #setContentLength(int)
*/
public int getContentLength() {
return (int) this.contentLength;
}
@@ -742,7 +747,7 @@ public class MockHttpServletResponse implements HttpServletResponse {
@Override
public void setStatus(int status) {
if (!this.isCommitted()) {
if (!isCommitted()) {
this.status = status;
}
}
@@ -752,6 +757,9 @@ public class MockHttpServletResponse implements HttpServletResponse {
return this.status;
}
/**
* Return the error message used when calling {@link HttpServletResponse#sendError(int, String)}.
*/
@Nullable
public String getErrorMessage() {
return this.errorMessage;
@@ -31,7 +31,6 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Integration tests for {@link MockMvcWebConnection}.
*
@@ -64,14 +63,15 @@ public class MockMvcWebConnectionTests {
public void contextPathEmpty() throws IOException {
this.webClient.setWebConnection(new MockMvcWebConnection(this.mockMvc, this.webClient, ""));
// Empty context path (root context) should not match to a URL with a context path
assertThatExceptionOfType(FailingHttpStatusCodeException.class).isThrownBy(() ->
this.webClient.getPage("http://localhost/context/a"))
.satisfies(ex -> assertThat(ex.getStatusCode()).isEqualTo(404));
assertThatExceptionOfType(FailingHttpStatusCodeException.class)
.isThrownBy(() -> this.webClient.getPage("http://localhost/context/a"))
.satisfies(ex -> assertThat(ex.getStatusCode()).isEqualTo(404));
this.webClient.setWebConnection(new MockMvcWebConnection(this.mockMvc, this.webClient));
// No context is the same providing an empty context path
assertThatExceptionOfType(FailingHttpStatusCodeException.class).isThrownBy(() ->
this.webClient.getPage("http://localhost/context/a"))
.satisfies(ex -> assertThat(ex.getStatusCode()).isEqualTo(404));
assertThatExceptionOfType(FailingHttpStatusCodeException.class)
.isThrownBy(() -> this.webClient.getPage("http://localhost/context/a"))
.satisfies(ex -> assertThat(ex.getStatusCode()).isEqualTo(404));
}
@Test
@@ -84,8 +84,9 @@ public class MockMvcWebConnectionTests {
@Test
public void infiniteForward() {
this.webClient.setWebConnection(new MockMvcWebConnection(this.mockMvc, this.webClient, ""));
assertThatIllegalStateException().isThrownBy(() -> this.webClient.getPage("http://localhost/infiniteForward"))
.withMessage("Forwarded 100 times in a row, potential infinite forward loop");
assertThatIllegalStateException()
.isThrownBy(() -> this.webClient.getPage("http://localhost/infiniteForward"))
.withMessage("Forwarded 100 times in a row, potential infinite forward loop");
}
@Test
@@ -32,7 +32,6 @@ import org.springframework.mock.web.MockHttpServletResponse;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link MockWebResponseBuilder}.
*
@@ -55,8 +54,6 @@ public class MockWebResponseBuilderTests {
}
// --- constructor
@Test
public void constructorWithNullWebRequest() {
assertThatIllegalArgumentException().isThrownBy(() ->
@@ -66,12 +63,10 @@ public class MockWebResponseBuilderTests {
@Test
public void constructorWithNullResponse() throws Exception {
assertThatIllegalArgumentException().isThrownBy(() ->
new MockWebResponseBuilder(0L, new WebRequest(new URL("http://company.example:80/test/this/here")), null));
new MockWebResponseBuilder(0L,
new WebRequest(new URL("http://company.example:80/test/this/here")), null));
}
// --- build
@Test
public void buildContent() throws Exception {
this.response.getWriter().write("expected content");
@@ -124,8 +119,7 @@ public class MockWebResponseBuilderTests {
.endsWith("; Secure; HttpOnly");
}
// SPR-14169
@Test
@Test // SPR-14169
public void buildResponseHeadersNullDomainDefaulted() throws Exception {
Cookie cookie = new Cookie("cookieA", "valueA");
this.response.addCookie(cookie);
@@ -52,6 +52,7 @@ class MockMvcHtmlUnitDriverBuilderTests {
private HtmlUnitDriver driver;
MockMvcHtmlUnitDriverBuilderTests(WebApplicationContext wac) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
@@ -48,6 +48,7 @@ class WebConnectionHtmlUnitDriverTests {
@Mock
private WebConnection connection;
@BeforeEach
void setup() throws Exception {
given(this.connection.getResponse(any(WebRequest.class))).willThrow(new IOException(""));
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2024 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.
@@ -44,10 +44,11 @@ public class IncorrectUpdateSemanticsDataAccessException extends InvalidDataAcce
super(msg, cause);
}
/**
* Return whether data was updated.
* If this method returns false, there's nothing to roll back.
* <p>The default implementation always returns true.
* If this method returns {@code false}, there is nothing to roll back.
* <p>The default implementation always returns {@code true}.
* This can be overridden in subclasses.
*/
public boolean wasDataUpdated() {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 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.
@@ -42,11 +42,6 @@ import org.springframework.util.StringValueResolver;
* If none found on the target class, the interface that the invoked method
* has been called through (in case of a JDK proxy) will be checked.
*
* <p>This implementation caches attributes by method after they are first used.
* If it is ever desirable to allow dynamic changing of transaction attributes
* (which is very unlikely), caching could be made configurable. Caching is
* desirable because of the cost of evaluating rollback rules.
*
* @author Rod Johnson
* @author Juergen Hoeller
* @since 1.1
@@ -95,7 +90,7 @@ public abstract class AbstractFallbackTransactionAttributeSource
* Determine the transaction attribute for this method invocation.
* <p>Defaults to the class's transaction attribute if no method attribute is found.
* @param method the method for the current invocation (never {@code null})
* @param targetClass the target class for this invocation (may be {@code null})
* @param targetClass the target class for this invocation (can be {@code null})
* @return a TransactionAttribute for this method, or {@code null} if the method
* is not transactional
*/
@@ -106,27 +101,15 @@ public abstract class AbstractFallbackTransactionAttributeSource
return null;
}
// First, see if we have a cached value.
Object cacheKey = getCacheKey(method, targetClass);
TransactionAttribute cached = this.attributeCache.get(cacheKey);
if (cached != null) {
// Value will either be canonical value indicating there is no transaction attribute,
// or an actual transaction attribute.
if (cached == NULL_TRANSACTION_ATTRIBUTE) {
return null;
}
else {
return cached;
}
return (cached != NULL_TRANSACTION_ATTRIBUTE ? cached : null);
}
else {
// We need to work it out.
TransactionAttribute txAttr = computeTransactionAttribute(method, targetClass);
// Put it in the cache.
if (txAttr == null) {
this.attributeCache.put(cacheKey, NULL_TRANSACTION_ATTRIBUTE);
}
else {
if (txAttr != null) {
String methodIdentification = ClassUtils.getQualifiedMethodName(method, targetClass);
if (txAttr instanceof DefaultTransactionAttribute dta) {
dta.setDescriptor(methodIdentification);
@@ -137,6 +120,9 @@ public abstract class AbstractFallbackTransactionAttributeSource
}
this.attributeCache.put(cacheKey, txAttr);
}
else {
this.attributeCache.put(cacheKey, NULL_TRANSACTION_ATTRIBUTE);
}
return txAttr;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 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.
@@ -57,7 +57,7 @@ public interface TransactionAttributeSource {
* Return the transaction attribute for the given method,
* or {@code null} if the method is non-transactional.
* @param method the method to introspect
* @param targetClass the target class (may be {@code null},
* @param targetClass the target class (can be {@code null},
* in which case the declaring class of the method must be used)
* @return the matching transaction attribute, or {@code null} if none found
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -77,7 +77,7 @@ final class TransactionAttributeSourcePointcut extends StaticMethodMatcherPointc
* {@link ClassFilter} that delegates to {@link TransactionAttributeSource#isCandidateClass}
* for filtering classes whose methods are not worth searching to begin with.
*/
private class TransactionAttributeSourceClassFilter implements ClassFilter {
private final class TransactionAttributeSourceClassFilter implements ClassFilter {
@Override
public boolean matches(Class<?> clazz) {
@@ -89,6 +89,7 @@ final class TransactionAttributeSourcePointcut extends StaticMethodMatcherPointc
return (transactionAttributeSource == null || transactionAttributeSource.isCandidateClass(clazz));
}
@Nullable
private TransactionAttributeSource getTransactionAttributeSource() {
return transactionAttributeSource;
}
@@ -96,7 +97,7 @@ final class TransactionAttributeSourcePointcut extends StaticMethodMatcherPointc
@Override
public boolean equals(@Nullable Object other) {
return (this == other || (other instanceof TransactionAttributeSourceClassFilter that &&
ObjectUtils.nullSafeEquals(transactionAttributeSource, that.getTransactionAttributeSource())));
ObjectUtils.nullSafeEquals(getTransactionAttributeSource(), that.getTransactionAttributeSource())));
}
@Override
@@ -106,9 +107,8 @@ final class TransactionAttributeSourcePointcut extends StaticMethodMatcherPointc
@Override
public String toString() {
return TransactionAttributeSourceClassFilter.class.getName() + ": " + transactionAttributeSource;
return TransactionAttributeSourceClassFilter.class.getName() + ": " + getTransactionAttributeSource();
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 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.
@@ -113,6 +113,7 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<R
}
@Override
@Nullable
protected Long getContentLength(Resource resource, @Nullable MediaType contentType) throws IOException {
// Don't try to determine contentLength on InputStreamResource - cannot be read afterwards...
// Note: custom InputStreamResource subclasses could provide a pre-calculated content length!
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -561,6 +561,7 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
}
@Override
@Nullable
protected Long getContentLength(Object object, @Nullable MediaType contentType) throws IOException {
if (object instanceof MappingJacksonValue mappingJacksonValue) {
object = mappingJacksonValue.getValue();
@@ -0,0 +1,44 @@
/*
* Copyright 2002-2024 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.web.context.request.async;
import java.io.IOException;
/**
* Raised when the response for an asynchronous request becomes unusable as
* indicated by a write failure, or a Servlet container error notification, or
* after the async request has completed.
*
* <p>The exception relies on response wrapping, and on {@code AsyncListener}
* notifications, managed by {@link StandardServletAsyncWebRequest}.
*
* @author Rossen Stoyanchev
* @since 5.3.33
*/
@SuppressWarnings("serial")
public class AsyncRequestNotUsableException extends IOException {
public AsyncRequestNotUsableException(String message) {
super(message);
}
public AsyncRequestNotUsableException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -17,16 +17,21 @@
package org.springframework.web.context.request.async;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.Locale;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.AsyncEvent;
import jakarta.servlet.AsyncListener;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.WriteListener;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponseWrapper;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -45,8 +50,6 @@ import org.springframework.web.context.request.ServletWebRequest;
*/
public class StandardServletAsyncWebRequest extends ServletWebRequest implements AsyncWebRequest, AsyncListener {
private final AtomicBoolean asyncCompleted = new AtomicBoolean();
private final List<Runnable> timeoutHandlers = new ArrayList<>();
private final List<Consumer<Throwable>> exceptionHandlers = new ArrayList<>();
@@ -59,6 +62,10 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
@Nullable
private AsyncContext asyncContext;
private State state;
private final ReentrantLock stateLock = new ReentrantLock();
/**
* Create a new instance for the given request/response pair.
@@ -66,7 +73,26 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
* @param response current HTTP response
*/
public StandardServletAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) {
super(request, response);
this(request, response, null);
}
/**
* Constructor to wrap the request and response for the current dispatch that
* also picks up the state of the last (probably the REQUEST) dispatch.
* @param request current HTTP request
* @param response current HTTP response
* @param previousRequest the existing request from the last dispatch
* @since 5.3.33
*/
StandardServletAsyncWebRequest(HttpServletRequest request, HttpServletResponse response,
@Nullable StandardServletAsyncWebRequest previousRequest) {
super(request, new LifecycleHttpServletResponse(response));
this.state = (previousRequest != null ? previousRequest.state : State.NEW);
//noinspection DataFlowIssue
((LifecycleHttpServletResponse) getResponse()).setAsyncWebRequest(this);
}
@@ -107,7 +133,7 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
*/
@Override
public boolean isAsyncComplete() {
return this.asyncCompleted.get();
return (this.state == State.COMPLETED);
}
@Override
@@ -117,11 +143,18 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
"in async request processing. This is done in Java code using the Servlet API " +
"or by adding \"<async-supported>true</async-supported>\" to servlet and " +
"filter declarations in web.xml.");
Assert.state(!isAsyncComplete(), "Async processing has already completed");
if (isAsyncStarted()) {
return;
}
if (this.state == State.NEW) {
this.state = State.ASYNC;
}
else {
Assert.state(this.state == State.ASYNC, "Cannot start async: [" + this.state + "]");
}
this.asyncContext = getRequest().startAsync(getRequest(), getResponse());
this.asyncContext.addListener(this);
if (this.timeout != null) {
@@ -131,8 +164,10 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
@Override
public void dispatch() {
Assert.state(this.asyncContext != null, "Cannot dispatch without an AsyncContext");
this.asyncContext.dispatch();
Assert.state(this.asyncContext != null, "AsyncContext not yet initialized");
if (!this.isAsyncComplete()) {
this.asyncContext.dispatch();
}
}
@@ -151,14 +186,516 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
@Override
public void onError(AsyncEvent event) throws IOException {
this.exceptionHandlers.forEach(consumer -> consumer.accept(event.getThrowable()));
this.stateLock.lock();
try {
this.state = State.ERROR;
Throwable ex = event.getThrowable();
this.exceptionHandlers.forEach(consumer -> consumer.accept(ex));
}
finally {
this.stateLock.unlock();
}
}
@Override
public void onComplete(AsyncEvent event) throws IOException {
this.completionHandlers.forEach(Runnable::run);
this.asyncContext = null;
this.asyncCompleted.set(true);
this.stateLock.lock();
try {
this.completionHandlers.forEach(Runnable::run);
this.asyncContext = null;
this.state = State.COMPLETED;
}
finally {
this.stateLock.unlock();
}
}
/**
* Package private access for testing only.
*/
ReentrantLock stateLock() {
return this.stateLock;
}
/**
* Response wrapper to wrap the output stream with {@link LifecycleServletOutputStream}.
* @since 5.3.33
*/
private static final class LifecycleHttpServletResponse extends HttpServletResponseWrapper {
@Nullable
private StandardServletAsyncWebRequest asyncWebRequest;
@Nullable
private ServletOutputStream outputStream;
@Nullable
private PrintWriter writer;
public LifecycleHttpServletResponse(HttpServletResponse response) {
super(response);
}
public void setAsyncWebRequest(StandardServletAsyncWebRequest asyncWebRequest) {
this.asyncWebRequest = asyncWebRequest;
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
int level = obtainLockAndCheckState();
try {
if (this.outputStream == null) {
Assert.notNull(this.asyncWebRequest, "Not initialized");
ServletOutputStream delegate = getResponse().getOutputStream();
this.outputStream = new LifecycleServletOutputStream(delegate, this);
}
}
catch (IOException ex) {
handleIOException(ex, "Failed to get ServletResponseOutput");
}
finally {
releaseLock(level);
}
return this.outputStream;
}
@Override
public PrintWriter getWriter() throws IOException {
int level = obtainLockAndCheckState();
try {
if (this.writer == null) {
Assert.notNull(this.asyncWebRequest, "Not initialized");
this.writer = new LifecyclePrintWriter(getResponse().getWriter(), this.asyncWebRequest);
}
}
catch (IOException ex) {
handleIOException(ex, "Failed to get PrintWriter");
}
finally {
releaseLock(level);
}
return this.writer;
}
@Override
public void flushBuffer() throws IOException {
int level = obtainLockAndCheckState();
try {
getResponse().flushBuffer();
}
catch (IOException ex) {
handleIOException(ex, "ServletResponse failed to flushBuffer");
}
finally {
releaseLock(level);
}
}
/**
* Return 0 if checks passed and lock is not needed, 1 if checks passed
* and lock is held, or raise AsyncRequestNotUsableException.
*/
private int obtainLockAndCheckState() throws AsyncRequestNotUsableException {
Assert.notNull(this.asyncWebRequest, "Not initialized");
if (this.asyncWebRequest.state == State.NEW) {
return 0;
}
this.asyncWebRequest.stateLock.lock();
if (this.asyncWebRequest.state == State.ASYNC) {
return 1;
}
this.asyncWebRequest.stateLock.unlock();
throw new AsyncRequestNotUsableException("Response not usable after " +
(this.asyncWebRequest.state == State.COMPLETED ?
"async request completion" : "response errors") + ".");
}
void handleIOException(IOException ex, String msg) throws AsyncRequestNotUsableException {
Assert.notNull(this.asyncWebRequest, "Not initialized");
this.asyncWebRequest.state = State.ERROR;
throw new AsyncRequestNotUsableException(msg + ": " + ex.getMessage(), ex);
}
void releaseLock(int level) {
Assert.notNull(this.asyncWebRequest, "Not initialized");
if (level > 0) {
this.asyncWebRequest.stateLock.unlock();
}
}
}
/**
* Wraps a ServletOutputStream to prevent use after Servlet container onError
* notifications, and after async request completion.
* @since 5.3.33
*/
private static final class LifecycleServletOutputStream extends ServletOutputStream {
private final ServletOutputStream delegate;
private final LifecycleHttpServletResponse response;
private LifecycleServletOutputStream(ServletOutputStream delegate, LifecycleHttpServletResponse response) {
this.delegate = delegate;
this.response = response;
}
@Override
public boolean isReady() {
return this.delegate.isReady();
}
@Override
public void setWriteListener(WriteListener writeListener) {
this.delegate.setWriteListener(writeListener);
}
@Override
public void write(int b) throws IOException {
int level = this.response.obtainLockAndCheckState();
try {
this.delegate.write(b);
}
catch (IOException ex) {
this.response.handleIOException(ex, "ServletOutputStream failed to write");
}
finally {
this.response.releaseLock(level);
}
}
public void write(byte[] buf, int offset, int len) throws IOException {
int level = this.response.obtainLockAndCheckState();
try {
this.delegate.write(buf, offset, len);
}
catch (IOException ex) {
this.response.handleIOException(ex, "ServletOutputStream failed to write");
}
finally {
this.response.releaseLock(level);
}
}
@Override
public void flush() throws IOException {
int level = this.response.obtainLockAndCheckState();
try {
this.delegate.flush();
}
catch (IOException ex) {
this.response.handleIOException(ex, "ServletOutputStream failed to flush");
}
finally {
this.response.releaseLock(level);
}
}
@Override
public void close() throws IOException {
int level = this.response.obtainLockAndCheckState();
try {
this.delegate.close();
}
catch (IOException ex) {
this.response.handleIOException(ex, "ServletOutputStream failed to close");
}
finally {
this.response.releaseLock(level);
}
}
}
/**
* Wraps a PrintWriter to prevent use after Servlet container onError
* notifications, and after async request completion.
* @since 5.3.33
*/
private static final class LifecyclePrintWriter extends PrintWriter {
private final PrintWriter delegate;
private final StandardServletAsyncWebRequest asyncWebRequest;
private LifecyclePrintWriter(PrintWriter delegate, StandardServletAsyncWebRequest asyncWebRequest) {
super(delegate);
this.delegate = delegate;
this.asyncWebRequest = asyncWebRequest;
}
@Override
public void flush() {
int level = tryObtainLockAndCheckState();
if (level > -1) {
try {
this.delegate.flush();
}
finally {
releaseLock(level);
}
}
}
@Override
public void close() {
int level = tryObtainLockAndCheckState();
if (level > -1) {
try {
this.delegate.close();
}
finally {
releaseLock(level);
}
}
}
@Override
public boolean checkError() {
return this.delegate.checkError();
}
@Override
public void write(int c) {
int level = tryObtainLockAndCheckState();
if (level > -1) {
try {
this.delegate.write(c);
}
finally {
releaseLock(level);
}
}
}
@Override
public void write(char[] buf, int off, int len) {
int level = tryObtainLockAndCheckState();
if (level > -1) {
try {
this.delegate.write(buf, off, len);
}
finally {
releaseLock(level);
}
}
}
@Override
public void write(char[] buf) {
this.delegate.write(buf);
}
@Override
public void write(String s, int off, int len) {
int level = tryObtainLockAndCheckState();
if (level > -1) {
try {
this.delegate.write(s, off, len);
}
finally {
releaseLock(level);
}
}
}
@Override
public void write(String s) {
this.delegate.write(s);
}
/**
* Return 0 if checks passed and lock is not needed, 1 if checks passed
* and lock is held, and -1 if checks did not pass.
*/
private int tryObtainLockAndCheckState() {
if (this.asyncWebRequest.state == State.NEW) {
return 0;
}
this.asyncWebRequest.stateLock.lock();
if (this.asyncWebRequest.state == State.ASYNC) {
return 1;
}
this.asyncWebRequest.stateLock.unlock();
return -1;
}
private void releaseLock(int level) {
if (level > 0) {
this.asyncWebRequest.stateLock.unlock();
}
}
// Plain delegates
@Override
public void print(boolean b) {
this.delegate.print(b);
}
@Override
public void print(char c) {
this.delegate.print(c);
}
@Override
public void print(int i) {
this.delegate.print(i);
}
@Override
public void print(long l) {
this.delegate.print(l);
}
@Override
public void print(float f) {
this.delegate.print(f);
}
@Override
public void print(double d) {
this.delegate.print(d);
}
@Override
public void print(char[] s) {
this.delegate.print(s);
}
@Override
public void print(String s) {
this.delegate.print(s);
}
@Override
public void print(Object obj) {
this.delegate.print(obj);
}
@Override
public void println() {
this.delegate.println();
}
@Override
public void println(boolean x) {
this.delegate.println(x);
}
@Override
public void println(char x) {
this.delegate.println(x);
}
@Override
public void println(int x) {
this.delegate.println(x);
}
@Override
public void println(long x) {
this.delegate.println(x);
}
@Override
public void println(float x) {
this.delegate.println(x);
}
@Override
public void println(double x) {
this.delegate.println(x);
}
@Override
public void println(char[] x) {
this.delegate.println(x);
}
@Override
public void println(String x) {
this.delegate.println(x);
}
@Override
public void println(Object x) {
this.delegate.println(x);
}
@Override
public PrintWriter printf(String format, Object... args) {
return this.delegate.printf(format, args);
}
@Override
public PrintWriter printf(Locale l, String format, Object... args) {
return this.delegate.printf(l, format, args);
}
@Override
public PrintWriter format(String format, Object... args) {
return this.delegate.format(format, args);
}
@Override
public PrintWriter format(Locale l, String format, Object... args) {
return this.delegate.format(l, format, args);
}
@Override
public PrintWriter append(CharSequence csq) {
return this.delegate.append(csq);
}
@Override
public PrintWriter append(CharSequence csq, int start, int end) {
return this.delegate.append(csq, start, end);
}
@Override
public PrintWriter append(char c) {
return this.delegate.append(c);
}
}
/**
* Represents a state for {@link StandardServletAsyncWebRequest} to be in.
* <p><pre>
* +------ NEW
* | |
* | v
* | ASYNC ----> +
* | | |
* | v |
* +----> ERROR |
* | |
* v |
* COMPLETED <---+
* </pre>
* @since 5.3.33
*/
private enum State {
/** New request (may not start async handling). */
NEW,
/** Async handling has started. */
ASYNC,
/** ServletOutputStream failed, or onError notification received. */
ERROR,
/** onComplete notification received. */
COMPLETED
}
}
@@ -22,7 +22,7 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicReference;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
@@ -88,12 +88,7 @@ public final class WebAsyncManager {
@Nullable
private volatile Object[] concurrentResultContext;
/*
* Whether the concurrentResult is an error. If such errors remain unhandled, some
* Servlet containers will call AsyncListener#onError at the end, after the ASYNC
* and/or the ERROR dispatch (Boot's case), and we need to ignore those.
*/
private volatile boolean errorHandlingInProgress;
private final AtomicReference<State> state = new AtomicReference<>(State.NOT_STARTED);
private final Map<Object, CallableProcessingInterceptor> callableInterceptors = new LinkedHashMap<>();
@@ -125,6 +120,15 @@ public final class WebAsyncManager {
WebAsyncUtils.WEB_ASYNC_MANAGER_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST));
}
/**
* Return the current {@link AsyncWebRequest}.
* @since 5.3.33
*/
@Nullable
public AsyncWebRequest getAsyncWebRequest() {
return this.asyncWebRequest;
}
/**
* Configure an AsyncTaskExecutor for use with concurrent processing via
* {@link #startCallableProcessing(Callable, Object...)}.
@@ -135,8 +139,8 @@ public final class WebAsyncManager {
}
/**
* Whether the selected handler for the current request chose to handle the
* request asynchronously. A return value of "true" indicates concurrent
* Return whether the selected handler for the current request chose to handle
* the request asynchronously. A return value of "true" indicates concurrent
* handling is under way and the response will remain open. A return value
* of "false" means concurrent handling was either not started or possibly
* that it has completed and the request was dispatched for further
@@ -147,16 +151,16 @@ public final class WebAsyncManager {
}
/**
* Whether a result value exists as a result of concurrent handling.
* Return whether a result value exists as a result of concurrent handling.
*/
public boolean hasConcurrentResult() {
return (this.concurrentResult != RESULT_NONE);
}
/**
* Provides access to the result from concurrent handling.
* Get the result from concurrent handling.
* @return an Object, possibly an {@code Exception} or {@code Throwable} if
* concurrent handling raised one.
* concurrent handling raised one
* @see #clearConcurrentResult()
*/
@Nullable
@@ -165,8 +169,7 @@ public final class WebAsyncManager {
}
/**
* Provides access to additional processing context saved at the start of
* concurrent handling.
* Get the additional processing context saved at the start of concurrent handling.
* @see #clearConcurrentResult()
*/
@Nullable
@@ -207,7 +210,7 @@ public final class WebAsyncManager {
/**
* Register a {@link CallableProcessingInterceptor} without a key.
* The key is derived from the class name and hashcode.
* The key is derived from the class name and hash code.
* @param interceptors one or more interceptors to register
*/
public void registerCallableInterceptors(CallableProcessingInterceptor... interceptors) {
@@ -230,8 +233,8 @@ public final class WebAsyncManager {
}
/**
* Register one or more {@link DeferredResultProcessingInterceptor DeferredResultProcessingInterceptors} without a specified key.
* The default key is derived from the interceptor class name and hash code.
* Register one or more {@link DeferredResultProcessingInterceptor DeferredResultProcessingInterceptors}
* without a specified key. The default key is derived from the interceptor class name and hash code.
* @param interceptors one or more interceptors to register
*/
public void registerDeferredResultInterceptors(DeferredResultProcessingInterceptor... interceptors) {
@@ -247,6 +250,12 @@ public final class WebAsyncManager {
* {@linkplain #getConcurrentResultContext() concurrentResultContext}.
*/
public void clearConcurrentResult() {
if (!this.state.compareAndSet(State.RESULT_SET, State.NOT_STARTED)) {
if (logger.isDebugEnabled()) {
logger.debug("Unexpected call to clear: [" + this.state.get() + "]");
}
return;
}
synchronized (WebAsyncManager.this) {
this.concurrentResult = RESULT_NONE;
this.concurrentResultContext = null;
@@ -287,6 +296,11 @@ public final class WebAsyncManager {
Assert.notNull(webAsyncTask, "WebAsyncTask must not be null");
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null");
if (!this.state.compareAndSet(State.NOT_STARTED, State.ASYNC_PROCESSING)) {
throw new IllegalStateException(
"Unexpected call to startCallableProcessing: [" + this.state.get() + "]");
}
Long timeout = webAsyncTask.getTimeout();
if (timeout != null) {
this.asyncWebRequest.setTimeout(timeout);
@@ -297,7 +311,7 @@ public final class WebAsyncManager {
this.taskExecutor = executor;
}
else {
logExecutorWarning();
logExecutorWarning(this.asyncWebRequest);
}
List<CallableProcessingInterceptor> interceptors = new ArrayList<>();
@@ -310,7 +324,7 @@ public final class WebAsyncManager {
this.asyncWebRequest.addTimeoutHandler(() -> {
if (logger.isDebugEnabled()) {
logger.debug("Async request timeout for " + formatRequestUri());
logger.debug("Servlet container timeout notification for " + formatUri(this.asyncWebRequest));
}
Object result = interceptorChain.triggerAfterTimeout(this.asyncWebRequest, callable);
if (result != CallableProcessingInterceptor.RESULT_NONE) {
@@ -319,14 +333,12 @@ public final class WebAsyncManager {
});
this.asyncWebRequest.addErrorHandler(ex -> {
if (!this.errorHandlingInProgress) {
if (logger.isDebugEnabled()) {
logger.debug("Async request error for " + formatRequestUri() + ": " + ex);
}
Object result = interceptorChain.triggerAfterError(this.asyncWebRequest, callable, ex);
result = (result != CallableProcessingInterceptor.RESULT_NONE ? result : ex);
setConcurrentResultAndDispatch(result);
if (logger.isDebugEnabled()) {
logger.debug("Servlet container error notification for " + formatUri(this.asyncWebRequest) + ": " + ex);
}
Object result = interceptorChain.triggerAfterError(this.asyncWebRequest, callable, ex);
result = (result != CallableProcessingInterceptor.RESULT_NONE ? result : ex);
setConcurrentResultAndDispatch(result);
});
this.asyncWebRequest.addCompletionHandler(() ->
@@ -351,14 +363,13 @@ public final class WebAsyncManager {
});
interceptorChain.setTaskFuture(future);
}
catch (RejectedExecutionException ex) {
catch (Throwable ex) {
Object result = interceptorChain.applyPostProcess(this.asyncWebRequest, callable, ex);
setConcurrentResultAndDispatch(result);
throw ex;
}
}
private void logExecutorWarning() {
private void logExecutorWarning(AsyncWebRequest asyncWebRequest) {
if (taskExecutorWarning && logger.isWarnEnabled()) {
synchronized (DEFAULT_TASK_EXECUTOR) {
AsyncTaskExecutor executor = this.taskExecutor;
@@ -370,7 +381,7 @@ public final class WebAsyncManager {
"Please, configure a TaskExecutor in the MVC config under \"async support\".\n" +
"The " + executorTypeName + " currently in use is not suitable under load.\n" +
"-------------------------------\n" +
"Request URI: '" + formatRequestUri() + "'\n" +
"Request URI: '" + formatUri(asyncWebRequest) + "'\n" +
"!!!");
taskExecutorWarning = false;
}
@@ -378,32 +389,35 @@ public final class WebAsyncManager {
}
}
private String formatRequestUri() {
HttpServletRequest request = this.asyncWebRequest.getNativeRequest(HttpServletRequest.class);
return request != null ? request.getRequestURI() : "servlet container";
}
private void setConcurrentResultAndDispatch(@Nullable Object result) {
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null");
synchronized (WebAsyncManager.this) {
if (this.concurrentResult != RESULT_NONE) {
if (!this.state.compareAndSet(State.ASYNC_PROCESSING, State.RESULT_SET)) {
if (logger.isDebugEnabled()) {
logger.debug("Async result already set: " +
"[" + this.state.get() + "], ignored result: " + result +
" for " + formatUri(this.asyncWebRequest));
}
return;
}
this.concurrentResult = result;
this.errorHandlingInProgress = (result instanceof Throwable);
}
if (this.asyncWebRequest.isAsyncComplete()) {
if (logger.isDebugEnabled()) {
logger.debug("Async result set but request already complete: " + formatRequestUri());
logger.debug("Async result set to: " + result + " for " + formatUri(this.asyncWebRequest));
}
return;
}
if (logger.isDebugEnabled()) {
boolean isError = result instanceof Throwable;
logger.debug("Async " + (isError ? "error" : "result set") + ", dispatch to " + formatRequestUri());
if (this.asyncWebRequest.isAsyncComplete()) {
if (logger.isDebugEnabled()) {
logger.debug("Async request already completed for " + formatUri(this.asyncWebRequest));
}
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Performing async dispatch for " + formatUri(this.asyncWebRequest));
}
this.asyncWebRequest.dispatch();
}
this.asyncWebRequest.dispatch();
}
/**
@@ -426,6 +440,11 @@ public final class WebAsyncManager {
Assert.notNull(deferredResult, "DeferredResult must not be null");
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null");
if (!this.state.compareAndSet(State.NOT_STARTED, State.ASYNC_PROCESSING)) {
throw new IllegalStateException(
"Unexpected call to startDeferredResultProcessing: [" + this.state.get() + "]");
}
Long timeout = deferredResult.getTimeoutValue();
if (timeout != null) {
this.asyncWebRequest.setTimeout(timeout);
@@ -439,6 +458,9 @@ public final class WebAsyncManager {
final DeferredResultInterceptorChain interceptorChain = new DeferredResultInterceptorChain(interceptors);
this.asyncWebRequest.addTimeoutHandler(() -> {
if (logger.isDebugEnabled()) {
logger.debug("Servlet container timeout notification for " + formatUri(this.asyncWebRequest));
}
try {
interceptorChain.triggerAfterTimeout(this.asyncWebRequest, deferredResult);
}
@@ -448,21 +470,22 @@ public final class WebAsyncManager {
});
this.asyncWebRequest.addErrorHandler(ex -> {
if (!this.errorHandlingInProgress) {
try {
if (!interceptorChain.triggerAfterError(this.asyncWebRequest, deferredResult, ex)) {
return;
}
deferredResult.setErrorResult(ex);
}
catch (Throwable interceptorEx) {
setConcurrentResultAndDispatch(interceptorEx);
if (logger.isDebugEnabled()) {
logger.debug("Servlet container error notification for " + formatUri(this.asyncWebRequest));
}
try {
if (!interceptorChain.triggerAfterError(this.asyncWebRequest, deferredResult, ex)) {
return;
}
deferredResult.setErrorResult(ex);
}
catch (Throwable interceptorEx) {
setConcurrentResultAndDispatch(interceptorEx);
}
});
this.asyncWebRequest.addCompletionHandler(()
-> interceptorChain.triggerAfterCompletion(this.asyncWebRequest, deferredResult));
this.asyncWebRequest.addCompletionHandler(() ->
interceptorChain.triggerAfterCompletion(this.asyncWebRequest, deferredResult));
interceptorChain.applyBeforeConcurrentHandling(this.asyncWebRequest, deferredResult);
startAsyncProcessing(processingContext);
@@ -483,13 +506,46 @@ public final class WebAsyncManager {
synchronized (WebAsyncManager.this) {
this.concurrentResult = RESULT_NONE;
this.concurrentResultContext = processingContext;
this.errorHandlingInProgress = false;
}
this.asyncWebRequest.startAsync();
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null");
if (logger.isDebugEnabled()) {
logger.debug("Started async request");
logger.debug("Started async request for " + formatUri(this.asyncWebRequest));
}
this.asyncWebRequest.startAsync();
}
private static String formatUri(AsyncWebRequest asyncWebRequest) {
HttpServletRequest request = asyncWebRequest.getNativeRequest(HttpServletRequest.class);
return (request != null ? "\"" + request.getRequestURI() + "\"" : "servlet container");
}
/**
* Represents a state for {@link WebAsyncManager} to be in.
* <p><pre>
* NOT_STARTED <------+
* | |
* v |
* ASYNC_PROCESSING |
* | |
* v |
* RESULT_SET -------+
* </pre>
* @since 5.3.33
*/
private enum State {
/** No async processing in progress. */
NOT_STARTED,
/** Async handling has started, but the result hasn't been set yet. */
ASYNC_PROCESSING,
/** The result is set, and an async dispatch was performed, unless there is a network error. */
RESULT_SET
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -47,12 +47,16 @@ public class WebAsyncTask<V> implements BeanFactoryAware {
@Nullable
private final String executorName;
@Nullable
private BeanFactory beanFactory;
@Nullable
private Callable<V> timeoutCallback;
@Nullable
private Callable<V> errorCallback;
@Nullable
private Runnable completionCallback;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -82,7 +82,10 @@ public abstract class WebAsyncUtils {
* @return an AsyncWebRequest instance (never {@code null})
*/
public static AsyncWebRequest createAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) {
return new StandardServletAsyncWebRequest(request, response);
AsyncWebRequest prev = getAsyncManager(request).getAsyncWebRequest();
return (prev instanceof StandardServletAsyncWebRequest standardRequest ?
new StandardServletAsyncWebRequest(request, response, standardRequest) :
new StandardServletAsyncWebRequest(request, response));
}
}
@@ -21,10 +21,10 @@ import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.LinkedHashSet;
import java.util.Set;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.WriteListener;
@@ -38,11 +38,12 @@ import org.springframework.util.FastByteArrayOutputStream;
/**
* {@link jakarta.servlet.http.HttpServletResponse} wrapper that caches all content written to
* the {@linkplain #getOutputStream() output stream} and {@linkplain #getWriter() writer},
* and allows this content to be retrieved via a {@link #getContentAsByteArray() byte array}.
* and allows this content to be retrieved via a {@linkplain #getContentAsByteArray() byte array}.
*
* <p>Used e.g. by {@link org.springframework.web.filter.ShallowEtagHeaderFilter}.
*
* @author Juergen Hoeller
* @author Sam Brannen
* @since 4.1.3
* @see ContentCachingRequestWrapper
*/
@@ -59,9 +60,6 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
@Nullable
private Integer contentLength;
@Nullable
private String contentType;
/**
* Create a new ContentCachingResponseWrapper for the given servlet response.
@@ -120,9 +118,16 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
return this.writer;
}
/**
* This method neither flushes content to the client nor commits the underlying
* response, since the content has not yet been copied to the response.
* <p>Invoke {@link #copyBodyToResponse()} to copy the cached body content to
* the wrapped response object and flush its buffer.
* @see jakarta.servlet.ServletResponseWrapper#flushBuffer()
*/
@Override
public void flushBuffer() throws IOException {
// do not flush the underlying response as the content has not been copied to it yet
// no-op
}
@Override
@@ -139,31 +144,13 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
throw new IllegalArgumentException("Content-Length exceeds ContentCachingResponseWrapper's maximum (" +
Integer.MAX_VALUE + "): " + len);
}
int lenInt = (int) len;
if (lenInt > this.content.size()) {
this.content.resize(lenInt);
}
this.contentLength = lenInt;
}
@Override
public void setContentType(String type) {
this.contentType = type;
}
@Override
@Nullable
public String getContentType() {
return this.contentType;
setContentLength((int) len);
}
@Override
public boolean containsHeader(String name) {
if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) {
return this.contentLength != null;
}
else if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
return this.contentType != null;
if (this.contentLength != null && HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) {
return true;
}
else {
return super.containsHeader(name);
@@ -175,9 +162,6 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) {
this.contentLength = Integer.valueOf(value);
}
else if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
this.contentType = value;
}
else {
super.setHeader(name, value);
}
@@ -188,9 +172,6 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) {
this.contentLength = Integer.valueOf(value);
}
else if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
this.contentType = value;
}
else {
super.addHeader(name, value);
}
@@ -219,11 +200,8 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
@Override
@Nullable
public String getHeader(String name) {
if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) {
return (this.contentLength != null) ? this.contentLength.toString() : null;
}
else if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
return this.contentType;
if (this.contentLength != null && HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) {
return this.contentLength.toString();
}
else {
return super.getHeader(name);
@@ -232,12 +210,8 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
@Override
public Collection<String> getHeaders(String name) {
if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) {
return this.contentLength != null ? Collections.singleton(this.contentLength.toString()) :
Collections.emptySet();
}
else if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
return this.contentType != null ? Collections.singleton(this.contentType) : Collections.emptySet();
if (this.contentLength != null && HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) {
return Collections.singleton(this.contentLength.toString());
}
else {
return super.getHeaders(name);
@@ -247,14 +221,9 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
@Override
public Collection<String> getHeaderNames() {
Collection<String> headerNames = super.getHeaderNames();
if (this.contentLength != null || this.contentType != null) {
List<String> result = new ArrayList<>(headerNames);
if (this.contentLength != null) {
result.add(HttpHeaders.CONTENT_LENGTH);
}
if (this.contentType != null) {
result.add(HttpHeaders.CONTENT_TYPE);
}
if (this.contentLength != null) {
Set<String> result = new LinkedHashSet<>(headerNames);
result.add(HttpHeaders.CONTENT_LENGTH);
return result;
}
else {
@@ -327,10 +296,6 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
}
this.contentLength = null;
}
if (complete || this.contentType != null) {
rawResponse.setContentType(this.contentType);
this.contentType = null;
}
}
this.content.writeTo(rawResponse.getOutputStream());
this.content.reset();
@@ -0,0 +1,96 @@
/*
* Copyright 2002-2024 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.web.util;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.NestedExceptionUtils;
import org.springframework.util.Assert;
/**
* Utility methods to assist with identifying and logging exceptions that indicate
* the client has gone away. Such exceptions fill logs with unnecessary stack
* traces. The utility methods help to log a single line message at DEBUG level,
* and a full stacktrace at TRACE level.
*
* @author Rossen Stoyanchev
* @since 6.1
*/
public class DisconnectedClientHelper {
private static final Set<String> EXCEPTION_PHRASES =
Set.of("broken pipe", "connection reset by peer");
private static final Set<String> EXCEPTION_TYPE_NAMES =
Set.of("AbortedException", "ClientAbortException",
"EOFException", "EofException", "AsyncRequestNotUsableException");
private final Log logger;
public DisconnectedClientHelper(String logCategory) {
Assert.notNull(logCategory, "'logCategory' is required");
this.logger = LogFactory.getLog(logCategory);
}
/**
* Check via {@link #isClientDisconnectedException} if the exception
* indicates the remote client disconnected, and if so log a single line
* message when DEBUG is on, and a full stacktrace when TRACE is on for
* the configured logger.
*/
public boolean checkAndLogClientDisconnectedException(Throwable ex) {
if (isClientDisconnectedException(ex)) {
if (logger.isTraceEnabled()) {
logger.trace("Looks like the client has gone away", ex);
}
else if (logger.isDebugEnabled()) {
logger.debug("Looks like the client has gone away: " + ex +
" (For a full stack trace, set the log category '" + logger + "' to TRACE level.)");
}
return true;
}
return false;
}
/**
* Whether the given exception indicates the client has gone away.
* Known cases covered:
* <ul>
* <li>ClientAbortException or EOFException for Tomcat
* <li>EofException for Jetty
* <li>IOException "Broken pipe" or "connection reset by peer"
* </ul>
*/
public static boolean isClientDisconnectedException(Throwable ex) {
String message = NestedExceptionUtils.getMostSpecificCause(ex).getMessage();
if (message != null) {
String text = message.toLowerCase();
for (String phrase : EXCEPTION_PHRASES) {
if (text.contains(phrase)) {
return true;
}
}
}
return EXCEPTION_TYPE_NAMES.contains(ex.getClass().getSimpleName());
}
}
@@ -77,9 +77,9 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable {
private static final String HTTP_PATTERN = "(?i)(http|https):";
private static final String USERINFO_PATTERN = "([^@/?#]*)";
private static final String USERINFO_PATTERN = "([^/?#]*)";
private static final String HOST_IPV4_PATTERN = "[^\\[/?#:]*";
private static final String HOST_IPV4_PATTERN = "[^/?#:]*";
private static final String HOST_IPV6_PATTERN = "\\[[\\p{XDigit}:.]*[%\\p{Alnum}]*]";
@@ -252,9 +252,7 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable {
builder.schemeSpecificPart(ssp);
}
else {
if (StringUtils.hasLength(scheme) && scheme.startsWith("http") && !StringUtils.hasLength(host)) {
throw new IllegalArgumentException("[" + uri + "] is not a valid HTTP URL");
}
checkSchemeAndHost(uri, scheme, host);
builder.userInfo(userInfo);
builder.host(host);
if (StringUtils.hasLength(port)) {
@@ -296,9 +294,7 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable {
builder.scheme(scheme != null ? scheme.toLowerCase() : null);
builder.userInfo(matcher.group(4));
String host = matcher.group(5);
if (StringUtils.hasLength(scheme) && !StringUtils.hasLength(host)) {
throw new IllegalArgumentException("[" + httpUrl + "] is not a valid HTTP URL");
}
checkSchemeAndHost(httpUrl, scheme, host);
builder.host(host);
String port = matcher.group(7);
if (StringUtils.hasLength(port)) {
@@ -317,6 +313,15 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable {
}
}
private static void checkSchemeAndHost(String uri, @Nullable String scheme, @Nullable String host) {
if (StringUtils.hasLength(scheme) && scheme.startsWith("http") && !StringUtils.hasLength(host)) {
throw new IllegalArgumentException("[" + uri + "] is not a valid HTTP URL");
}
if (StringUtils.hasLength(host) && host.startsWith("[") && !host.endsWith("]")) {
throw new IllegalArgumentException("Invalid IPV6 host in [" + uri + "]");
}
}
/**
* Create a new {@code UriComponents} object from the URI associated with
* the given HttpRequest while also overlaying with values from the headers
@@ -402,6 +407,7 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable {
if (StringUtils.hasLength(port)) {
builder.port(port);
}
checkSchemeAndHost(origin, scheme, host);
return builder;
}
else {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 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.
@@ -66,7 +66,7 @@ public class UriTemplate implements Serializable {
* @param uriTemplate the URI template string
*/
public UriTemplate(String uriTemplate) {
Assert.hasText(uriTemplate, "'uriTemplate' must not be null");
Assert.notNull(uriTemplate, "'uriTemplate' must not be null");
this.uriTemplate = uriTemplate;
this.uriComponents = UriComponentsBuilder.fromUriString(uriTemplate).build();
@@ -0,0 +1,357 @@
/*
* Copyright 2002-2024 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.web.context.request.async;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.concurrent.atomic.AtomicInteger;
import jakarta.servlet.AsyncEvent;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.mockito.stubbing.Answer;
import org.springframework.web.testfixture.servlet.MockAsyncContext;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.BDDMockito.doAnswer;
import static org.mockito.BDDMockito.doThrow;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.mock;
import static org.mockito.BDDMockito.verify;
import static org.mockito.BDDMockito.verifyNoInteractions;
/**
* {@link StandardServletAsyncWebRequest} tests related to response wrapping in
* order to enforce thread safety and prevent use after errors.
*
* @author Rossen Stoyanchev
*/
public class AsyncRequestNotUsableTests {
private final MockHttpServletRequest request = new MockHttpServletRequest();
private final HttpServletResponse response = mock();
private final ServletOutputStream outputStream = mock();
private final PrintWriter writer = mock();
private StandardServletAsyncWebRequest asyncRequest;
@BeforeEach
void setup() throws IOException {
this.request.setAsyncSupported(true);
given(this.response.getOutputStream()).willReturn(this.outputStream);
given(this.response.getWriter()).willReturn(this.writer);
this.asyncRequest = new StandardServletAsyncWebRequest(this.request, this.response);
}
@AfterEach
void tearDown() {
assertThat(this.asyncRequest.stateLock().isLocked()).isFalse();
}
@SuppressWarnings("DataFlowIssue")
private ServletOutputStream getWrappedOutputStream() throws IOException {
return this.asyncRequest.getResponse().getOutputStream();
}
@SuppressWarnings("DataFlowIssue")
private PrintWriter getWrappedWriter() throws IOException {
return this.asyncRequest.getResponse().getWriter();
}
@Nested
class ResponseTests {
@Test
void notUsableAfterError() throws IOException {
asyncRequest.startAsync();
asyncRequest.onError(new AsyncEvent(new MockAsyncContext(request, response), new Exception()));
HttpServletResponse wrapped = asyncRequest.getResponse();
assertThat(wrapped).isNotNull();
assertThatThrownBy(wrapped::getOutputStream).hasMessage("Response not usable after response errors.");
assertThatThrownBy(wrapped::getWriter).hasMessage("Response not usable after response errors.");
assertThatThrownBy(wrapped::flushBuffer).hasMessage("Response not usable after response errors.");
}
@Test
void notUsableAfterCompletion() throws IOException {
asyncRequest.startAsync();
asyncRequest.onComplete(new AsyncEvent(new MockAsyncContext(request, response)));
HttpServletResponse wrapped = asyncRequest.getResponse();
assertThat(wrapped).isNotNull();
assertThatThrownBy(wrapped::getOutputStream).hasMessage("Response not usable after async request completion.");
assertThatThrownBy(wrapped::getWriter).hasMessage("Response not usable after async request completion.");
assertThatThrownBy(wrapped::flushBuffer).hasMessage("Response not usable after async request completion.");
}
@Test
void notUsableWhenRecreatedAfterCompletion() throws IOException {
asyncRequest.startAsync();
asyncRequest.onComplete(new AsyncEvent(new MockAsyncContext(request, response)));
StandardServletAsyncWebRequest newWebRequest =
new StandardServletAsyncWebRequest(request, response, asyncRequest);
HttpServletResponse wrapped = newWebRequest.getResponse();
assertThat(wrapped).isNotNull();
assertThatThrownBy(wrapped::getOutputStream).hasMessage("Response not usable after async request completion.");
assertThatThrownBy(wrapped::getWriter).hasMessage("Response not usable after async request completion.");
assertThatThrownBy(wrapped::flushBuffer).hasMessage("Response not usable after async request completion.");
}
}
@Nested
class OutputStreamTests {
@Test
void use() throws IOException {
testUseOutputStream();
}
@Test
void useInAsyncState() throws IOException {
asyncRequest.startAsync();
testUseOutputStream();
}
private void testUseOutputStream() throws IOException {
ServletOutputStream wrapped = getWrappedOutputStream();
wrapped.write('a');
wrapped.write(new byte[0], 1, 2);
wrapped.flush();
wrapped.close();
verify(outputStream).write('a');
verify(outputStream).write(new byte[0], 1, 2);
verify(outputStream).flush();
verify(outputStream).close();
}
@Test
void notUsableAfterCompletion() throws IOException {
asyncRequest.startAsync();
ServletOutputStream wrapped = getWrappedOutputStream();
asyncRequest.onComplete(new AsyncEvent(new MockAsyncContext(request, response)));
assertThatThrownBy(() -> wrapped.write('a')).hasMessage("Response not usable after async request completion.");
assertThatThrownBy(() -> wrapped.write(new byte[0])).hasMessage("Response not usable after async request completion.");
assertThatThrownBy(() -> wrapped.write(new byte[0], 0, 0)).hasMessage("Response not usable after async request completion.");
assertThatThrownBy(wrapped::flush).hasMessage("Response not usable after async request completion.");
assertThatThrownBy(wrapped::close).hasMessage("Response not usable after async request completion.");
}
@Test
void lockingNotUsed() throws IOException {
AtomicInteger count = new AtomicInteger(-1);
doAnswer((Answer<Void>) invocation -> {
count.set(asyncRequest.stateLock().getHoldCount());
return null;
}).when(outputStream).write('a');
// Access ServletOutputStream in NEW state (no async handling) without locking
getWrappedOutputStream().write('a');
assertThat(count.get()).isEqualTo(0);
}
@Test
void lockingUsedInAsyncState() throws IOException {
AtomicInteger count = new AtomicInteger(-1);
doAnswer((Answer<Void>) invocation -> {
count.set(asyncRequest.stateLock().getHoldCount());
return null;
}).when(outputStream).write('a');
// Access ServletOutputStream in ASYNC state with locking
asyncRequest.startAsync();
getWrappedOutputStream().write('a');
assertThat(count.get()).isEqualTo(1);
}
}
@Nested
class OutputStreamErrorTests {
@Test
void writeInt() throws IOException {
asyncRequest.startAsync();
ServletOutputStream wrapped = getWrappedOutputStream();
doThrow(new IOException("Broken pipe")).when(outputStream).write('a');
assertThatThrownBy(() -> wrapped.write('a')).hasMessage("ServletOutputStream failed to write: Broken pipe");
}
@Test
void writeBytesFull() throws IOException {
asyncRequest.startAsync();
ServletOutputStream wrapped = getWrappedOutputStream();
byte[] bytes = new byte[0];
doThrow(new IOException("Broken pipe")).when(outputStream).write(bytes, 0, 0);
assertThatThrownBy(() -> wrapped.write(bytes)).hasMessage("ServletOutputStream failed to write: Broken pipe");
}
@Test
void writeBytes() throws IOException {
asyncRequest.startAsync();
ServletOutputStream wrapped = getWrappedOutputStream();
byte[] bytes = new byte[0];
doThrow(new IOException("Broken pipe")).when(outputStream).write(bytes, 0, 0);
assertThatThrownBy(() -> wrapped.write(bytes, 0, 0)).hasMessage("ServletOutputStream failed to write: Broken pipe");
}
@Test
void flush() throws IOException {
asyncRequest.startAsync();
ServletOutputStream wrapped = getWrappedOutputStream();
doThrow(new IOException("Broken pipe")).when(outputStream).flush();
assertThatThrownBy(wrapped::flush).hasMessage("ServletOutputStream failed to flush: Broken pipe");
}
@Test
void close() throws IOException {
asyncRequest.startAsync();
ServletOutputStream wrapped = getWrappedOutputStream();
doThrow(new IOException("Broken pipe")).when(outputStream).close();
assertThatThrownBy(wrapped::close).hasMessage("ServletOutputStream failed to close: Broken pipe");
}
@Test
void writeErrorPreventsFurtherWriting() throws IOException {
ServletOutputStream wrapped = getWrappedOutputStream();
doThrow(new IOException("Broken pipe")).when(outputStream).write('a');
assertThatThrownBy(() -> wrapped.write('a')).hasMessage("ServletOutputStream failed to write: Broken pipe");
assertThatThrownBy(() -> wrapped.write('a')).hasMessage("Response not usable after response errors.");
}
@Test
void writeErrorInAsyncStatePreventsFurtherWriting() throws IOException {
asyncRequest.startAsync();
ServletOutputStream wrapped = getWrappedOutputStream();
doThrow(new IOException("Broken pipe")).when(outputStream).write('a');
assertThatThrownBy(() -> wrapped.write('a')).hasMessage("ServletOutputStream failed to write: Broken pipe");
assertThatThrownBy(() -> wrapped.write('a')).hasMessage("Response not usable after response errors.");
}
}
@Nested
class WriterTests {
@Test
void useWriter() throws IOException {
testUseWriter();
}
@Test
void useWriterInAsyncState() throws IOException {
asyncRequest.startAsync();
testUseWriter();
}
private void testUseWriter() throws IOException {
PrintWriter wrapped = getWrappedWriter();
wrapped.write('a');
wrapped.write(new char[0], 1, 2);
wrapped.write("abc", 1, 2);
wrapped.flush();
wrapped.close();
verify(writer).write('a');
verify(writer).write(new char[0], 1, 2);
verify(writer).write("abc", 1, 2);
verify(writer).flush();
verify(writer).close();
}
@Test
void writerNotUsableAfterCompletion() throws IOException {
asyncRequest.startAsync();
PrintWriter wrapped = getWrappedWriter();
asyncRequest.onComplete(new AsyncEvent(new MockAsyncContext(request, response)));
char[] chars = new char[0];
wrapped.write('a');
wrapped.write(chars, 1, 2);
wrapped.flush();
wrapped.close();
verifyNoInteractions(writer);
}
@Test
void lockingNotUsed() throws IOException {
AtomicInteger count = new AtomicInteger(-1);
doAnswer((Answer<Void>) invocation -> {
count.set(asyncRequest.stateLock().getHoldCount());
return null;
}).when(writer).write('a');
// Use Writer in NEW state (no async handling) without locking
PrintWriter wrapped = getWrappedWriter();
wrapped.write('a');
assertThat(count.get()).isEqualTo(0);
}
@Test
void lockingUsedInAsyncState() throws IOException {
AtomicInteger count = new AtomicInteger(-1);
doAnswer((Answer<Void>) invocation -> {
count.set(asyncRequest.stateLock().getHoldCount());
return null;
}).when(writer).write('a');
// Use Writer in ASYNC state with locking
asyncRequest.startAsync();
PrintWriter wrapped = getWrappedWriter();
wrapped.write('a');
assertThat(count.get()).isEqualTo(1);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -95,9 +95,8 @@ public class StandardServletAsyncWebRequestTests {
@Test
public void startAsyncAfterCompleted() throws Exception {
this.asyncRequest.onComplete(new AsyncEvent(new MockAsyncContext(this.request, this.response)));
assertThatIllegalStateException().isThrownBy(
this.asyncRequest::startAsync)
.withMessage("Async processing has already completed");
assertThatIllegalStateException().isThrownBy(this.asyncRequest::startAsync)
.withMessage("Cannot start async: [COMPLETED]");
}
@Test
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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,54 +16,254 @@
package org.springframework.web.filter;
import java.nio.charset.StandardCharsets;
import java.util.stream.Stream;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.Named;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
import org.springframework.web.util.ContentCachingResponseWrapper;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Named.named;
import static org.springframework.http.HttpHeaders.CONTENT_LENGTH;
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
import static org.springframework.http.HttpHeaders.TRANSFER_ENCODING;
/**
* Unit tests for {@link ContentCachingResponseWrapper}.
*
* @author Rossen Stoyanchev
* @author Sam Brannen
*/
public class ContentCachingResponseWrapperTests {
class ContentCachingResponseWrapperTests {
@Test
void copyBodyToResponse() throws Exception {
byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
byte[] responseBody = "Hello World".getBytes(UTF_8);
MockHttpServletResponse response = new MockHttpServletResponse();
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
responseWrapper.setStatus(HttpServletResponse.SC_OK);
responseWrapper.setStatus(HttpServletResponse.SC_CREATED);
FileCopyUtils.copy(responseBody, responseWrapper.getOutputStream());
responseWrapper.copyBodyToResponse();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_CREATED);
assertThat(response.getContentLength()).isGreaterThan(0);
assertThat(response.getContentAsByteArray()).isEqualTo(responseBody);
}
@Test
void copyBodyToResponseWithPresetHeaders() throws Exception {
String PUZZLE = "puzzle";
String ENIGMA = "enigma";
String NUMBER = "number";
String MAGIC = "42";
byte[] responseBody = "Hello World".getBytes(UTF_8);
int responseLength = responseBody.length;
int originalContentLength = 999;
String contentType = MediaType.APPLICATION_JSON_VALUE;
MockHttpServletResponse response = new MockHttpServletResponse();
response.setContentType(contentType);
response.setContentLength(originalContentLength);
response.setHeader(PUZZLE, ENIGMA);
response.setIntHeader(NUMBER, 42);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
responseWrapper.setStatus(HttpServletResponse.SC_CREATED);
assertThat(responseWrapper.getStatus()).isEqualTo(HttpServletResponse.SC_CREATED);
assertThat(responseWrapper.getContentSize()).isZero();
assertThat(responseWrapper.getHeaderNames())
.containsExactlyInAnyOrder(PUZZLE, NUMBER, CONTENT_TYPE, CONTENT_LENGTH);
assertHeader(responseWrapper, PUZZLE, ENIGMA);
assertHeader(responseWrapper, NUMBER, MAGIC);
assertHeader(responseWrapper, CONTENT_LENGTH, originalContentLength);
assertContentTypeHeader(responseWrapper, contentType);
FileCopyUtils.copy(responseBody, responseWrapper.getOutputStream());
assertThat(responseWrapper.getContentSize()).isEqualTo(responseLength);
responseWrapper.copyBodyToResponse();
assertThat(responseWrapper.getStatus()).isEqualTo(HttpServletResponse.SC_CREATED);
assertThat(responseWrapper.getContentSize()).isZero();
assertThat(responseWrapper.getHeaderNames())
.containsExactlyInAnyOrder(PUZZLE, NUMBER, CONTENT_TYPE, CONTENT_LENGTH);
assertHeader(responseWrapper, PUZZLE, ENIGMA);
assertHeader(responseWrapper, NUMBER, MAGIC);
assertHeader(responseWrapper, CONTENT_LENGTH, responseLength);
assertContentTypeHeader(responseWrapper, contentType);
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_CREATED);
assertThat(response.getContentLength()).isEqualTo(responseLength);
assertThat(response.getContentAsByteArray()).isEqualTo(responseBody);
assertThat(response.getHeaderNames())
.containsExactlyInAnyOrder(PUZZLE, NUMBER, CONTENT_TYPE, CONTENT_LENGTH);
assertHeader(response, PUZZLE, ENIGMA);
assertHeader(response, NUMBER, MAGIC);
assertHeader(response, CONTENT_LENGTH, responseLength);
assertContentTypeHeader(response, contentType);
}
@ParameterizedTest(name = "[{index}] {0}")
@MethodSource("setContentLengthFunctions")
void copyBodyToResponseWithOverridingContentLength(SetContentLength setContentLength) throws Exception {
byte[] responseBody = "Hello World".getBytes(UTF_8);
int responseLength = responseBody.length;
int originalContentLength = 11;
int overridingContentLength = 22;
MockHttpServletResponse response = new MockHttpServletResponse();
response.setContentLength(originalContentLength);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
responseWrapper.setContentLength(overridingContentLength);
setContentLength.invoke(responseWrapper, overridingContentLength);
assertThat(responseWrapper.getContentSize()).isZero();
assertThat(responseWrapper.getHeaderNames()).containsExactlyInAnyOrder(CONTENT_LENGTH);
assertHeader(response, CONTENT_LENGTH, originalContentLength);
assertHeader(responseWrapper, CONTENT_LENGTH, overridingContentLength);
FileCopyUtils.copy(responseBody, responseWrapper.getOutputStream());
assertThat(responseWrapper.getContentSize()).isEqualTo(responseLength);
responseWrapper.copyBodyToResponse();
assertThat(responseWrapper.getContentSize()).isZero();
assertThat(responseWrapper.getHeaderNames()).containsExactlyInAnyOrder(CONTENT_LENGTH);
assertHeader(response, CONTENT_LENGTH, responseLength);
assertHeader(responseWrapper, CONTENT_LENGTH, responseLength);
assertThat(response.getContentLength()).isEqualTo(responseLength);
assertThat(response.getContentAsByteArray()).isEqualTo(responseBody);
assertThat(response.getHeaderNames()).containsExactlyInAnyOrder(CONTENT_LENGTH);
}
private static Stream<Named<SetContentLength>> setContentLengthFunctions() {
return Stream.of(
named("setContentLength()", HttpServletResponse::setContentLength),
named("setContentLengthLong()", HttpServletResponse::setContentLengthLong),
named("setIntHeader()", (response, contentLength) -> response.setIntHeader(CONTENT_LENGTH, contentLength)),
named("addIntHeader()", (response, contentLength) -> response.addIntHeader(CONTENT_LENGTH, contentLength)),
named("setHeader()", (response, contentLength) -> response.setHeader(CONTENT_LENGTH, "" + contentLength)),
named("addHeader()", (response, contentLength) -> response.addHeader(CONTENT_LENGTH, "" + contentLength))
);
}
@ParameterizedTest(name = "[{index}] {0}")
@MethodSource("setContentTypeFunctions")
void copyBodyToResponseWithOverridingContentType(SetContentType setContentType) throws Exception {
byte[] responseBody = "Hello World".getBytes(UTF_8);
int responseLength = responseBody.length;
String originalContentType = MediaType.TEXT_PLAIN_VALUE;
String overridingContentType = MediaType.APPLICATION_JSON_VALUE;
MockHttpServletResponse response = new MockHttpServletResponse();
response.setContentType(originalContentType);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
assertContentTypeHeader(response, originalContentType);
assertContentTypeHeader(responseWrapper, originalContentType);
setContentType.invoke(responseWrapper, overridingContentType);
assertThat(responseWrapper.getContentSize()).isZero();
assertThat(responseWrapper.getHeaderNames()).containsExactlyInAnyOrder(CONTENT_TYPE);
assertContentTypeHeader(response, overridingContentType);
assertContentTypeHeader(responseWrapper, overridingContentType);
FileCopyUtils.copy(responseBody, responseWrapper.getOutputStream());
assertThat(responseWrapper.getContentSize()).isEqualTo(responseLength);
responseWrapper.copyBodyToResponse();
assertThat(responseWrapper.getContentSize()).isZero();
assertThat(responseWrapper.getHeaderNames()).containsExactlyInAnyOrder(CONTENT_TYPE, CONTENT_LENGTH);
assertHeader(response, CONTENT_LENGTH, responseLength);
assertHeader(responseWrapper, CONTENT_LENGTH, responseLength);
assertContentTypeHeader(response, overridingContentType);
assertContentTypeHeader(responseWrapper, overridingContentType);
assertThat(response.getContentLength()).isEqualTo(responseLength);
assertThat(response.getContentAsByteArray()).isEqualTo(responseBody);
assertThat(response.getHeaderNames()).containsExactlyInAnyOrder(CONTENT_TYPE, CONTENT_LENGTH);
}
private static Stream<Named<SetContentType>> setContentTypeFunctions() {
return Stream.of(
named("setContentType()", HttpServletResponse::setContentType),
named("setHeader()", (response, contentType) -> response.setHeader(CONTENT_TYPE, contentType)),
named("addHeader()", (response, contentType) -> response.addHeader(CONTENT_TYPE, contentType))
);
}
@Test
void copyBodyToResponseWithTransferEncoding() throws Exception {
byte[] responseBody = "6\r\nHello 5\r\nWorld0\r\n\r\n".getBytes(StandardCharsets.UTF_8);
byte[] responseBody = "6\r\nHello 5\r\nWorld0\r\n\r\n".getBytes(UTF_8);
MockHttpServletResponse response = new MockHttpServletResponse();
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
responseWrapper.setStatus(HttpServletResponse.SC_OK);
responseWrapper.setHeader(HttpHeaders.TRANSFER_ENCODING, "chunked");
responseWrapper.setStatus(HttpServletResponse.SC_CREATED);
responseWrapper.setHeader(TRANSFER_ENCODING, "chunked");
FileCopyUtils.copy(responseBody, responseWrapper.getOutputStream());
responseWrapper.copyBodyToResponse();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getHeader(HttpHeaders.TRANSFER_ENCODING)).isEqualTo("chunked");
assertThat(response.getHeader(HttpHeaders.CONTENT_LENGTH)).isNull();
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_CREATED);
assertHeader(response, TRANSFER_ENCODING, "chunked");
assertHeader(response, CONTENT_LENGTH, null);
assertThat(response.getContentAsByteArray()).isEqualTo(responseBody);
}
private void assertHeader(HttpServletResponse response, String header, int value) {
assertHeader(response, header, Integer.toString(value));
}
private void assertHeader(HttpServletResponse response, String header, String value) {
if (value == null) {
assertThat(response.containsHeader(header)).as(header).isFalse();
assertThat(response.getHeader(header)).as(header).isNull();
assertThat(response.getHeaders(header)).as(header).isEmpty();
}
else {
assertThat(response.containsHeader(header)).as(header).isTrue();
assertThat(response.getHeader(header)).as(header).isEqualTo(value);
assertThat(response.getHeaders(header)).as(header).containsExactly(value);
}
}
private void assertContentTypeHeader(HttpServletResponse response, String contentType) {
assertHeader(response, CONTENT_TYPE, contentType);
assertThat(response.getContentType()).as(CONTENT_TYPE).isEqualTo(contentType);
}
@FunctionalInterface
private interface SetContentLength {
void invoke(HttpServletResponse response, int contentLength);
}
@FunctionalInterface
private interface SetContentType {
void invoke(HttpServletResponse response, String contentType);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -17,32 +17,35 @@
package org.springframework.web.filter;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE;
/**
* Tests for {@link ShallowEtagHeaderFilter}.
*
* @author Arjen Poutsma
* @author Brian Clozel
* @author Juergen Hoeller
* @author Sam Brannen
*/
public class ShallowEtagHeaderFilterTests {
class ShallowEtagHeaderFilterTests {
private final ShallowEtagHeaderFilter filter = new ShallowEtagHeaderFilter();
@Test
public void isEligibleForEtag() {
void isEligibleForEtag() {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
MockHttpServletResponse response = new MockHttpServletResponse();
@@ -61,15 +64,15 @@ public class ShallowEtagHeaderFilterTests {
}
@Test
public void filterNoMatch() throws Exception {
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
void filterNoMatch() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
MockHttpServletResponse response = new MockHttpServletResponse();
final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
byte[] responseBody = "Hello World".getBytes(UTF_8);
FilterChain filterChain = (filterRequest, filterResponse) -> {
assertThat(filterRequest).as("Invalid request passed").isEqualTo(request);
((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK);
filterResponse.setContentType(MediaType.TEXT_PLAIN_VALUE);
filterResponse.setContentType(TEXT_PLAIN_VALUE);
FileCopyUtils.copy(responseBody, filterResponse.getOutputStream());
};
filter.doFilter(request, response, filterChain);
@@ -77,21 +80,21 @@ public class ShallowEtagHeaderFilterTests {
assertThat(response.getStatus()).as("Invalid status").isEqualTo(200);
assertThat(response.getHeader("ETag")).as("Invalid ETag").isEqualTo("\"0b10a8db164e0754105b7a99be72e3fe5\"");
assertThat(response.getContentLength()).as("Invalid Content-Length header").isGreaterThan(0);
assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(MediaType.TEXT_PLAIN_VALUE);
assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(TEXT_PLAIN_VALUE);
assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(responseBody);
}
@Test
public void filterNoMatchWeakETag() throws Exception {
void filterNoMatchWeakETag() throws Exception {
this.filter.setWriteWeakETag(true);
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
MockHttpServletResponse response = new MockHttpServletResponse();
final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
byte[] responseBody = "Hello World".getBytes(UTF_8);
FilterChain filterChain = (filterRequest, filterResponse) -> {
assertThat(filterRequest).as("Invalid request passed").isEqualTo(request);
((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK);
filterResponse.setContentType(MediaType.TEXT_PLAIN_VALUE);
filterResponse.setContentType(TEXT_PLAIN_VALUE);
FileCopyUtils.copy(responseBody, filterResponse.getOutputStream());
};
filter.doFilter(request, response, filterChain);
@@ -99,22 +102,22 @@ public class ShallowEtagHeaderFilterTests {
assertThat(response.getStatus()).as("Invalid status").isEqualTo(200);
assertThat(response.getHeader("ETag")).as("Invalid ETag").isEqualTo("W/\"0b10a8db164e0754105b7a99be72e3fe5\"");
assertThat(response.getContentLength()).as("Invalid Content-Length header").isGreaterThan(0);
assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(MediaType.TEXT_PLAIN_VALUE);
assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(TEXT_PLAIN_VALUE);
assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(responseBody);
}
@Test
public void filterMatch() throws Exception {
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
void filterMatch() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
String etag = "\"0b10a8db164e0754105b7a99be72e3fe5\"";
request.addHeader("If-None-Match", etag);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = (filterRequest, filterResponse) -> {
assertThat(filterRequest).as("Invalid request passed").isEqualTo(request);
byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
byte[] responseBody = "Hello World".getBytes(UTF_8);
filterResponse.setContentLength(responseBody.length);
filterResponse.setContentType(MediaType.TEXT_PLAIN_VALUE);
filterResponse.setContentType(TEXT_PLAIN_VALUE);
FileCopyUtils.copy(responseBody, filterResponse.getOutputStream());
};
filter.doFilter(request, response, filterChain);
@@ -122,21 +125,20 @@ public class ShallowEtagHeaderFilterTests {
assertThat(response.getStatus()).as("Invalid status").isEqualTo(304);
assertThat(response.getHeader("ETag")).as("Invalid ETag").isEqualTo("\"0b10a8db164e0754105b7a99be72e3fe5\"");
assertThat(response.containsHeader("Content-Length")).as("Response has Content-Length header").isFalse();
assertThat(response.containsHeader("Content-Type")).as("Response has Content-Type header").isFalse();
byte[] expecteds = new byte[0];
assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(expecteds);
assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(TEXT_PLAIN_VALUE);
assertThat(response.getContentAsByteArray()).as("Invalid content").isEmpty();
}
@Test
public void filterMatchWeakEtag() throws Exception {
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
void filterMatchWeakEtag() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
String etag = "\"0b10a8db164e0754105b7a99be72e3fe5\"";
request.addHeader("If-None-Match", "W/" + etag);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = (filterRequest, filterResponse) -> {
assertThat(filterRequest).as("Invalid request passed").isEqualTo(request);
byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
byte[] responseBody = "Hello World".getBytes(UTF_8);
FileCopyUtils.copy(responseBody, filterResponse.getOutputStream());
filterResponse.setContentLength(responseBody.length);
};
@@ -145,13 +147,12 @@ public class ShallowEtagHeaderFilterTests {
assertThat(response.getStatus()).as("Invalid status").isEqualTo(304);
assertThat(response.getHeader("ETag")).as("Invalid ETag").isEqualTo("\"0b10a8db164e0754105b7a99be72e3fe5\"");
assertThat(response.containsHeader("Content-Length")).as("Response has Content-Length header").isFalse();
byte[] expecteds = new byte[0];
assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(expecteds);
assertThat(response.getContentAsByteArray()).as("Invalid content").isEmpty();
}
@Test
public void filterWriter() throws Exception {
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
void filterWriter() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
String etag = "\"0b10a8db164e0754105b7a99be72e3fe5\"";
request.addHeader("If-None-Match", etag);
MockHttpServletResponse response = new MockHttpServletResponse();
@@ -167,19 +168,20 @@ public class ShallowEtagHeaderFilterTests {
assertThat(response.getStatus()).as("Invalid status").isEqualTo(304);
assertThat(response.getHeader("ETag")).as("Invalid ETag").isEqualTo("\"0b10a8db164e0754105b7a99be72e3fe5\"");
assertThat(response.containsHeader("Content-Length")).as("Response has Content-Length header").isFalse();
byte[] expecteds = new byte[0];
assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(expecteds);
assertThat(response.getContentAsByteArray()).as("Invalid content").isEmpty();
}
@Test // SPR-12960
public void filterWriterWithDisabledCaching() throws Exception {
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
void filterWriterWithDisabledCaching() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
MockHttpServletResponse response = new MockHttpServletResponse();
response.setContentType(TEXT_PLAIN_VALUE);
final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
byte[] responseBody = "Hello World".getBytes(UTF_8);
FilterChain filterChain = (filterRequest, filterResponse) -> {
assertThat(filterRequest).as("Invalid request passed").isEqualTo(request);
((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK);
filterResponse.setContentType(APPLICATION_JSON_VALUE);
FileCopyUtils.copy(responseBody, filterResponse.getOutputStream());
};
@@ -188,15 +190,16 @@ public class ShallowEtagHeaderFilterTests {
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getHeader("ETag")).isNull();
assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(APPLICATION_JSON_VALUE);
assertThat(response.getContentAsByteArray()).isEqualTo(responseBody);
}
@Test
public void filterSendError() throws Exception {
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
void filterSendError() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
MockHttpServletResponse response = new MockHttpServletResponse();
final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
byte[] responseBody = "Hello World".getBytes(UTF_8);
FilterChain filterChain = (filterRequest, filterResponse) -> {
assertThat(filterRequest).as("Invalid request passed").isEqualTo(request);
response.setContentLength(100);
@@ -212,11 +215,11 @@ public class ShallowEtagHeaderFilterTests {
}
@Test
public void filterSendErrorMessage() throws Exception {
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
void filterSendErrorMessage() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
MockHttpServletResponse response = new MockHttpServletResponse();
final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
byte[] responseBody = "Hello World".getBytes(UTF_8);
FilterChain filterChain = (filterRequest, filterResponse) -> {
assertThat(filterRequest).as("Invalid request passed").isEqualTo(request);
response.setContentLength(100);
@@ -233,11 +236,11 @@ public class ShallowEtagHeaderFilterTests {
}
@Test
public void filterSendRedirect() throws Exception {
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
void filterSendRedirect() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
MockHttpServletResponse response = new MockHttpServletResponse();
final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
byte[] responseBody = "Hello World".getBytes(UTF_8);
FilterChain filterChain = (filterRequest, filterResponse) -> {
assertThat(filterRequest).as("Invalid request passed").isEqualTo(request);
response.setContentLength(100);
@@ -254,11 +257,11 @@ public class ShallowEtagHeaderFilterTests {
}
@Test // SPR-13717
public void filterFlushResponse() throws Exception {
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
void filterFlushResponse() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
MockHttpServletResponse response = new MockHttpServletResponse();
final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
byte[] responseBody = "Hello World".getBytes(UTF_8);
FilterChain filterChain = (filterRequest, filterResponse) -> {
assertThat(filterRequest).as("Invalid request passed").isEqualTo(request);
((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 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.
@@ -27,6 +27,7 @@ import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatNoException;
/**
* @author Arjen Poutsma
@@ -35,6 +36,16 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
*/
class UriTemplateTests {
@Test
void emptyPathDoesNotThrowException() {
assertThatNoException().isThrownBy(() -> new UriTemplate(""));
}
@Test
void nullPathThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new UriTemplate(null));
}
@Test
void getVariableNames() {
UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}");
@@ -42,6 +53,13 @@ class UriTemplateTests {
assertThat(variableNames).as("Invalid variable names").isEqualTo(Arrays.asList("hotel", "booking"));
}
@Test
void getVariableNamesFromEmpty() {
UriTemplate template = new UriTemplate("");
List<String> variableNames = template.getVariableNames();
assertThat(variableNames).isEmpty();
}
@Test
void expandVarArgs() {
UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}");
@@ -49,6 +67,13 @@ class UriTemplateTests {
assertThat(result).as("Invalid expanded template").isEqualTo(URI.create("/hotels/1/bookings/42"));
}
@Test
void expandVarArgsFromEmpty() {
UriTemplate template = new UriTemplate("");
URI result = template.expand();
assertThat(result).as("Invalid expanded template").isEqualTo(URI.create(""));
}
@Test // SPR-9712
void expandVarArgsWithArrayValue() {
UriTemplate template = new UriTemplate("/sum?numbers={numbers}");
@@ -124,6 +149,15 @@ class UriTemplateTests {
assertThat(template.matches(null)).as("UriTemplate matches").isFalse();
}
@Test
void matchesAgainstEmpty() {
UriTemplate template = new UriTemplate("");
assertThat(template.matches("/hotels/1/bookings/42")).as("UriTemplate matches").isFalse();
assertThat(template.matches("/hotels/bookings")).as("UriTemplate matches").isFalse();
assertThat(template.matches("")).as("UriTemplate does not match").isTrue();
assertThat(template.matches(null)).as("UriTemplate matches").isFalse();
}
@Test
void matchesCustomRegex() {
UriTemplate template = new UriTemplate("/hotels/{hotel:\\d+}");
@@ -142,6 +176,13 @@ class UriTemplateTests {
assertThat(result).as("Invalid match").isEqualTo(expected);
}
@Test
void matchAgainstEmpty() {
UriTemplate template = new UriTemplate("");
Map<String, String> result = template.match("/hotels/1/bookings/42");
assertThat(result).as("Invalid match").isEmpty();
}
@Test
void matchCustomRegex() {
Map<String, String> expected = new HashMap<>(2);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -292,6 +292,11 @@ public class MockHttpServletResponse implements HttpServletResponse {
doAddHeaderValue(HttpHeaders.CONTENT_LENGTH, contentLength, true);
}
/**
* Get the length of the content body from the HTTP Content-Length header.
* @return the value of the Content-Length header
* @see #setContentLength(int)
*/
public int getContentLength() {
return (int) this.contentLength;
}
@@ -742,7 +747,7 @@ public class MockHttpServletResponse implements HttpServletResponse {
@Override
public void setStatus(int status) {
if (!this.isCommitted()) {
if (!isCommitted()) {
this.status = status;
}
}
@@ -752,6 +757,9 @@ public class MockHttpServletResponse implements HttpServletResponse {
return this.status;
}
/**
* Return the error message used when calling {@link HttpServletResponse#sendError(int, String)}.
*/
@Nullable
public String getErrorMessage() {
return this.errorMessage;
@@ -465,7 +465,7 @@ final class DefaultWebClient implements WebClient {
final AtomicBoolean responseReceived = new AtomicBoolean();
return responseMono
.doOnNext(response -> responseReceived.set(true))
.doOnError(observationContext::setError)
.doOnError(observation::error)
.doFinally(signalType -> {
if (signalType == SignalType.CANCEL && !responseReceived.get()) {
observationContext.setAborted(true);
@@ -110,7 +110,8 @@ class WebClientObservationTests {
StepVerifier.create(client.get().uri("/path").retrieve().bodyToMono(Void.class))
.expectError(IllegalStateException.class)
.verify(Duration.ofSeconds(5));
assertThatHttpObservation().hasLowCardinalityKeyValue("exception", "IllegalStateException")
assertThatHttpObservation().hasError()
.hasLowCardinalityKeyValue("exception", "IllegalStateException")
.hasLowCardinalityKeyValue("status", "CLIENT_ERROR");
}
@@ -180,7 +181,7 @@ class WebClientObservationTests {
StepVerifier.create(responseMono)
.expectError(IllegalStateException.class)
.verify(Duration.ofSeconds(5));
assertThatHttpObservation()
assertThatHttpObservation().hasError()
.hasLowCardinalityKeyValue("exception", "IllegalStateException")
.hasLowCardinalityKeyValue("status", "200");
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -95,6 +95,7 @@ public class HandlerFunctionAdapter implements HandlerAdapter, Ordered {
Object handler) throws Exception {
WebAsyncManager asyncManager = getWebAsyncManager(servletRequest, servletResponse);
servletResponse = getWrappedResponse(asyncManager);
ServerRequest serverRequest = getServerRequest(servletRequest);
ServerResponse serverResponse;
@@ -124,6 +125,22 @@ public class HandlerFunctionAdapter implements HandlerAdapter, Ordered {
return asyncManager;
}
/**
* Obtain response wrapped by
* {@link org.springframework.web.context.request.async.StandardServletAsyncWebRequest}
* to enforce lifecycle rules from Servlet spec (section 2.3.3.4)
* in case of async handling.
*/
private static HttpServletResponse getWrappedResponse(WebAsyncManager asyncManager) {
AsyncWebRequest asyncRequest = asyncManager.getAsyncWebRequest();
Assert.notNull(asyncRequest, "No AsyncWebRequest");
HttpServletResponse servletResponse = asyncRequest.getNativeResponse(HttpServletResponse.class);
Assert.notNull(servletResponse, "No HttpServletResponse");
return servletResponse;
}
private ServerRequest getServerRequest(HttpServletRequest servletRequest) {
ServerRequest serverRequest =
(ServerRequest) servletRequest.getAttribute(RouterFunctions.REQUEST_ATTRIBUTE);
@@ -843,7 +843,21 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ServletWebRequest webRequest = new ServletWebRequest(request, response);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
asyncWebRequest.setTimeout(this.asyncRequestTimeout);
asyncManager.setTaskExecutor(this.taskExecutor);
asyncManager.setAsyncWebRequest(asyncWebRequest);
asyncManager.registerCallableInterceptors(this.callableInterceptors);
asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);
// Obtain wrapped response to enforce lifecycle rule from Servlet spec, section 2.3.3.4
response = asyncWebRequest.getNativeResponse(HttpServletResponse.class);
ServletWebRequest webRequest = (asyncWebRequest instanceof ServletWebRequest ?
(ServletWebRequest) asyncWebRequest : new ServletWebRequest(request, response));
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
@@ -862,15 +876,6 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
asyncWebRequest.setTimeout(this.asyncRequestTimeout);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.setTaskExecutor(this.taskExecutor);
asyncManager.setAsyncWebRequest(asyncWebRequest);
asyncManager.registerCallableInterceptors(this.callableInterceptors);
asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);
if (asyncManager.hasConcurrentResult()) {
Object result = asyncManager.getConcurrentResult();
Object[] resultContext = asyncManager.getConcurrentResultContext();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2024 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.
@@ -32,7 +32,6 @@ import jakarta.servlet.http.PushBuilder;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpMethod;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.WebRequest;
@@ -68,21 +67,6 @@ import org.springframework.web.servlet.support.RequestContextUtils;
*/
public class ServletRequestMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Nullable
private static Class<?> pushBuilder;
static {
try {
pushBuilder = ClassUtils.forName("jakarta.servlet.http.PushBuilder",
ServletRequestMethodArgumentResolver.class.getClassLoader());
}
catch (ClassNotFoundException ex) {
// Servlet 4.0 PushBuilder not found - not supported for injection
pushBuilder = null;
}
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
@@ -90,7 +74,7 @@ public class ServletRequestMethodArgumentResolver implements HandlerMethodArgume
ServletRequest.class.isAssignableFrom(paramType) ||
MultipartRequest.class.isAssignableFrom(paramType) ||
HttpSession.class.isAssignableFrom(paramType) ||
(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
PushBuilder.class.isAssignableFrom(paramType) ||
(Principal.class.isAssignableFrom(paramType) && !parameter.hasParameterAnnotations()) ||
InputStream.class.isAssignableFrom(paramType) ||
Reader.class.isAssignableFrom(paramType) ||
@@ -143,8 +127,13 @@ public class ServletRequestMethodArgumentResolver implements HandlerMethodArgume
}
return session;
}
else if (pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) {
return PushBuilderDelegate.resolvePushBuilder(request, paramType);
else if (PushBuilder.class.isAssignableFrom(paramType)) {
PushBuilder pushBuilder = request.newPushBuilder();
if (pushBuilder != null && !paramType.isInstance(pushBuilder)) {
throw new IllegalStateException(
"Current push builder is not of type [" + paramType.getName() + "]: " + pushBuilder);
}
return pushBuilder;
}
else if (InputStream.class.isAssignableFrom(paramType)) {
InputStream inputStream = request.getInputStream();
@@ -189,22 +178,4 @@ public class ServletRequestMethodArgumentResolver implements HandlerMethodArgume
throw new UnsupportedOperationException("Unknown parameter type: " + paramType.getName());
}
/**
* Inner class to avoid a hard dependency on Servlet API 4.0 at runtime.
*/
private static class PushBuilderDelegate {
@Nullable
public static Object resolvePushBuilder(HttpServletRequest request, Class<?> paramType) {
PushBuilder pushBuilder = request.newPushBuilder();
if (pushBuilder != null && !paramType.isInstance(pushBuilder)) {
throw new IllegalStateException(
"Current push builder is not of type [" + paramType.getName() + "]: " + pushBuilder);
}
return pushBuilder;
}
}
}
@@ -44,6 +44,7 @@ import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
@@ -129,6 +130,10 @@ import org.springframework.web.util.WebUtils;
* <td><div class="block">AsyncRequestTimeoutException</div></td>
* <td><div class="block">503 (SC_SERVICE_UNAVAILABLE)</div></td>
* </tr>
* <tr class="even-row-color">
* <td><div class="block">AsyncRequestNotUsableException</div></td>
* <td><div class="block">Not applicable</div></td>
* </tr>
* </tbody>
* </table>
*
@@ -223,6 +228,10 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
else if (ex instanceof BindException theEx) {
return handleBindException(theEx, request, response, handler);
}
else if (ex instanceof AsyncRequestNotUsableException) {
return handleAsyncRequestNotUsableException(
(AsyncRequestNotUsableException) ex, request, response, handler);
}
}
catch (Exception handlerEx) {
if (logger.isWarnEnabled()) {
@@ -434,6 +443,23 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
return null;
}
/**
* Handle the case of an I/O failure from the ServletOutputStream.
* <p>By default, do nothing since the response is not usable.
* @param ex the {@link AsyncRequestTimeoutException} to be handled
* @param request current HTTP request
* @param response current HTTP response
* @param handler the executed handler, or {@code null} if none chosen
* at the time of the exception (for example, if multipart resolution failed)
* @return an empty ModelAndView indicating the exception was handled
* @since 5.3.33
*/
protected ModelAndView handleAsyncRequestNotUsableException(AsyncRequestNotUsableException ex,
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) {
return new ModelAndView();
}
/**
* Handle an {@link ErrorResponse} exception.
* <p>The default implementation sets status and the headers of the response
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2024 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.
@@ -138,7 +138,7 @@ public abstract class AbstractPdfView extends AbstractView {
* The subclass can either have fixed preferences or retrieve
* them from bean properties defined on the View.
* @return an int containing the bits information against PdfWriter definitions
* @see com.lowagie.text.pdf.PdfWriter#AllowPrinting
* @see com.lowagie.text.pdf.PdfWriter#ALLOW_PRINTING
* @see com.lowagie.text.pdf.PdfWriter#PageLayoutSinglePage
*/
protected int getViewerPreferences() {
@@ -0,0 +1,107 @@
/*
* Copyright 2002-2024 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.web.servlet.function.support;
import java.io.IOException;
import java.util.List;
import jakarta.servlet.AsyncEvent;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
import org.springframework.web.context.request.async.StandardServletAsyncWebRequest;
import org.springframework.web.context.request.async.WebAsyncManager;
import org.springframework.web.context.request.async.WebAsyncUtils;
import org.springframework.web.servlet.function.HandlerFunction;
import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;
import org.springframework.web.testfixture.servlet.MockAsyncContext;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.BDDMockito.doThrow;
import static org.mockito.BDDMockito.mock;
/**
* Unit tests for {@link HandlerFunctionAdapter}.
*
* @author Rossen Stoyanchev
*/
public class HandlerFunctionAdapterTests {
private final MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/");
private final MockHttpServletResponse servletResponse = new MockHttpServletResponse();
private final HandlerFunctionAdapter adapter = new HandlerFunctionAdapter();
@BeforeEach
void setUp() {
this.servletRequest.setAttribute(RouterFunctions.REQUEST_ATTRIBUTE,
ServerRequest.create(this.servletRequest, List.of(new StringHttpMessageConverter())));
}
@Test
void asyncRequestNotUsable() throws Exception {
HandlerFunction<?> handler = request -> ServerResponse.sse(sseBuilder -> {
try {
sseBuilder.data("data 1");
sseBuilder.data("data 2");
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
});
this.servletRequest.setAsyncSupported(true);
HttpServletResponse mockServletResponse = mock(HttpServletResponse.class);
doThrow(new IOException("Broken pipe")).when(mockServletResponse).getOutputStream();
// Use of response should be rejected
assertThatThrownBy(() -> adapter.handle(servletRequest, mockServletResponse, handler))
.hasRootCauseInstanceOf(IOException.class)
.hasRootCauseMessage("Broken pipe");
}
@Test
void asyncRequestNotUsableOnAsyncDispatch() throws Exception {
HandlerFunction<?> handler = request -> ServerResponse.ok().body("body");
// Put AsyncWebRequest in ERROR state
StandardServletAsyncWebRequest asyncRequest = new StandardServletAsyncWebRequest(servletRequest, servletResponse);
asyncRequest.onError(new AsyncEvent(new MockAsyncContext(servletRequest, servletResponse), new Exception()));
// Set it as the current AsyncWebRequest, from the initial REQUEST dispatch
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(servletRequest);
asyncManager.setAsyncWebRequest(asyncRequest);
// Use of response should be rejected
assertThatThrownBy(() -> adapter.handle(servletRequest, servletResponse, handler))
.isInstanceOf(AsyncRequestNotUsableException.class);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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,8 +16,11 @@
package org.springframework.web.servlet.mvc.method.annotation;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -25,6 +28,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import jakarta.servlet.AsyncEvent;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -46,6 +50,10 @@ import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
import org.springframework.web.context.request.async.StandardServletAsyncWebRequest;
import org.springframework.web.context.request.async.WebAsyncManager;
import org.springframework.web.context.request.async.WebAsyncUtils;
import org.springframework.web.context.support.StaticWebApplicationContext;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.annotation.ModelMethodProcessor;
@@ -55,13 +63,15 @@ import org.springframework.web.method.support.InvocableHandlerMethod;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.FlashMap;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.testfixture.servlet.MockAsyncContext;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Unit tests for {@link RequestMappingHandlerAdapter}.
* Tests for {@link RequestMappingHandlerAdapter}.
*
* @author Rossen Stoyanchev
* @author Sam Brannen
@@ -249,9 +259,7 @@ public class RequestMappingHandlerAdapterTests {
assertThat(mav.getModel().get("attr3")).isNull();
}
// SPR-10859
@Test
@Test // gh-15486
public void responseBodyAdvice() throws Exception {
List<HttpMessageConverter<?>> converters = new ArrayList<>();
converters.add(new MappingJackson2HttpMessageConverter());
@@ -271,6 +279,26 @@ public class RequestMappingHandlerAdapterTests {
assertThat(this.response.getContentAsString()).isEqualTo("{\"status\":400,\"message\":\"body\"}");
}
@Test
void asyncRequestNotUsable() throws Exception {
// Put AsyncWebRequest in ERROR state
StandardServletAsyncWebRequest asyncRequest = new StandardServletAsyncWebRequest(this.request, this.response);
asyncRequest.onError(new AsyncEvent(new MockAsyncContext(this.request, this.response), new Exception()));
// Set it as the current AsyncWebRequest, from the initial REQUEST dispatch
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(this.request);
asyncManager.setAsyncWebRequest(asyncRequest);
// AsyncWebRequest created for current dispatch should inherit state
HandlerMethod handlerMethod = handlerMethod(new SimpleController(), "handleOutputStream", OutputStream.class);
this.handlerAdapter.afterPropertiesSet();
// Use of response should be rejected
assertThatThrownBy(() -> this.handlerAdapter.handle(this.request, this.response, handlerMethod))
.isInstanceOf(AsyncRequestNotUsableException.class);
}
private HandlerMethod handlerMethod(Object handler, String methodName, Class<?>... paramTypes) throws Exception {
Method method = handler.getClass().getDeclaredMethod(methodName, paramTypes);
return new InvocableHandlerMethod(handler, method);
@@ -296,14 +324,16 @@ public class RequestMappingHandlerAdapterTests {
}
public ResponseEntity<Map<String, String>> handleWithResponseEntity() {
return new ResponseEntity<>(Collections.singletonMap(
"foo", "bar"), HttpStatus.OK);
return new ResponseEntity<>(Collections.singletonMap("foo", "bar"), HttpStatus.OK);
}
public ResponseEntity<String> handleBadRequest() {
return new ResponseEntity<>("body", HttpStatus.BAD_REQUEST);
}
public void handleOutputStream(OutputStream outputStream) throws IOException {
outputStream.write("body".getBytes(StandardCharsets.UTF_8));
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 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.
@@ -97,6 +97,10 @@ public class ResponseEntityExceptionHandlerTests {
.filter(method -> method.getName().startsWith("handle") && (method.getParameterCount() == 4))
.filter(method -> !method.getName().equals("handleErrorResponse"))
.map(method -> method.getParameterTypes()[0])
.filter(exceptionType -> {
String name = exceptionType.getSimpleName();
return !name.equals("AsyncRequestNotUsableException");
})
.forEach(exceptionType -> assertThat(annotation.value())
.as("@ExceptionHandler is missing declaration for " + exceptionType.getName())
.contains((Class<Exception>) exceptionType));