mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 14fa75c98a | |||
| f2fd2f1226 | |||
| 072ebb6ffc | |||
| 08e7f7efa4 | |||
| b1fafbf7e1 | |||
| a3647a8c5e | |||
| 5c8d9cd0b2 | |||
| 40d5196243 | |||
| ec2c9b5d0e | |||
| 36539bdaa9 | |||
| 67ba7dd1da | |||
| 7b3fcf2647 | |||
| fd76c33589 | |||
| 1a7a6f421f | |||
| 9ac1feceb5 | |||
| 7029042e44 | |||
| 1a5661d426 | |||
| b208c63414 | |||
| 814c003b43 | |||
| 5c34e1d11a | |||
| d57775bbb2 | |||
| b598ad3f33 | |||
| b44ef70bc3 | |||
| d1b3107398 | |||
| 629c560316 | |||
| 7bf07ef393 | |||
| ca602ef874 | |||
| e9bf5f5569 | |||
| 9a6f636e17 | |||
| 7686f5467e | |||
| 429c477f6a | |||
| 5187281b50 | |||
| 24a4487050 | |||
| 8fba4a448a | |||
| 8f286de773 |
@@ -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]
|
||||
======
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+4
-3
@@ -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.
|
||||
|
||||
+4
-7
@@ -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,
|
||||
|
||||
+6
-6
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+12
-13
@@ -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);
|
||||
|
||||
|
||||
+3
-5
@@ -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) {
|
||||
|
||||
+2
-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.
|
||||
@@ -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);
|
||||
|
||||
+2
-2
@@ -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
|
||||
*/
|
||||
|
||||
+2
-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.
|
||||
@@ -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
|
||||
|
||||
+24
-28
@@ -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);
|
||||
|
||||
Vendored
+2
-2
@@ -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
|
||||
*/
|
||||
|
||||
spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java
Vendored
+7
-7
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+12
-15
@@ -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());
|
||||
|
||||
|
||||
+22
-19
@@ -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 + "]");
|
||||
}
|
||||
|
||||
+3
-3
@@ -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;
|
||||
|
||||
+2
-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.
|
||||
@@ -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())
|
||||
|
||||
+97
-68
@@ -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() {
|
||||
|
||||
+13
-13
@@ -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");
|
||||
|
||||
+1
-2
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+6
-7
@@ -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 {
|
||||
|
||||
+8
-8
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+5
-5
@@ -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
|
||||
|
||||
+2
-2
@@ -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.
|
||||
*/
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
+2
-4
@@ -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());
|
||||
}
|
||||
|
||||
+8
-5
@@ -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 ?
|
||||
|
||||
+8
-18
@@ -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
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
Args = -H:ServiceLoaderFeatureExcludeServices=org.hibernate.bytecode.spi.BytecodeProvider
|
||||
+5
-3
@@ -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;
|
||||
|
||||
|
||||
+10
-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;
|
||||
|
||||
+10
-9
@@ -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
|
||||
|
||||
+3
-9
@@ -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);
|
||||
|
||||
+1
@@ -52,6 +52,7 @@ class MockMvcHtmlUnitDriverBuilderTests {
|
||||
|
||||
private HtmlUnitDriver driver;
|
||||
|
||||
|
||||
MockMvcHtmlUnitDriverBuilderTests(WebApplicationContext wac) {
|
||||
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
|
||||
}
|
||||
|
||||
+1
@@ -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(""));
|
||||
|
||||
+4
-3
@@ -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() {
|
||||
|
||||
+8
-22
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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
|
||||
*/
|
||||
|
||||
+5
-5
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-1
@@ -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!
|
||||
|
||||
+2
-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.
|
||||
@@ -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();
|
||||
|
||||
+44
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+550
-13
@@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+117
-61
@@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
-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.
|
||||
@@ -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;
|
||||
|
||||
|
||||
|
||||
+5
-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.
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+22
-57
@@ -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();
|
||||
|
||||
|
||||
+357
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+3
-4
@@ -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
|
||||
|
||||
+213
-13
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+49
-46
@@ -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);
|
||||
|
||||
+10
-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;
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+3
-2
@@ -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");
|
||||
}
|
||||
|
||||
+18
-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,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);
|
||||
|
||||
+15
-10
@@ -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();
|
||||
|
||||
+9
-38
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+26
@@ -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
|
||||
|
||||
+2
-2
@@ -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() {
|
||||
|
||||
+107
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+37
-7
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+5
-1
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user