mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e354390837 | |||
| c53132ecdf | |||
| 03c3ec1577 | |||
| c88bfc54c9 | |||
| a96558c965 | |||
| 1612b7c5db | |||
| 01acb80501 | |||
| f4438ce9e3 | |||
| 5033b9d3c5 | |||
| d733023a29 | |||
| df67c1cf2d | |||
| 05814f7a42 | |||
| ba2bb08589 | |||
| 141df5291d | |||
| dcfe33f427 | |||
| 3a61460f91 | |||
| ee284f2ee6 | |||
| 2591cab561 | |||
| 8456cd1e74 | |||
| 7699b4af9c | |||
| 6c5de48059 | |||
| 9f678ce698 | |||
| 467a484df6 | |||
| 83efe8cff4 | |||
| 2c83144946 | |||
| c165dd5e0e | |||
| 449b85f446 | |||
| c2a66e723f | |||
| 2ee34a5632 | |||
| 1bc82d241a | |||
| 80e7ee321e | |||
| ecd3dd8883 | |||
| 332953c9a4 | |||
| 1cdd56bf02 | |||
| 3041071269 | |||
| 2da821389c | |||
| d484e4f3ff | |||
| 74dc61b8c4 | |||
| 717358b56b | |||
| 836634c47f | |||
| a6f6ecfe6c | |||
| e3da26ebbd | |||
| e1c008f5a3 | |||
| cb849a7071 | |||
| 3e37279db6 | |||
| b3264ec2a8 | |||
| df860fd3cd | |||
| 636523a2f5 | |||
| a19b51b7e0 | |||
| fbdece6759 | |||
| 64d42fefda | |||
| 447cfa18e9 | |||
| ec3d9d6253 | |||
| 5cd2cb38e1 | |||
| e9fb5eb38a | |||
| 5a858915ea | |||
| d85a020e4e | |||
| 0cc79ba366 | |||
| 1e29911292 | |||
| 931686a5ee | |||
| bf715ac23e | |||
| b213344d25 | |||
| 0a48984fab | |||
| cbdd107799 |
@@ -21,7 +21,7 @@ jobs:
|
||||
toolchain: false
|
||||
- version: 21
|
||||
toolchain: true
|
||||
- version: 24
|
||||
- version: 25
|
||||
toolchain: true
|
||||
exclude:
|
||||
- os:
|
||||
|
||||
+2
-2
@@ -86,7 +86,7 @@ configure([rootProject] + javaProjects) { project ->
|
||||
ext.javadocLinks = [
|
||||
"https://docs.oracle.com/en/java/javase/17/docs/api/",
|
||||
"https://jakarta.ee/specifications/platform/9/apidocs/",
|
||||
"https://docs.jboss.org/hibernate/orm/5.6/javadocs/",
|
||||
"https://docs.hibernate.org/orm/5.6/javadocs/",
|
||||
"https://www.quartz-scheduler.org/api/2.3.0/",
|
||||
"https://fasterxml.github.io/jackson-core/javadoc/2.14/",
|
||||
"https://fasterxml.github.io/jackson-databind/javadoc/2.14/",
|
||||
@@ -97,7 +97,7 @@ configure([rootProject] + javaProjects) { project ->
|
||||
// TODO Uncomment link to JUnit 5 docs once we execute Gradle with Java 18+.
|
||||
// See https://github.com/spring-projects/spring-framework/issues/27497
|
||||
//
|
||||
// "https://junit.org/junit5/docs/5.13.4/api/",
|
||||
// "https://junit.org/junit5/docs/5.14.0/api/",
|
||||
"https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/",
|
||||
//"https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/",
|
||||
"https://r2dbc.io/spec/1.0.0.RELEASE/api/",
|
||||
|
||||
+14
-18
@@ -85,11 +85,11 @@ element. The following example shows how to use it:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
<bean id="theTargetBean" class="..."/>
|
||||
<bean id="collaborator" class="..." />
|
||||
|
||||
<bean id="theClientBean" class="...">
|
||||
<bean id="client" class="...">
|
||||
<property name="targetName">
|
||||
<idref bean="theTargetBean"/>
|
||||
<idref bean="collaborator" />
|
||||
</property>
|
||||
</bean>
|
||||
----
|
||||
@@ -99,28 +99,24 @@ following snippet:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
<bean id="theTargetBean" class="..." />
|
||||
<bean id="collaborator" class="..." />
|
||||
|
||||
<bean id="theClientBean" class="...">
|
||||
<property name="targetName" ref="theTargetBean"/>
|
||||
<bean id="client" class="...">
|
||||
<property name="targetName" value="collaborator" />
|
||||
</bean>
|
||||
----
|
||||
|
||||
The first form is preferable to the second, because using the `idref` tag lets the
|
||||
container validate at deployment time that the referenced, named bean actually
|
||||
exists. In the second variation, no validation is performed on the value that is passed
|
||||
to the `targetName` property of the `client` bean. Typos are only discovered (with most
|
||||
container validate at deployment time that the referenced, named bean actually exists. In
|
||||
the second variation, no validation is performed on the value that is passed to the
|
||||
`targetName` property of the `client` bean. Typos are therefore only discovered (with most
|
||||
likely fatal results) when the `client` bean is actually instantiated. If the `client`
|
||||
bean is a xref:core/beans/factory-scopes.adoc[prototype] bean, this typo and the resulting exception
|
||||
may only be discovered long after the container is deployed.
|
||||
bean is a xref:core/beans/factory-scopes.adoc[prototype] bean, this typo and the resulting
|
||||
exception may only be discovered long after the container is deployed.
|
||||
|
||||
NOTE: The `local` attribute on the `idref` element is no longer supported in the 4.0 beans
|
||||
XSD, since it does not provide value over a regular `bean` reference any more. Change
|
||||
your existing `idref local` references to `idref bean` when upgrading to the 4.0 schema.
|
||||
|
||||
A common place (at least in versions earlier than Spring 2.0) where the `<idref/>` element
|
||||
brings value is in the configuration of xref:core/aop-api/pfb.adoc#aop-pfb-1[AOP interceptors] in a
|
||||
`ProxyFactoryBean` bean definition. Using `<idref/>` elements when you specify the
|
||||
NOTE: A common place (at least in versions earlier than Spring 2.0) where the `<idref/>`
|
||||
element brings value is in the configuration of xref:core/aop-api/pfb.adoc#aop-pfb-1[AOP interceptors]
|
||||
in a `ProxyFactoryBean` bean definition. Using `<idref/>` elements when you specify the
|
||||
interceptor names prevents you from misspelling an interceptor ID.
|
||||
|
||||
|
||||
|
||||
@@ -310,7 +310,7 @@ Java::
|
||||
Flux<String> source = ... ;
|
||||
Mono<Void> output = session.send(source.map(session::textMessage)); <2>
|
||||
|
||||
return Mono.zip(input, output).then(); <3>
|
||||
return input.and(output); <3>
|
||||
}
|
||||
}
|
||||
----
|
||||
@@ -338,7 +338,7 @@ Kotlin::
|
||||
val source: Flux<String> = ...
|
||||
val output = session.send(source.map(session::textMessage)) // <2>
|
||||
|
||||
return Mono.zip(input, output).then() // <3>
|
||||
return input.and(output) // <3>
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
@@ -112,10 +112,11 @@ You can map requests by using glob patterns and wildcards:
|
||||
| `+{name}+`
|
||||
| Matches a path segment and captures it as a variable named "name"
|
||||
| `+"/projects/{project}/versions"+` matches `+"/projects/spring/versions"+` and captures `+project=spring+`
|
||||
`+"/projects/{project}/versions"+` does not match `+"/projects/spring/framework/versions"+` as it captures a single path segment.
|
||||
|
||||
| `+{name:[a-z]+}+`
|
||||
| Matches the regexp `+"[a-z]+"+` as a path variable named "name"
|
||||
| `+"/projects/{project:[a-z]+}/versions"+` matches `+"/projects/spring/versions"+` but not `+"/projects/spring1/versions"+`
|
||||
| `{name:[a-z]+}`
|
||||
| Matches the regexp `[a-z]+` as a path variable named "name"
|
||||
| `/projects/{project:[a-z]+}/versions` matches `/projects/spring/versions` but not `/projects/spring1/versions`
|
||||
|
||||
| `+{*path}+`
|
||||
| Matches zero or more path segments until the end of the path and captures it as a variable named "path"
|
||||
|
||||
@@ -8,30 +8,30 @@ javaPlatform {
|
||||
|
||||
dependencies {
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.18.4.1"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.14.11"))
|
||||
api(platform("io.netty:netty-bom:4.1.127.Final"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.14.12"))
|
||||
api(platform("io.netty:netty-bom:4.1.128.Final"))
|
||||
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
|
||||
api(platform("io.projectreactor:reactor-bom:2024.0.10"))
|
||||
api(platform("io.projectreactor:reactor-bom:2024.0.11"))
|
||||
api(platform("io.rsocket:rsocket-bom:1.1.5"))
|
||||
api(platform("org.apache.groovy:groovy-bom:4.0.28"))
|
||||
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
|
||||
api(platform("org.assertj:assertj-bom:3.27.3"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.0.26"))
|
||||
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.26"))
|
||||
api(platform("org.assertj:assertj-bom:3.27.6"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.0.28"))
|
||||
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.28"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3"))
|
||||
api(platform("org.junit:junit-bom:5.13.4"))
|
||||
api(platform("org.mockito:mockito-bom:5.19.0"))
|
||||
api(platform("org.junit:junit-bom:5.14.0"))
|
||||
api(platform("org.mockito:mockito-bom:5.20.0"))
|
||||
|
||||
constraints {
|
||||
api("com.fasterxml:aalto-xml:1.3.3")
|
||||
api("com.fasterxml:aalto-xml:1.3.4")
|
||||
api("com.fasterxml.woodstox:woodstox-core:6.7.0")
|
||||
api("com.github.ben-manes.caffeine:caffeine:3.2.2")
|
||||
api("com.github.librepdf:openpdf:1.3.43")
|
||||
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.13.1")
|
||||
api("com.google.protobuf:protobuf-java-util:4.32.0")
|
||||
api("com.google.code.gson:gson:2.13.2")
|
||||
api("com.google.protobuf:protobuf-java-util:4.32.1")
|
||||
api("com.h2database:h2:2.3.232")
|
||||
api("com.jayway.jsonpath:json-path:2.9.0")
|
||||
api("com.oracle.database.jdbc:ojdbc11:21.9.0.0")
|
||||
@@ -53,11 +53,11 @@ dependencies {
|
||||
api("io.r2dbc:r2dbc-h2:1.0.0.RELEASE")
|
||||
api("io.r2dbc:r2dbc-spi-test:1.0.0.RELEASE")
|
||||
api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE")
|
||||
api("io.reactivex.rxjava3:rxjava:3.1.11")
|
||||
api("io.reactivex.rxjava3:rxjava:3.1.12")
|
||||
api("io.smallrye.reactive:mutiny:1.10.0")
|
||||
api("io.undertow:undertow-core:2.3.19.Final")
|
||||
api("io.undertow:undertow-servlet:2.3.19.Final")
|
||||
api("io.undertow:undertow-websockets-jsr:2.3.19.Final")
|
||||
api("io.undertow:undertow-core:2.3.20.Final")
|
||||
api("io.undertow:undertow-servlet:2.3.20.Final")
|
||||
api("io.undertow:undertow-websockets-jsr:2.3.20.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")
|
||||
@@ -90,11 +90,11 @@ dependencies {
|
||||
api("junit:junit:4.13.2")
|
||||
api("net.sf.jopt-simple:jopt-simple:5.0.4")
|
||||
api("org.apache-extras.beanshell:bsh:2.0b6")
|
||||
api("org.apache.activemq:activemq-broker:5.17.6")
|
||||
api("org.apache.activemq:activemq-kahadb-store:5.17.6")
|
||||
api("org.apache.activemq:activemq-stomp:5.17.6")
|
||||
api("org.apache.activemq:artemis-jakarta-client:2.31.2")
|
||||
api("org.apache.activemq:artemis-junit-5:2.31.2")
|
||||
api("org.apache.activemq:activemq-broker:5.17.7")
|
||||
api("org.apache.activemq:activemq-kahadb-store:5.17.7")
|
||||
api("org.apache.activemq:activemq-stomp:5.17.7")
|
||||
api("org.apache.activemq:artemis-jakarta-client:2.42.0")
|
||||
api("org.apache.activemq:artemis-junit-5:2.42.0")
|
||||
api("org.apache.commons:commons-pool2:2.9.0")
|
||||
api("org.apache.derby:derby:10.16.1.1")
|
||||
api("org.apache.derby:derbyclient:10.16.1.1")
|
||||
@@ -113,9 +113,9 @@ dependencies {
|
||||
api("org.bouncycastle:bcpkix-jdk18on:1.72")
|
||||
api("org.codehaus.jettison:jettison:1.5.4")
|
||||
api("org.crac:crac:1.4.0")
|
||||
api("org.dom4j:dom4j:2.1.4")
|
||||
api("org.easymock:easymock:5.5.0")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.11")
|
||||
api("org.dom4j:dom4j:2.2.0")
|
||||
api("org.easymock:easymock:5.6.0")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.12")
|
||||
api("org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.4")
|
||||
api("org.eclipse:yasson:2.0.4")
|
||||
api("org.ehcache:ehcache:3.10.8")
|
||||
@@ -129,7 +129,7 @@ dependencies {
|
||||
api("org.hibernate:hibernate-core-jakarta:5.6.15.Final")
|
||||
api("org.hibernate:hibernate-validator:7.0.5.Final")
|
||||
api("org.hsqldb:hsqldb:2.7.4")
|
||||
api("org.htmlunit:htmlunit:4.16.0")
|
||||
api("org.htmlunit:htmlunit:4.17.0")
|
||||
api("org.javamoney:moneta:1.4.4")
|
||||
api("org.jruby:jruby:9.4.13.0")
|
||||
api("org.junit.support:testng-engine:1.0.5")
|
||||
@@ -137,16 +137,16 @@ dependencies {
|
||||
api("org.ogce:xpp3:1.1.6")
|
||||
api("org.python:jython-standalone:2.7.4")
|
||||
api("org.quartz-scheduler:quartz:2.3.2")
|
||||
api("org.seleniumhq.selenium:htmlunit3-driver:4.35.0")
|
||||
api("org.seleniumhq.selenium:selenium-java:4.35.0")
|
||||
api("org.seleniumhq.selenium:htmlunit3-driver:4.36.1")
|
||||
api("org.seleniumhq.selenium:selenium-java:4.36.0")
|
||||
api("org.skyscreamer:jsonassert:1.5.3")
|
||||
api("org.slf4j:slf4j-api:2.0.17")
|
||||
api("org.testng:testng:7.11.0")
|
||||
api("org.webjars:underscorejs:1.8.3")
|
||||
api("org.webjars:webjars-locator-core:0.59")
|
||||
api("org.webjars:webjars-locator-lite:1.1.0")
|
||||
api("org.xmlunit:xmlunit-assertj:2.10.3")
|
||||
api("org.xmlunit:xmlunit-matchers:2.10.3")
|
||||
api("org.yaml:snakeyaml:2.4")
|
||||
api("org.xmlunit:xmlunit-assertj:2.10.4")
|
||||
api("org.xmlunit:xmlunit-matchers:2.10.4")
|
||||
api("org.yaml:snakeyaml:2.5")
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
version=6.2.11
|
||||
version=6.2.12
|
||||
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
|
||||
@@ -81,13 +81,19 @@ public abstract class ScopedProxyUtils {
|
||||
// Copy autowire settings from original bean definition.
|
||||
proxyDefinition.setAutowireCandidate(targetDefinition.isAutowireCandidate());
|
||||
proxyDefinition.setPrimary(targetDefinition.isPrimary());
|
||||
proxyDefinition.setFallback(targetDefinition.isFallback());
|
||||
if (targetDefinition instanceof AbstractBeanDefinition abd) {
|
||||
proxyDefinition.setDefaultCandidate(abd.isDefaultCandidate());
|
||||
proxyDefinition.copyQualifiersFrom(abd);
|
||||
}
|
||||
|
||||
// The target bean should be ignored in favor of the scoped proxy.
|
||||
targetDefinition.setAutowireCandidate(false);
|
||||
targetDefinition.setPrimary(false);
|
||||
targetDefinition.setFallback(false);
|
||||
if (targetDefinition instanceof AbstractBeanDefinition abd) {
|
||||
abd.setDefaultCandidate(false);
|
||||
}
|
||||
|
||||
// Register the target bean as separate bean in the factory.
|
||||
registry.registerBeanDefinition(targetBeanName, targetDefinition);
|
||||
|
||||
+4
-3
@@ -37,7 +37,6 @@ import static org.mockito.Mockito.verify;
|
||||
* Tests for {@link AsyncExecutionInterceptor}.
|
||||
*
|
||||
* @author Bao Ngo
|
||||
* @since 7.0
|
||||
*/
|
||||
class AsyncExecutionInterceptorTests {
|
||||
|
||||
@@ -62,11 +61,13 @@ class AsyncExecutionInterceptorTests {
|
||||
O run();
|
||||
}
|
||||
|
||||
|
||||
static class FutureRunner implements GenericRunner<Future<Void>> {
|
||||
|
||||
@Override
|
||||
public Future<Void> run() {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
});
|
||||
return CompletableFuture.runAsync(() -> {});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,6 +18,15 @@ package org.springframework.aop.scope;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.BeanDefinitionHolder;
|
||||
import org.springframework.beans.factory.support.AbstractBeanDefinition;
|
||||
import org.springframework.beans.factory.support.AutowireCandidateQualifier;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.GenericBeanDefinition;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.beans.factory.support.SimpleBeanDefinitionRegistry;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
@@ -25,6 +34,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
||||
* Tests for {@link ScopedProxyUtils}.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @author Juergen Hoeller
|
||||
* @since 5.1.10
|
||||
*/
|
||||
class ScopedProxyUtilsTests {
|
||||
@@ -53,15 +63,79 @@ class ScopedProxyUtilsTests {
|
||||
@Test
|
||||
void getOriginalBeanNameForNullTargetBean() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ScopedProxyUtils.getOriginalBeanName(null))
|
||||
.withMessage("bean name 'null' does not refer to the target of a scoped proxy");
|
||||
.isThrownBy(() -> ScopedProxyUtils.getOriginalBeanName(null))
|
||||
.withMessage("bean name 'null' does not refer to the target of a scoped proxy");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOriginalBeanNameForNonScopedTarget() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ScopedProxyUtils.getOriginalBeanName("myBean"))
|
||||
.withMessage("bean name 'myBean' does not refer to the target of a scoped proxy");
|
||||
.isThrownBy(() -> ScopedProxyUtils.getOriginalBeanName("myBean"))
|
||||
.withMessage("bean name 'myBean' does not refer to the target of a scoped proxy");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createScopedProxyTargetAppliesAutowireSettingsToProxyBeanDefinition() {
|
||||
AbstractBeanDefinition targetDefinition = new GenericBeanDefinition();
|
||||
// Opposite of defaults
|
||||
targetDefinition.setAutowireCandidate(false);
|
||||
targetDefinition.setDefaultCandidate(false);
|
||||
targetDefinition.setPrimary(true);
|
||||
targetDefinition.setFallback(true);
|
||||
|
||||
BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry();
|
||||
BeanDefinitionHolder proxyHolder = ScopedProxyUtils.createScopedProxy(
|
||||
new BeanDefinitionHolder(targetDefinition, "myBean"), registry, false);
|
||||
AbstractBeanDefinition proxyBeanDefinition = (AbstractBeanDefinition) proxyHolder.getBeanDefinition();
|
||||
|
||||
assertThat(proxyBeanDefinition.isAutowireCandidate()).isFalse();
|
||||
assertThat(proxyBeanDefinition.isDefaultCandidate()).isFalse();
|
||||
assertThat(proxyBeanDefinition.isPrimary()).isTrue();
|
||||
assertThat(proxyBeanDefinition.isFallback()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createScopedProxyTargetAppliesBeanAttributesToProxyBeanDefinition() {
|
||||
GenericBeanDefinition targetDefinition = new GenericBeanDefinition();
|
||||
// Opposite of defaults
|
||||
targetDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
targetDefinition.setSource("theSource");
|
||||
targetDefinition.addQualifier(new AutowireCandidateQualifier("myQualifier"));
|
||||
|
||||
BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry();
|
||||
BeanDefinitionHolder proxyHolder = ScopedProxyUtils.createScopedProxy(
|
||||
new BeanDefinitionHolder(targetDefinition, "myBean"), registry, false);
|
||||
BeanDefinition proxyBeanDefinition = proxyHolder.getBeanDefinition();
|
||||
|
||||
assertThat(proxyBeanDefinition.getRole()).isEqualTo(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
assertThat(proxyBeanDefinition).isInstanceOf(RootBeanDefinition.class);
|
||||
assertThat(proxyBeanDefinition.getPropertyValues()).hasSize(2);
|
||||
assertThat(proxyBeanDefinition.getPropertyValues().get("proxyTargetClass")).isEqualTo(false);
|
||||
assertThat(proxyBeanDefinition.getPropertyValues().get("targetBeanName")).isEqualTo(
|
||||
ScopedProxyUtils.getTargetBeanName("myBean"));
|
||||
|
||||
RootBeanDefinition rootBeanDefinition = (RootBeanDefinition) proxyBeanDefinition;
|
||||
assertThat(rootBeanDefinition.getQualifiers()).hasSize(1);
|
||||
assertThat(rootBeanDefinition.hasQualifier("myQualifier")).isTrue();
|
||||
assertThat(rootBeanDefinition.getSource()).isEqualTo("theSource");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createScopedProxyTargetCleansAutowireSettingsInTargetDefinition() {
|
||||
AbstractBeanDefinition targetDefinition = new GenericBeanDefinition();
|
||||
targetDefinition.setAutowireCandidate(true);
|
||||
targetDefinition.setDefaultCandidate(true);
|
||||
targetDefinition.setPrimary(true);
|
||||
targetDefinition.setFallback(true);
|
||||
|
||||
BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry();
|
||||
ScopedProxyUtils.createScopedProxy(
|
||||
new BeanDefinitionHolder(targetDefinition, "myBean"), registry, false);
|
||||
|
||||
assertThat(targetDefinition.isAutowireCandidate()).isFalse();
|
||||
assertThat(targetDefinition.isDefaultCandidate()).isFalse();
|
||||
assertThat(targetDefinition.isPrimary()).isFalse();
|
||||
assertThat(targetDefinition.isFallback()).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+28
@@ -19,6 +19,7 @@ package org.springframework.aop.framework
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.LocalDateTime
|
||||
|
||||
/**
|
||||
* Tests for Kotlin support in [CglibAopProxy].
|
||||
@@ -48,6 +49,13 @@ class CglibAopProxyKotlinTests {
|
||||
assertThatThrownBy { proxy.checkedException() }.isInstanceOf(CheckedException::class.java)
|
||||
}
|
||||
|
||||
@Test // gh-35487
|
||||
fun jvmDefault() {
|
||||
val proxyFactory = ProxyFactory()
|
||||
proxyFactory.setTarget(AddressRepo())
|
||||
proxyFactory.proxy
|
||||
}
|
||||
|
||||
|
||||
open class MyKotlinBean {
|
||||
|
||||
@@ -63,4 +71,24 @@ class CglibAopProxyKotlinTests {
|
||||
}
|
||||
|
||||
class CheckedException() : Exception()
|
||||
|
||||
open class AddressRepo(): CrudRepo<Address, Int>
|
||||
|
||||
interface CrudRepo<E : Any, ID : Any> {
|
||||
fun save(e: E): E {
|
||||
return e
|
||||
}
|
||||
fun delete(id: ID): Long {
|
||||
return 0L
|
||||
}
|
||||
}
|
||||
|
||||
data class Address(
|
||||
val id: Int = 0,
|
||||
val street: String,
|
||||
val version: Int = 0,
|
||||
val createdAt: LocalDateTime? = null,
|
||||
val updatedAt: LocalDateTime? = null,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
+3
@@ -294,9 +294,12 @@ public class InstanceSupplierCodeGenerator {
|
||||
|
||||
this.generationContext.getRuntimeHints().reflection().registerMethod(factoryMethod, ExecutableMode.INVOKE);
|
||||
GeneratedMethod getInstanceMethod = generateGetInstanceSupplierMethod(method -> {
|
||||
CodeWarnings codeWarnings = new CodeWarnings();
|
||||
Class<?> suppliedType = ClassUtils.resolvePrimitiveIfNecessary(factoryMethod.getReturnType());
|
||||
codeWarnings.detectDeprecation(suppliedType, factoryMethod);
|
||||
method.addJavadoc("Get the bean instance supplier for '$L'.", beanName);
|
||||
method.addModifiers(PRIVATE_STATIC);
|
||||
codeWarnings.suppress(method);
|
||||
method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, suppliedType));
|
||||
method.addStatement(generateInstanceSupplierForFactoryMethod(
|
||||
factoryMethod, suppliedType, targetClass, factoryMethod.getName()));
|
||||
|
||||
+13
@@ -152,6 +152,18 @@ public interface ConfigurableListableBeanFactory
|
||||
*/
|
||||
boolean isConfigurationFrozen();
|
||||
|
||||
/**
|
||||
* Mark current thread as main bootstrap thread for singleton instantiation,
|
||||
* with lenient bootstrap locking applying for background threads.
|
||||
* <p>Any such marker is to be removed at the end of the managed bootstrap in
|
||||
* {@link #preInstantiateSingletons()}.
|
||||
* @since 6.2.12
|
||||
* @see #setBootstrapExecutor
|
||||
* @see #preInstantiateSingletons()
|
||||
*/
|
||||
default void prepareSingletonBootstrap() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that all non-lazy-init singletons are instantiated, also considering
|
||||
* {@link org.springframework.beans.factory.FactoryBean FactoryBeans}.
|
||||
@@ -159,6 +171,7 @@ public interface ConfigurableListableBeanFactory
|
||||
* @throws BeansException if one of the singleton beans could not be created.
|
||||
* Note: This may have left the factory with some beans already initialized!
|
||||
* Call {@link #destroySingletons()} for full cleanup in this case.
|
||||
* @see #prepareSingletonBootstrap()
|
||||
* @see #destroySingletons()
|
||||
*/
|
||||
void preInstantiateSingletons() throws BeansException;
|
||||
|
||||
+14
-3
@@ -1102,6 +1102,11 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareSingletonBootstrap() {
|
||||
this.mainThreadPrefix = getThreadNamePrefix();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preInstantiateSingletons() throws BeansException {
|
||||
if (logger.isTraceEnabled()) {
|
||||
@@ -1114,7 +1119,9 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
|
||||
// Trigger initialization of all non-lazy singleton beans...
|
||||
this.preInstantiationThread.set(PreInstantiation.MAIN);
|
||||
this.mainThreadPrefix = getThreadNamePrefix();
|
||||
if (this.mainThreadPrefix == null) {
|
||||
this.mainThreadPrefix = getThreadNamePrefix();
|
||||
}
|
||||
try {
|
||||
List<CompletableFuture<?>> futures = new ArrayList<>();
|
||||
for (String beanName : beanNames) {
|
||||
@@ -1474,7 +1481,10 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
@Override
|
||||
public void registerSingleton(String beanName, Object singletonObject) throws IllegalStateException {
|
||||
super.registerSingleton(beanName, singletonObject);
|
||||
|
||||
updateManualSingletonNames(set -> set.add(beanName), set -> !this.beanDefinitionMap.containsKey(beanName));
|
||||
this.allBeanNamesByType.remove(Object.class);
|
||||
this.singletonBeanNamesByType.remove(Object.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -2098,8 +2108,9 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
boolean candidateLocal = containsBeanDefinition(candidateBeanName);
|
||||
boolean primaryLocal = containsBeanDefinition(primaryBeanName);
|
||||
if (candidateLocal == primaryLocal) {
|
||||
throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(),
|
||||
"more than one 'primary' bean found among candidates: " + candidates.keySet());
|
||||
String message = "more than one 'primary' bean found among candidates: " + candidates.keySet();
|
||||
logger.trace(message);
|
||||
throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(), message);
|
||||
}
|
||||
else if (candidateLocal) {
|
||||
primaryBeanName = candidateBeanName;
|
||||
|
||||
+36
-31
@@ -128,43 +128,48 @@ public abstract class FactoryBeanRegistrySupport extends DefaultSingletonBeanReg
|
||||
locked = (lockFlag && this.singletonLock.tryLock());
|
||||
}
|
||||
try {
|
||||
Object object = this.factoryBeanObjectCache.get(beanName);
|
||||
if (object == null) {
|
||||
object = doGetObjectFromFactoryBean(factory, beanName);
|
||||
// Only post-process and store if not put there already during getObject() call above
|
||||
// (for example, because of circular reference processing triggered by custom getBean calls)
|
||||
Object alreadyThere = this.factoryBeanObjectCache.get(beanName);
|
||||
if (alreadyThere != null) {
|
||||
object = alreadyThere;
|
||||
}
|
||||
else {
|
||||
if (shouldPostProcess) {
|
||||
if (locked) {
|
||||
if (isSingletonCurrentlyInCreation(beanName)) {
|
||||
// Temporarily return non-post-processed object, not storing it yet
|
||||
return object;
|
||||
}
|
||||
beforeSingletonCreation(beanName);
|
||||
}
|
||||
try {
|
||||
object = postProcessObjectFromFactoryBean(object, beanName);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
throw new BeanCreationException(beanName,
|
||||
"Post-processing of FactoryBean's singleton object failed", ex);
|
||||
}
|
||||
finally {
|
||||
// Defensively synchronize against non-thread-safe FactoryBean.getObject() implementations,
|
||||
// potentially to be called from a background thread while the main thread currently calls
|
||||
// the same getObject() method within the singleton lock.
|
||||
synchronized (factory) {
|
||||
Object object = this.factoryBeanObjectCache.get(beanName);
|
||||
if (object == null) {
|
||||
object = doGetObjectFromFactoryBean(factory, beanName);
|
||||
// Only post-process and store if not put there already during getObject() call above
|
||||
// (for example, because of circular reference processing triggered by custom getBean calls)
|
||||
Object alreadyThere = this.factoryBeanObjectCache.get(beanName);
|
||||
if (alreadyThere != null) {
|
||||
object = alreadyThere;
|
||||
}
|
||||
else {
|
||||
if (shouldPostProcess) {
|
||||
if (locked) {
|
||||
afterSingletonCreation(beanName);
|
||||
if (isSingletonCurrentlyInCreation(beanName)) {
|
||||
// Temporarily return non-post-processed object, not storing it yet
|
||||
return object;
|
||||
}
|
||||
beforeSingletonCreation(beanName);
|
||||
}
|
||||
try {
|
||||
object = postProcessObjectFromFactoryBean(object, beanName);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
throw new BeanCreationException(beanName,
|
||||
"Post-processing of FactoryBean's singleton object failed", ex);
|
||||
}
|
||||
finally {
|
||||
if (locked) {
|
||||
afterSingletonCreation(beanName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (containsSingleton(beanName)) {
|
||||
this.factoryBeanObjectCache.put(beanName, object);
|
||||
if (containsSingleton(beanName)) {
|
||||
this.factoryBeanObjectCache.put(beanName, object);
|
||||
}
|
||||
}
|
||||
}
|
||||
return object;
|
||||
}
|
||||
return object;
|
||||
}
|
||||
finally {
|
||||
if (locked) {
|
||||
|
||||
+7
-2
@@ -87,6 +87,7 @@ import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.core.testfixture.io.SerializationTestUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.util.StringValueResolver;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -1798,7 +1799,7 @@ class DefaultListableBeanFactoryTests {
|
||||
|
||||
assertThatExceptionOfType(NoUniqueBeanDefinitionException.class)
|
||||
.isThrownBy(() -> lbf.getBean(TestBean.class))
|
||||
.withMessageContaining("more than one 'primary'");
|
||||
.withMessageEndingWith("more than one 'primary' bean found among candidates: [bd1, bd2]");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -2122,7 +2123,7 @@ class DefaultListableBeanFactoryTests {
|
||||
|
||||
assertThatExceptionOfType(NoUniqueBeanDefinitionException.class)
|
||||
.isThrownBy(() -> lbf.getBean(ConstructorDependency.class, 42))
|
||||
.withMessageContaining("more than one 'primary'");
|
||||
.withMessageEndingWith("more than one 'primary' bean found among candidates: [bd1, bd2]");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -3223,6 +3224,10 @@ class DefaultListableBeanFactoryTests {
|
||||
assertThat(lbf.getBeanNamesForType(DerivedTestBean.class)).containsExactly("bd1");
|
||||
assertThat(lbf.getBeanNamesForType(NestedTestBean.class)).isSameAs(nestedBeanNames);
|
||||
assertThat(lbf.getBeanNamesForType(Object.class)).isSameAs(allBeanNames);
|
||||
|
||||
lbf.registerSingleton("bd3", new Object());
|
||||
assertThat(lbf.getBeanNamesForType(NestedTestBean.class)).isSameAs(nestedBeanNames);
|
||||
assertThat(lbf.getBeanNamesForType(Object.class)).containsExactly(StringUtils.addStringToArray(allBeanNames, "bd3"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
+30
@@ -418,6 +418,16 @@ class InstanceSupplierCodeGeneratorTests {
|
||||
compileAndCheckWarnings(beanDefinition);
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateWhenTargetFactoryMethodIsProtectedAndReturnTypeIsDeprecated() {
|
||||
BeanDefinition beanDefinition = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(DeprecatedBean.class)
|
||||
.setFactoryMethodOnBean("deprecatedReturnTypeProtected", "config").getBeanDefinition();
|
||||
beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder
|
||||
.genericBeanDefinition(DeprecatedMemberConfiguration.class).getBeanDefinition());
|
||||
compileAndCheckWarnings(beanDefinition);
|
||||
}
|
||||
|
||||
private void compileAndCheckWarnings(BeanDefinition beanDefinition) {
|
||||
assertThatNoException().isThrownBy(() -> compile(TEST_COMPILER, beanDefinition,
|
||||
((instanceSupplier, compiled) -> {})));
|
||||
@@ -464,6 +474,26 @@ class InstanceSupplierCodeGeneratorTests {
|
||||
compileAndCheckWarnings(beanDefinition);
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateWhenTargetFactoryMethodReturnTypeIsDeprecatedForRemoval() {
|
||||
BeanDefinition beanDefinition = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(DeprecatedForRemovalBean.class)
|
||||
.setFactoryMethodOnBean("deprecatedReturnType", "config").getBeanDefinition();
|
||||
beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder
|
||||
.genericBeanDefinition(DeprecatedForRemovalMemberConfiguration.class).getBeanDefinition());
|
||||
compileAndCheckWarnings(beanDefinition);
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateWhenTargetFactoryMethodIsProtectedAndReturnTypeIsDeprecatedForRemoval() {
|
||||
BeanDefinition beanDefinition = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(DeprecatedForRemovalBean.class)
|
||||
.setFactoryMethodOnBean("deprecatedReturnTypeProtected", "config").getBeanDefinition();
|
||||
beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder
|
||||
.genericBeanDefinition(DeprecatedForRemovalMemberConfiguration.class).getBeanDefinition());
|
||||
compileAndCheckWarnings(beanDefinition);
|
||||
}
|
||||
|
||||
private void compileAndCheckWarnings(BeanDefinition beanDefinition) {
|
||||
assertThatNoException().isThrownBy(() -> compile(TEST_COMPILER, beanDefinition,
|
||||
((instanceSupplier, compiled) -> {})));
|
||||
|
||||
+10
@@ -33,4 +33,14 @@ public class DeprecatedForRemovalMemberConfiguration {
|
||||
return bean.toString();
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
public DeprecatedForRemovalBean deprecatedReturnType() {
|
||||
return new DeprecatedForRemovalBean();
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
DeprecatedForRemovalBean deprecatedReturnTypeProtected() {
|
||||
return new DeprecatedForRemovalBean();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
@@ -38,4 +38,9 @@ public class DeprecatedMemberConfiguration {
|
||||
return new DeprecatedBean();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
DeprecatedBean deprecatedReturnTypeProtected() {
|
||||
return new DeprecatedBean();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-2
@@ -37,12 +37,13 @@ import javax.lang.model.element.TypeElement;
|
||||
|
||||
/**
|
||||
* Annotation {@link Processor} that writes a {@link CandidateComponentsMetadata}
|
||||
* file for spring components.
|
||||
* file for Spring components.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Juergen Hoeller
|
||||
* @since 5.0
|
||||
* @deprecated as of 6.1, in favor of the AOT engine.
|
||||
* @deprecated as of 6.1, in favor of the AOT engine and the forthcoming
|
||||
* support for an AOT-generated Spring components index
|
||||
*/
|
||||
@Deprecated(since = "6.1", forRemoval = true)
|
||||
public class CandidateComponentsIndexer implements Processor {
|
||||
|
||||
+2
-2
@@ -25,8 +25,8 @@ import javax.lang.model.element.ElementKind;
|
||||
|
||||
/**
|
||||
* A {@link StereotypesProvider} that extracts a stereotype for each
|
||||
* {@code jakarta.*} or {@code javax.*} annotation <i>present</i> on a class or
|
||||
* interface.
|
||||
* {@code jakarta.*} or {@code javax.*} annotation <i>present</i>
|
||||
* on a class or interface.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 5.0
|
||||
|
||||
@@ -17,19 +17,18 @@
|
||||
package org.springframework.context.annotation;
|
||||
|
||||
/**
|
||||
* Enumeration used to determine whether JDK proxy-based or
|
||||
* Enumeration used to determine whether JDK/CGLIB proxy-based or
|
||||
* AspectJ weaving-based advice should be applied.
|
||||
*
|
||||
* @author Chris Beams
|
||||
* @since 3.1
|
||||
* @see org.springframework.scheduling.annotation.EnableAsync#mode()
|
||||
* @see org.springframework.scheduling.annotation.AsyncConfigurationSelector#selectImports
|
||||
* @see org.springframework.transaction.annotation.EnableTransactionManagement#mode()
|
||||
* @see org.springframework.scheduling.annotation.EnableAsync#mode()
|
||||
*/
|
||||
public enum AdviceMode {
|
||||
|
||||
/**
|
||||
* JDK proxy-based advice.
|
||||
* JDK/CGLIB proxy-based advice.
|
||||
*/
|
||||
PROXY,
|
||||
|
||||
|
||||
+6
-7
@@ -90,7 +90,6 @@ import org.springframework.util.ClassUtils;
|
||||
* @see ScannedGenericBeanDefinition
|
||||
* @see CandidateComponentsIndex
|
||||
*/
|
||||
@SuppressWarnings("removal") // components index
|
||||
public class ClassPathScanningCandidateComponentProvider implements EnvironmentCapable, ResourceLoaderAware {
|
||||
|
||||
static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
|
||||
@@ -452,9 +451,9 @@ public class ClassPathScanningCandidateComponentProvider implements EnvironmentC
|
||||
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
|
||||
Set<BeanDefinition> candidates = new LinkedHashSet<>();
|
||||
try {
|
||||
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
|
||||
String packageSearchPattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
|
||||
resolveBasePackage(basePackage) + '/' + this.resourcePattern;
|
||||
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
|
||||
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPattern);
|
||||
boolean traceEnabled = logger.isTraceEnabled();
|
||||
boolean debugEnabled = logger.isDebugEnabled();
|
||||
for (Resource resource : resources) {
|
||||
@@ -537,13 +536,13 @@ public class ClassPathScanningCandidateComponentProvider implements EnvironmentC
|
||||
* @return whether the class qualifies as a candidate component
|
||||
*/
|
||||
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
|
||||
for (TypeFilter tf : this.excludeFilters) {
|
||||
if (tf.match(metadataReader, getMetadataReaderFactory())) {
|
||||
for (TypeFilter filter : this.excludeFilters) {
|
||||
if (filter.match(metadataReader, getMetadataReaderFactory())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (TypeFilter tf : this.includeFilters) {
|
||||
if (tf.match(metadataReader, getMetadataReaderFactory())) {
|
||||
for (TypeFilter filter : this.includeFilters) {
|
||||
if (filter.match(metadataReader, getMetadataReaderFactory())) {
|
||||
return isConditionMatch(metadataReader);
|
||||
}
|
||||
}
|
||||
|
||||
+10
-2
@@ -157,9 +157,17 @@ class ConfigurationClassBeanDefinitionReader {
|
||||
|
||||
ScopeMetadata scopeMetadata = scopeMetadataResolver.resolveScopeMetadata(configBeanDef);
|
||||
configBeanDef.setScope(scopeMetadata.getScopeName());
|
||||
String configBeanName = this.importBeanNameGenerator.generateBeanName(configBeanDef, this.registry);
|
||||
AnnotationConfigUtils.processCommonDefinitionAnnotations(configBeanDef, metadata);
|
||||
|
||||
String configBeanName;
|
||||
try {
|
||||
configBeanName = this.importBeanNameGenerator.generateBeanName(configBeanDef, this.registry);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
throw new IllegalStateException("Failed to generate bean name for imported class '" +
|
||||
configClass.getMetadata().getClassName() + "'", ex);
|
||||
}
|
||||
|
||||
AnnotationConfigUtils.processCommonDefinitionAnnotations(configBeanDef, metadata);
|
||||
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(configBeanDef, configBeanName);
|
||||
definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
|
||||
this.registry.registerBeanDefinition(definitionHolder.getBeanName(), definitionHolder.getBeanDefinition());
|
||||
|
||||
+2
-2
@@ -63,13 +63,13 @@ public interface AotApplicationContextInitializer<C extends ConfigurableApplicat
|
||||
|
||||
private static <C extends ConfigurableApplicationContext> void initialize(
|
||||
C applicationContext, String... initializerClassNames) {
|
||||
|
||||
Log logger = LogFactory.getLog(AotApplicationContextInitializer.class);
|
||||
ClassLoader classLoader = applicationContext.getClassLoader();
|
||||
logger.debug("Initializing ApplicationContext with AOT");
|
||||
for (String initializerClassName : initializerClassNames) {
|
||||
logger.trace(LogMessage.format("Applying %s", initializerClassName));
|
||||
instantiateInitializer(initializerClassName, classLoader)
|
||||
.initialize(applicationContext);
|
||||
instantiateInitializer(initializerClassName, classLoader).initialize(applicationContext);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -101,10 +101,9 @@ public @interface EventListener {
|
||||
|
||||
/**
|
||||
* The event classes that this listener handles.
|
||||
* <p>If this attribute is specified with a single value, the
|
||||
* annotated method may optionally accept a single parameter.
|
||||
* However, if this attribute is specified with multiple values,
|
||||
* the annotated method must <em>not</em> declare any parameters.
|
||||
* <p>The annotated method may optionally accept a single parameter
|
||||
* of the given event class, or of a common base class or interface
|
||||
* for all given event classes.
|
||||
*/
|
||||
@AliasFor("value")
|
||||
Class<?>[] classes() default {};
|
||||
|
||||
+2
-4
@@ -45,9 +45,7 @@ import org.springframework.util.MultiValueMap;
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 5.0
|
||||
* @deprecated as of 6.1, in favor of the AOT engine.
|
||||
*/
|
||||
@Deprecated(since = "6.1", forRemoval = true)
|
||||
public class CandidateComponentsIndex {
|
||||
|
||||
private static final AntPathMatcher pathMatcher = new AntPathMatcher(".");
|
||||
@@ -83,7 +81,7 @@ public class CandidateComponentsIndex {
|
||||
public Set<String> getCandidateTypes(String basePackage, String stereotype) {
|
||||
List<Entry> candidates = this.index.get(stereotype);
|
||||
if (candidates != null) {
|
||||
return candidates.parallelStream()
|
||||
return candidates.stream()
|
||||
.filter(t -> t.match(basePackage))
|
||||
.map(t -> t.type)
|
||||
.collect(Collectors.toSet());
|
||||
@@ -94,7 +92,7 @@ public class CandidateComponentsIndex {
|
||||
|
||||
private static class Entry {
|
||||
|
||||
private final String type;
|
||||
final String type;
|
||||
|
||||
private final String packageName;
|
||||
|
||||
|
||||
-3
@@ -38,10 +38,7 @@ import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 5.0
|
||||
* @deprecated as of 6.1, in favor of the AOT engine.
|
||||
*/
|
||||
@Deprecated(since = "6.1", forRemoval = true)
|
||||
@SuppressWarnings("removal")
|
||||
public final class CandidateComponentsIndexLoader {
|
||||
|
||||
/**
|
||||
|
||||
+3
@@ -936,6 +936,9 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
|
||||
// Mark current thread for singleton instantiation with applied bootstrap locking.
|
||||
beanFactory.prepareSingletonBootstrap();
|
||||
|
||||
// Initialize bootstrap executor for this context.
|
||||
if (beanFactory.containsBean(BOOTSTRAP_EXECUTOR_BEAN_NAME) &&
|
||||
beanFactory.isTypeMatch(BOOTSTRAP_EXECUTOR_BEAN_NAME, Executor.class)) {
|
||||
|
||||
-1
@@ -87,7 +87,6 @@ public class AsyncAnnotationBeanPostProcessor extends AbstractBeanFactoryAwareAd
|
||||
private Class<? extends Annotation> asyncAnnotationType;
|
||||
|
||||
|
||||
|
||||
public AsyncAnnotationBeanPostProcessor() {
|
||||
setBeforeExistingAdvisors(true);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,9 @@ public class Task {
|
||||
|
||||
|
||||
/**
|
||||
* Return the underlying task.
|
||||
* Return a {@link Runnable} that executes the underlying task.
|
||||
* <p>Note, this does not necessarily return the {@link Task#Task(Runnable) original runnable}
|
||||
* as it can be wrapped by the Framework for additional support.
|
||||
*/
|
||||
public Runnable getRunnable() {
|
||||
return this.runnable;
|
||||
|
||||
-2
@@ -69,8 +69,6 @@ import org.springframework.validation.method.ParameterValidationResult;
|
||||
* at the type level of the containing target class, applying to all public service methods
|
||||
* of that class. By default, JSR-303 will validate against its default group only.
|
||||
*
|
||||
* <p>This functionality requires a Bean Validation 1.1+ provider.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 3.1
|
||||
|
||||
-2
@@ -60,8 +60,6 @@ import org.springframework.validation.method.MethodValidationResult;
|
||||
* inline constraint annotations. Validation groups can be specified through {@code @Validated}
|
||||
* as well. By default, JSR-303 will validate against its default group only.
|
||||
*
|
||||
* <p>This functionality requires a Bean Validation 1.1+ provider.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @since 3.1
|
||||
* @see MethodValidationInterceptor
|
||||
|
||||
-1
@@ -56,7 +56,6 @@ public class SpringConstraintValidatorFactory implements ConstraintValidatorFact
|
||||
return this.beanFactory.createBean(key);
|
||||
}
|
||||
|
||||
// Bean Validation 1.1 releaseInstance method
|
||||
@Override
|
||||
public void releaseInstance(ConstraintValidator<?, ?> instance) {
|
||||
this.beanFactory.destroyBean(instance);
|
||||
|
||||
-2
@@ -55,8 +55,6 @@ import org.springframework.validation.SmartValidator;
|
||||
* {@link CustomValidatorBean} and {@link LocalValidatorFactoryBean},
|
||||
* and as the primary implementation of the {@link SmartValidator} interface.
|
||||
*
|
||||
* <p>This adapter is fully compatible with Bean Validation 1.1 as well as 2.0.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 3.0
|
||||
|
||||
+55
@@ -31,6 +31,7 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.beans.testfixture.beans.TestBean;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.weaving.LoadTimeWeaverAware;
|
||||
import org.springframework.core.SpringProperties;
|
||||
import org.springframework.core.testfixture.EnabledForTestGroups;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
@@ -68,6 +69,16 @@ class BackgroundBootstrapTests {
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(10)
|
||||
@EnabledForTestGroups(LONG_RUNNING)
|
||||
void bootstrapWithLoadTimeWeaverAware() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(LoadTimeWeaverAwareBeanConfig.class);
|
||||
ctx.getBean("testBean1", TestBean.class);
|
||||
ctx.getBean("testBean2", TestBean.class);
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(10)
|
||||
@EnabledForTestGroups(LONG_RUNNING)
|
||||
@@ -266,6 +277,50 @@ class BackgroundBootstrapTests {
|
||||
}
|
||||
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class LoadTimeWeaverAwareBeanConfig {
|
||||
|
||||
@Bean
|
||||
LoadTimeWeaverAware loadTimeWeaverAware(ObjectProvider<TestBean> testBean1) {
|
||||
Thread thread = new Thread(testBean1::getObject);
|
||||
thread.start();
|
||||
try {
|
||||
thread.join();
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
return (loadTimeWeaver -> {});
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestBean testBean1(TestBean testBean2) {
|
||||
return new TestBean(testBean2);
|
||||
}
|
||||
|
||||
@Bean @Lazy
|
||||
public FactoryBean<TestBean> testBean2() {
|
||||
try {
|
||||
Thread.sleep(2000);
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
TestBean testBean = new TestBean();
|
||||
return new FactoryBean<>() {
|
||||
@Override
|
||||
public TestBean getObject() {
|
||||
return testBean;
|
||||
}
|
||||
@Override
|
||||
public Class<?> getObjectType() {
|
||||
return testBean.getClass();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class StrictLockingBeanConfig {
|
||||
|
||||
|
||||
+15
-9
@@ -61,6 +61,7 @@ class ClassPathBeanDefinitionScannerTests {
|
||||
GenericApplicationContext context = new GenericApplicationContext();
|
||||
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context);
|
||||
int beanCount = scanner.scan(BASE_PACKAGE);
|
||||
|
||||
assertThat(beanCount).isGreaterThanOrEqualTo(12);
|
||||
assertThat(context.containsBean("serviceInvocationCounter")).isTrue();
|
||||
assertThat(context.containsBean("fooServiceImpl")).isTrue();
|
||||
@@ -73,8 +74,8 @@ class ClassPathBeanDefinitionScannerTests {
|
||||
assertThat(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue();
|
||||
assertThat(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)).isTrue();
|
||||
assertThat(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_FACTORY_BEAN_NAME)).isTrue();
|
||||
context.refresh();
|
||||
|
||||
context.refresh();
|
||||
FooServiceImpl fooService = context.getBean("fooServiceImpl", FooServiceImpl.class);
|
||||
assertThat(context.getDefaultListableBeanFactory().containsSingleton("myNamedComponent")).isTrue();
|
||||
assertThat(fooService.foo(123)).isEqualTo("bar");
|
||||
@@ -157,6 +158,7 @@ class ClassPathBeanDefinitionScannerTests {
|
||||
|
||||
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context);
|
||||
int beanCount = scanner.scan(BASE_PACKAGE);
|
||||
|
||||
assertThat(beanCount).isGreaterThanOrEqualTo(12);
|
||||
|
||||
ClassPathBeanDefinitionScanner scanner2 = new ClassPathBeanDefinitionScanner(context) {
|
||||
@@ -182,8 +184,8 @@ class ClassPathBeanDefinitionScannerTests {
|
||||
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context);
|
||||
scanner.setIncludeAnnotationConfig(false);
|
||||
int beanCount = scanner.scan(BASE_PACKAGE);
|
||||
assertThat(beanCount).isGreaterThanOrEqualTo(7);
|
||||
|
||||
assertThat(beanCount).isGreaterThanOrEqualTo(7);
|
||||
assertThat(context.containsBean("serviceInvocationCounter")).isTrue();
|
||||
assertThat(context.containsBean("fooServiceImpl")).isTrue();
|
||||
assertThat(context.containsBean("stubFooDao")).isTrue();
|
||||
@@ -482,12 +484,14 @@ class ClassPathBeanDefinitionScannerTests {
|
||||
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context);
|
||||
scanner.setBeanNameGenerator(new TestBeanNameGenerator());
|
||||
int beanCount = scanner.scan(BASE_PACKAGE);
|
||||
assertThat(beanCount).isGreaterThanOrEqualTo(12);
|
||||
context.refresh();
|
||||
|
||||
assertThat(beanCount).isGreaterThanOrEqualTo(12);
|
||||
|
||||
context.refresh();
|
||||
FooServiceImpl fooService = context.getBean("fooService", FooServiceImpl.class);
|
||||
StaticListableBeanFactory myBf = (StaticListableBeanFactory) context.getBean("myBf");
|
||||
MessageSource ms = (MessageSource) context.getBean("messageSource");
|
||||
|
||||
assertThat(fooService.isInitCalled()).isTrue();
|
||||
assertThat(fooService.foo(123)).isEqualTo("bar");
|
||||
assertThat(fooService.lookupFoo(123)).isEqualTo("bar");
|
||||
@@ -509,9 +513,10 @@ class ClassPathBeanDefinitionScannerTests {
|
||||
scanner.setIncludeAnnotationConfig(false);
|
||||
scanner.setBeanNameGenerator(new TestBeanNameGenerator());
|
||||
int beanCount = scanner.scan(BASE_PACKAGE);
|
||||
assertThat(beanCount).isGreaterThanOrEqualTo(7);
|
||||
context.refresh();
|
||||
|
||||
assertThat(beanCount).isGreaterThanOrEqualTo(7);
|
||||
|
||||
context.refresh();
|
||||
try {
|
||||
context.getBean("fooService");
|
||||
}
|
||||
@@ -545,9 +550,10 @@ class ClassPathBeanDefinitionScannerTests {
|
||||
scanner.setAutowireCandidatePatterns("*NoSuchDao");
|
||||
scanner.scan(BASE_PACKAGE);
|
||||
context.refresh();
|
||||
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() ->
|
||||
context.getBean("fooService"))
|
||||
.satisfies(ex -> assertThat(ex.getMostSpecificCause()).isInstanceOf(NoSuchBeanDefinitionException.class));
|
||||
assertThatExceptionOfType(BeanCreationException.class)
|
||||
.isThrownBy(() -> context.getBean("fooService"))
|
||||
.satisfies(ex ->
|
||||
assertThat(ex.getMostSpecificCause()).isInstanceOf(NoSuchBeanDefinitionException.class));
|
||||
}
|
||||
|
||||
|
||||
|
||||
+34
-28
@@ -409,11 +409,14 @@ 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());
|
||||
.withMessageContainingAll(
|
||||
"bar",
|
||||
"SingletonBeanConfig",
|
||||
TestBean.class.getName()
|
||||
);
|
||||
}
|
||||
|
||||
@Test // gh-25430
|
||||
@@ -422,10 +425,13 @@ class ConfigurationClassPostProcessorTests {
|
||||
DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory();
|
||||
beanFactory.setAllowBeanDefinitionOverriding(false);
|
||||
context.register(FirstConfiguration.class, SecondConfiguration.class);
|
||||
|
||||
assertThatIllegalStateException().isThrownBy(context::refresh)
|
||||
.withMessageContaining("alias 'taskExecutor'")
|
||||
.withMessageContaining("name 'applicationTaskExecutor'")
|
||||
.withMessageContaining("bean definition 'taskExecutor'");
|
||||
.withMessageContainingAll(
|
||||
"alias 'taskExecutor'",
|
||||
"name 'applicationTaskExecutor'",
|
||||
"bean definition 'taskExecutor'"
|
||||
);
|
||||
context.close();
|
||||
}
|
||||
|
||||
@@ -978,7 +984,7 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSelfReferenceExclusionForFactoryMethodOnSameBean() {
|
||||
void selfReferenceExclusionForFactoryMethodOnSameBean() {
|
||||
AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor();
|
||||
bpp.setBeanFactory(beanFactory);
|
||||
beanFactory.addBeanPostProcessor(bpp);
|
||||
@@ -992,7 +998,7 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConfigWithDefaultMethods() {
|
||||
void configWithDefaultMethods() {
|
||||
AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor();
|
||||
bpp.setBeanFactory(beanFactory);
|
||||
beanFactory.addBeanPostProcessor(bpp);
|
||||
@@ -1006,7 +1012,7 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConfigWithDefaultMethodsUsingAsm() {
|
||||
void configWithDefaultMethodsUsingAsm() {
|
||||
AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor();
|
||||
bpp.setBeanFactory(beanFactory);
|
||||
beanFactory.addBeanPostProcessor(bpp);
|
||||
@@ -1020,7 +1026,7 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConfigWithFailingInit() { // gh-23343
|
||||
void configWithFailingInit() { // gh-23343
|
||||
AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor();
|
||||
bpp.setBeanFactory(beanFactory);
|
||||
beanFactory.addBeanPostProcessor(bpp);
|
||||
@@ -1034,7 +1040,7 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCircularDependency() {
|
||||
void circularDependency() {
|
||||
AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor();
|
||||
bpp.setBeanFactory(beanFactory);
|
||||
beanFactory.addBeanPostProcessor(bpp);
|
||||
@@ -1048,42 +1054,42 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCircularDependencyWithApplicationContext() {
|
||||
void circularDependencyWithApplicationContext() {
|
||||
assertThatExceptionOfType(BeanCreationException.class)
|
||||
.isThrownBy(() -> new AnnotationConfigApplicationContext(A.class, AStrich.class))
|
||||
.withMessageContaining("Circular reference");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPrototypeArgumentThroughBeanMethodCall() {
|
||||
void prototypeArgumentThroughBeanMethodCall() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(BeanArgumentConfigWithPrototype.class);
|
||||
ctx.getBean(FooFactory.class).createFoo(new BarArgument());
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSingletonArgumentThroughBeanMethodCall() {
|
||||
void singletonArgumentThroughBeanMethodCall() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(BeanArgumentConfigWithSingleton.class);
|
||||
ctx.getBean(FooFactory.class).createFoo(new BarArgument());
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNullArgumentThroughBeanMethodCall() {
|
||||
void nullArgumentThroughBeanMethodCall() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(BeanArgumentConfigWithNull.class);
|
||||
ctx.getBean("aFoo");
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInjectionPointMatchForNarrowTargetReturnType() {
|
||||
void injectionPointMatchForNarrowTargetReturnType() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(FooBarConfiguration.class);
|
||||
assertThat(ctx.getBean(FooImpl.class).bar).isSameAs(ctx.getBean(BarImpl.class));
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVarargOnBeanMethod() {
|
||||
void varargOnBeanMethod() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(VarargConfiguration.class, TestBean.class);
|
||||
VarargConfiguration bean = ctx.getBean(VarargConfiguration.class);
|
||||
assertThat(bean.testBeans).isNotNull();
|
||||
@@ -1093,7 +1099,7 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmptyVarargOnBeanMethod() {
|
||||
void emptyVarargOnBeanMethod() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(VarargConfiguration.class);
|
||||
VarargConfiguration bean = ctx.getBean(VarargConfiguration.class);
|
||||
assertThat(bean.testBeans).isNotNull();
|
||||
@@ -1102,7 +1108,7 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCollectionArgumentOnBeanMethod() {
|
||||
void collectionArgumentOnBeanMethod() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CollectionArgumentConfiguration.class, TestBean.class);
|
||||
CollectionArgumentConfiguration bean = ctx.getBean(CollectionArgumentConfiguration.class);
|
||||
assertThat(bean.testBeans).containsExactly(ctx.getBean(TestBean.class));
|
||||
@@ -1110,7 +1116,7 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmptyCollectionArgumentOnBeanMethod() {
|
||||
void emptyCollectionArgumentOnBeanMethod() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CollectionArgumentConfiguration.class);
|
||||
CollectionArgumentConfiguration bean = ctx.getBean(CollectionArgumentConfiguration.class);
|
||||
assertThat(bean.testBeans).isEmpty();
|
||||
@@ -1118,7 +1124,7 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMapArgumentOnBeanMethod() {
|
||||
void mapArgumentOnBeanMethod() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(MapArgumentConfiguration.class, DummyRunnable.class);
|
||||
MapArgumentConfiguration bean = ctx.getBean(MapArgumentConfiguration.class);
|
||||
assertThat(bean.testBeans).hasSize(1).containsValue(ctx.getBean(Runnable.class));
|
||||
@@ -1126,7 +1132,7 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmptyMapArgumentOnBeanMethod() {
|
||||
void emptyMapArgumentOnBeanMethod() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(MapArgumentConfiguration.class);
|
||||
MapArgumentConfiguration bean = ctx.getBean(MapArgumentConfiguration.class);
|
||||
assertThat(bean.testBeans).isEmpty();
|
||||
@@ -1134,7 +1140,7 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCollectionInjectionFromSameConfigurationClass() {
|
||||
void collectionInjectionFromSameConfigurationClass() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CollectionInjectionConfiguration.class);
|
||||
CollectionInjectionConfiguration bean = ctx.getBean(CollectionInjectionConfiguration.class);
|
||||
assertThat(bean.testBeans).containsExactly(ctx.getBean(TestBean.class));
|
||||
@@ -1142,7 +1148,7 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMapInjectionFromSameConfigurationClass() {
|
||||
void mapInjectionFromSameConfigurationClass() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(MapInjectionConfiguration.class);
|
||||
MapInjectionConfiguration bean = ctx.getBean(MapInjectionConfiguration.class);
|
||||
assertThat(bean.testBeans).containsOnly(Map.entry("testBean", ctx.getBean(Runnable.class)));
|
||||
@@ -1150,20 +1156,20 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBeanLookupFromSameConfigurationClass() {
|
||||
void beanLookupFromSameConfigurationClass() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(BeanLookupConfiguration.class);
|
||||
assertThat(ctx.getBean(BeanLookupConfiguration.class).getTestBean()).isSameAs(ctx.getBean(TestBean.class));
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNameClashBetweenConfigurationClassAndBean() {
|
||||
void nameClashBetweenConfigurationClassAndBean() {
|
||||
assertThatExceptionOfType(BeanDefinitionStoreException.class)
|
||||
.isThrownBy(() -> new AnnotationConfigApplicationContext(MyTestBean.class).getBean("myTestBean", TestBean.class));
|
||||
.isThrownBy(() -> new AnnotationConfigApplicationContext(MyTestBean.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBeanDefinitionRegistryPostProcessorConfig() {
|
||||
void beanDefinitionRegistryPostProcessorConfig() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(BeanDefinitionRegistryPostProcessorConfig.class);
|
||||
assertThat(ctx.getBean("myTestBean")).isInstanceOf(TestBean.class);
|
||||
ctx.close();
|
||||
|
||||
+17
-2
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.context.event;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
@@ -105,8 +106,9 @@ class AnnotationDrivenEventListenerTests {
|
||||
this.eventCollector.assertTotalEventsCount(1);
|
||||
|
||||
this.eventCollector.clear();
|
||||
this.context.publishEvent(event);
|
||||
this.eventCollector.assertEvent(listener, event);
|
||||
TestEvent otherEvent = new TestEvent(this, Integer.valueOf(1));
|
||||
this.context.publishEvent(otherEvent);
|
||||
this.eventCollector.assertEvent(listener, otherEvent);
|
||||
this.eventCollector.assertTotalEventsCount(1);
|
||||
|
||||
context.getBean(ApplicationEventMulticaster.class).removeApplicationListeners(l ->
|
||||
@@ -742,6 +744,11 @@ class AnnotationDrivenEventListenerTests {
|
||||
public void handleString(String content) {
|
||||
collectEvent(content);
|
||||
}
|
||||
|
||||
@EventListener({Boolean.class, Integer.class})
|
||||
public void handleBooleanOrInteger(Serializable content) {
|
||||
collectEvent(content);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1009,6 +1016,8 @@ class AnnotationDrivenEventListenerTests {
|
||||
|
||||
void handleString(String payload);
|
||||
|
||||
void handleBooleanOrInteger(Serializable content);
|
||||
|
||||
void handleTimestamp(Long timestamp);
|
||||
|
||||
void handleRatio(Double ratio);
|
||||
@@ -1031,6 +1040,12 @@ class AnnotationDrivenEventListenerTests {
|
||||
super.handleString(payload);
|
||||
}
|
||||
|
||||
@EventListener({Boolean.class, Integer.class})
|
||||
@Override
|
||||
public void handleBooleanOrInteger(Serializable content) {
|
||||
super.handleBooleanOrInteger(content);
|
||||
}
|
||||
|
||||
@ConditionalEvent("#root.event.timestamp > #p0")
|
||||
@Override
|
||||
public void handleTimestamp(Long timestamp) {
|
||||
|
||||
@@ -18,11 +18,12 @@ package org.springframework.context.event.test;
|
||||
|
||||
/**
|
||||
* @author Stephane Nicoll
|
||||
* @author Juergen Hoeller
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class TestEvent extends IdentifiableApplicationEvent {
|
||||
|
||||
public final String msg;
|
||||
public final Object msg;
|
||||
|
||||
public TestEvent(Object source, String id, String msg) {
|
||||
super(source, id);
|
||||
@@ -34,6 +35,11 @@ public class TestEvent extends IdentifiableApplicationEvent {
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
public TestEvent(Object source, Integer msg) {
|
||||
super(source);
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
public TestEvent(Object source) {
|
||||
this(source, "test");
|
||||
}
|
||||
|
||||
-2
@@ -32,8 +32,6 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("removal")
|
||||
public class CandidateComponentsIndexLoaderTests {
|
||||
|
||||
@Test
|
||||
|
||||
-2
@@ -30,8 +30,6 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("removal")
|
||||
public class CandidateComponentsIndexTests {
|
||||
|
||||
@Test
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1278,29 +1278,32 @@ public class Enhancer extends AbstractClassGenerator {
|
||||
Signature bridgeTarget = (Signature) bridgeToTarget.get(method.getSignature());
|
||||
if (bridgeTarget != null) {
|
||||
// checkcast each argument against the target's argument types
|
||||
for (int i = 0; i < bridgeTarget.getArgumentTypes().length; i++) {
|
||||
Type[] argTypes = method.getSignature().getArgumentTypes();
|
||||
Type[] targetTypes = bridgeTarget.getArgumentTypes();
|
||||
for (int i = 0; i < targetTypes.length; i++) {
|
||||
e.load_arg(i);
|
||||
Type target = bridgeTarget.getArgumentTypes()[i];
|
||||
if (!target.equals(method.getSignature().getArgumentTypes()[i])) {
|
||||
Type argType = argTypes[i];
|
||||
Type target = targetTypes[i];
|
||||
if (!target.equals(argType)) {
|
||||
if (!TypeUtils.isPrimitive(target)) {
|
||||
e.box(argType);
|
||||
}
|
||||
e.checkcast(target);
|
||||
}
|
||||
}
|
||||
|
||||
e.invoke_virtual_this(bridgeTarget);
|
||||
|
||||
// Not necessary to cast if the target & bridge have the same return type.
|
||||
Type retType = method.getSignature().getReturnType();
|
||||
// Not necessary to cast if the target & bridge have
|
||||
// the same return type.
|
||||
// (This conveniently includes void and primitive types,
|
||||
// which would fail if casted. It's not possible to
|
||||
// covariant from boxed to unbox (or vice versa), so no having
|
||||
// to box/unbox for bridges).
|
||||
// TODO: It also isn't necessary to checkcast if the return is
|
||||
// assignable from the target. (This would happen if a subclass
|
||||
// used covariant returns to narrow the return type within a bridge
|
||||
// method.)
|
||||
if (!retType.equals(bridgeTarget.getReturnType())) {
|
||||
e.checkcast(retType);
|
||||
Type target = bridgeTarget.getReturnType();
|
||||
if (!target.equals(retType)) {
|
||||
if (!TypeUtils.isPrimitive(target)) {
|
||||
e.unbox(retType);
|
||||
}
|
||||
else {
|
||||
e.checkcast(retType);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
+12
-8
@@ -175,14 +175,18 @@ public abstract class AbstractCharSequenceDecoder<T extends CharSequence> extend
|
||||
public final T decode(DataBuffer dataBuffer, ResolvableType elementType,
|
||||
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
|
||||
|
||||
Charset charset = getCharset(mimeType);
|
||||
T value = decodeInternal(dataBuffer, charset);
|
||||
DataBufferUtils.release(dataBuffer);
|
||||
LogFormatUtils.traceDebug(logger, traceOn -> {
|
||||
String formatted = LogFormatUtils.formatValue(value, !traceOn);
|
||||
return Hints.getLogPrefix(hints) + "Decoded " + formatted;
|
||||
});
|
||||
return value;
|
||||
try {
|
||||
Charset charset = getCharset(mimeType);
|
||||
T value = decodeInternal(dataBuffer, charset);
|
||||
LogFormatUtils.traceDebug(logger, traceOn -> {
|
||||
String formatted = LogFormatUtils.formatValue(value, !traceOn);
|
||||
return Hints.getLogPrefix(hints) + "Decoded " + formatted;
|
||||
});
|
||||
return value;
|
||||
}
|
||||
finally {
|
||||
DataBufferUtils.release(dataBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
private Charset getCharset(@Nullable MimeType mimeType) {
|
||||
|
||||
@@ -36,8 +36,9 @@ import org.springframework.util.Assert;
|
||||
* <p>{@code DataBuffer}s has a separate {@linkplain #readPosition() read} and
|
||||
* {@linkplain #writePosition() write} position, as opposed to {@code ByteBuffer}'s
|
||||
* single {@linkplain ByteBuffer#position() position}. As such, the {@code DataBuffer}
|
||||
* does not require a {@linkplain ByteBuffer#flip() flip} to read after writing. In general,
|
||||
* the following invariant holds for the read and write positions, and the capacity:
|
||||
* does not require a {@linkplain ByteBuffer#flip() flip} to read after writing.
|
||||
* In general, the following invariant holds for the read and write positions,
|
||||
* and the capacity:
|
||||
*
|
||||
* <blockquote>
|
||||
* {@code 0} {@code <=}
|
||||
@@ -46,12 +47,13 @@ import org.springframework.util.Assert;
|
||||
* <i>capacity</i>
|
||||
* </blockquote>
|
||||
*
|
||||
* <p>The {@linkplain #capacity() capacity} of a {@code DataBuffer} is expanded on demand,
|
||||
* similar to {@code StringBuilder}.
|
||||
* <p>The {@linkplain #capacity() capacity} of a {@code DataBuffer} is expanded on
|
||||
* demand, similar to {@code StringBuilder}.
|
||||
*
|
||||
* <p>The main purpose of the {@code DataBuffer} abstraction is to provide a convenient wrapper
|
||||
* around {@link ByteBuffer} which is similar to Netty's {@link io.netty.buffer.ByteBuf} but
|
||||
* can also be used on non-Netty platforms (i.e. Servlet containers).
|
||||
* <p>The main purpose of the {@code DataBuffer} abstraction is to provide a
|
||||
* convenient wrapper around {@link ByteBuffer} which is similar to Netty's
|
||||
* {@link io.netty.buffer.ByteBuf} but can also be used on non-Netty platforms
|
||||
* (i.e. Servlet containers).
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Brian Clozel
|
||||
@@ -113,8 +115,8 @@ public interface DataBuffer {
|
||||
* the current capacity, it will be expanded.
|
||||
* @param capacity the new capacity
|
||||
* @return this buffer
|
||||
* @deprecated as of 6.0, in favor of {@link #ensureWritable(int)}, which
|
||||
* has different semantics
|
||||
* @deprecated as of 6.0, in favor of {@link #ensureWritable(int)}
|
||||
* which has different semantics
|
||||
*/
|
||||
@Deprecated(since = "6.0")
|
||||
DataBuffer capacity(int capacity);
|
||||
@@ -186,6 +188,28 @@ public interface DataBuffer {
|
||||
*/
|
||||
byte getByte(int index);
|
||||
|
||||
/**
|
||||
* Process a range of bytes from the current buffer using a
|
||||
* {@link ByteProcessor}.
|
||||
* @param index the index at which the processing will start
|
||||
* @param length the maximum number of bytes to be processed
|
||||
* @param processor the processor that consumes bytes
|
||||
* @return the position that was reached when processing was stopped,
|
||||
* or {@code -1} if the entire byte range was processed.
|
||||
* @throws IndexOutOfBoundsException when {@code index} is out of bounds
|
||||
* @since 6.2.12
|
||||
*/
|
||||
default int forEachByte(int index, int length, ByteProcessor processor) {
|
||||
Assert.isTrue(length >= 0, "Length must be >= 0");
|
||||
for (int position = index; position < index + length; position++) {
|
||||
byte b = getByte(position);
|
||||
if(!processor.process(b)) {
|
||||
return position;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a single byte from the current reading position from this data buffer.
|
||||
* @return the byte at this buffer's current reading position
|
||||
@@ -218,16 +242,16 @@ public interface DataBuffer {
|
||||
DataBuffer write(byte b);
|
||||
|
||||
/**
|
||||
* Write the given source into this buffer, starting at the current writing position
|
||||
* of this buffer.
|
||||
* Write the given source into this buffer, starting at the current writing
|
||||
* position of this buffer.
|
||||
* @param source the bytes to be written into this buffer
|
||||
* @return this buffer
|
||||
*/
|
||||
DataBuffer write(byte[] source);
|
||||
|
||||
/**
|
||||
* Write at most {@code length} bytes of the given source into this buffer, starting
|
||||
* at the current writing position of this buffer.
|
||||
* Write at most {@code length} bytes of the given source into this buffer,
|
||||
* starting at the current writing position of this buffer.
|
||||
* @param source the bytes to be written into this buffer
|
||||
* @param offset the index within {@code source} to start writing from
|
||||
* @param length the maximum number of bytes to be written from {@code source}
|
||||
@@ -236,8 +260,8 @@ public interface DataBuffer {
|
||||
DataBuffer write(byte[] source, int offset, int length);
|
||||
|
||||
/**
|
||||
* Write one or more {@code DataBuffer}s to this buffer, starting at the current
|
||||
* writing position. It is the responsibility of the caller to
|
||||
* Write one or more {@code DataBuffer}s to this buffer, starting at the
|
||||
* current writing position. It is the responsibility of the caller to
|
||||
* {@linkplain DataBufferUtils#release(DataBuffer) release} the given data buffers.
|
||||
* @param buffers the byte buffers to write into this buffer
|
||||
* @return this buffer
|
||||
@@ -245,8 +269,8 @@ public interface DataBuffer {
|
||||
DataBuffer write(DataBuffer... buffers);
|
||||
|
||||
/**
|
||||
* Write one or more {@link ByteBuffer} to this buffer, starting at the current
|
||||
* writing position.
|
||||
* Write one or more {@link ByteBuffer} to this buffer, starting at the
|
||||
* current writing position.
|
||||
* @param buffers the byte buffers to write into this buffer
|
||||
* @return this buffer
|
||||
*/
|
||||
@@ -299,36 +323,37 @@ public interface DataBuffer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@code DataBuffer} whose contents is a shared subsequence of this
|
||||
* data buffer's content. Data between this data buffer and the returned buffer is
|
||||
* shared; though changes in the returned buffer's position will not be reflected
|
||||
* in the reading nor writing position of this data buffer.
|
||||
* Create a new {@code DataBuffer} whose contents is a shared subsequence
|
||||
* of this data buffer's content. Data between this data buffer and the
|
||||
* returned buffer is shared; though changes in the returned buffer's
|
||||
* position will not be reflected in the reading nor writing position
|
||||
* of this data buffer.
|
||||
* <p><strong>Note</strong> that this method will <strong>not</strong> call
|
||||
* {@link DataBufferUtils#retain(DataBuffer)} on the resulting slice: the reference
|
||||
* count will not be increased.
|
||||
* {@link DataBufferUtils#retain(DataBuffer)} on the resulting slice:
|
||||
* the reference count will not be increased.
|
||||
* @param index the index at which to start the slice
|
||||
* @param length the length of the slice
|
||||
* @return the specified slice of this data buffer
|
||||
* @deprecated as of 6.0, in favor of {@link #split(int)}, which
|
||||
* has different semantics
|
||||
* @deprecated as of 6.0, in favor of {@link #split(int)}
|
||||
* which has different semantics
|
||||
*/
|
||||
@Deprecated(since = "6.0")
|
||||
DataBuffer slice(int index, int length);
|
||||
|
||||
/**
|
||||
* Create a new {@code DataBuffer} whose contents is a shared, retained subsequence of this
|
||||
* data buffer's content. Data between this data buffer and the returned buffer is
|
||||
* shared; though changes in the returned buffer's position will not be reflected
|
||||
* in the reading nor writing position of this data buffer.
|
||||
* Create a new {@code DataBuffer} whose contents is a shared, retained subsequence
|
||||
* of this data buffer's content. Data between this data buffer and the returned
|
||||
* buffer is shared; though changes in the returned buffer's position will not be
|
||||
* reflected in the reading nor writing position of this data buffer.
|
||||
* <p><strong>Note</strong> that unlike {@link #slice(int, int)}, this method
|
||||
* <strong>will</strong> call {@link DataBufferUtils#retain(DataBuffer)} (or equivalent) on the
|
||||
* resulting slice.
|
||||
* <strong>will</strong> call {@link DataBufferUtils#retain(DataBuffer)}
|
||||
* (or equivalent) on the resulting slice.
|
||||
* @param index the index at which to start the slice
|
||||
* @param length the length of the slice
|
||||
* @return the specified, retained slice of this data buffer
|
||||
* @since 5.2
|
||||
* @deprecated as of 6.0, in favor of {@link #split(int)}, which
|
||||
* has different semantics
|
||||
* @deprecated as of 6.0, in favor of {@link #split(int)}
|
||||
* which has different semantics
|
||||
*/
|
||||
@Deprecated(since = "6.0")
|
||||
default DataBuffer retainedSlice(int index, int length) {
|
||||
@@ -337,12 +362,10 @@ public interface DataBuffer {
|
||||
|
||||
/**
|
||||
* Splits this data buffer into two at the given index.
|
||||
*
|
||||
* <p>Data that precedes the {@code index} will be returned in a new buffer,
|
||||
* while this buffer will contain data that follows after {@code index}.
|
||||
* Memory between the two buffers is shared, but independent and cannot
|
||||
* overlap (unlike {@link #slice(int, int) slice}).
|
||||
*
|
||||
* <p>The {@linkplain #readPosition() read} and
|
||||
* {@linkplain #writePosition() write} position of the returned buffer are
|
||||
* truncated to fit within the buffers {@linkplain #capacity() capacity} if
|
||||
@@ -362,22 +385,22 @@ public interface DataBuffer {
|
||||
* will not be reflected in the reading nor writing position of this data buffer.
|
||||
* @return this data buffer as a byte buffer
|
||||
* @deprecated as of 6.0, in favor of {@link #toByteBuffer(ByteBuffer)},
|
||||
* {@link #readableByteBuffers()}, or {@link #writableByteBuffers()}.
|
||||
* {@link #readableByteBuffers()} or {@link #writableByteBuffers()}
|
||||
*/
|
||||
@Deprecated(since = "6.0")
|
||||
ByteBuffer asByteBuffer();
|
||||
|
||||
/**
|
||||
* Expose a subsequence of this buffer's bytes as a {@link ByteBuffer}. Data between
|
||||
* this {@code DataBuffer} and the returned {@code ByteBuffer} is shared; though
|
||||
* changes in the returned buffer's {@linkplain ByteBuffer#position() position}
|
||||
* Expose a subsequence of this buffer's bytes as a {@link ByteBuffer}. Data
|
||||
* between this {@code DataBuffer} and the returned {@code ByteBuffer} is shared;
|
||||
* though changes in the returned buffer's {@linkplain ByteBuffer#position() position}
|
||||
* will not be reflected in the reading nor writing position of this data buffer.
|
||||
* @param index the index at which to start the byte buffer
|
||||
* @param length the length of the returned byte buffer
|
||||
* @return this data buffer as a byte buffer
|
||||
* @since 5.0.1
|
||||
* @deprecated as of 6.0, in favor of {@link #toByteBuffer(int, ByteBuffer, int, int)},
|
||||
* {@link #readableByteBuffers()}, or {@link #writableByteBuffers()}.
|
||||
* {@link #readableByteBuffers()} or {@link #writableByteBuffers()}
|
||||
*/
|
||||
@Deprecated(since = "6.0")
|
||||
ByteBuffer asByteBuffer(int index, int length);
|
||||
@@ -531,4 +554,22 @@ public interface DataBuffer {
|
||||
void close();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process a range of bytes one by one.
|
||||
* @since 6.2.12
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface ByteProcessor {
|
||||
|
||||
/**
|
||||
* Process the given {@code byte} and indicate whether processing
|
||||
* should continue further.
|
||||
* @param b a byte from the {@link DataBuffer}
|
||||
* @return {@code true} if processing should continue,
|
||||
* or {@code false} if processing should stop at this element
|
||||
*/
|
||||
boolean process(byte b);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+11
@@ -31,8 +31,19 @@ package org.springframework.core.io.buffer;
|
||||
public class DataBufferLimitException extends IllegalStateException {
|
||||
|
||||
|
||||
/**
|
||||
* Create an instance with the given message.
|
||||
*/
|
||||
public DataBufferLimitException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance with a message and a cause, e.g. {@link OutOfMemoryError}.
|
||||
* @since 6.2.12
|
||||
*/
|
||||
public DataBufferLimitException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -832,13 +832,9 @@ public abstract class DataBufferUtils {
|
||||
|
||||
@Override
|
||||
public int match(DataBuffer dataBuffer) {
|
||||
for (int pos = dataBuffer.readPosition(); pos < dataBuffer.writePosition(); pos++) {
|
||||
byte b = dataBuffer.getByte(pos);
|
||||
if (match(b)) {
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
int start = dataBuffer.readPosition();
|
||||
int end = dataBuffer.writePosition();
|
||||
return dataBuffer.forEachByte(start, end - start, b -> !this.match(b));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -881,14 +877,13 @@ public abstract class DataBufferUtils {
|
||||
|
||||
@Override
|
||||
public int match(DataBuffer dataBuffer) {
|
||||
for (int pos = dataBuffer.readPosition(); pos < dataBuffer.writePosition(); pos++) {
|
||||
byte b = dataBuffer.getByte(pos);
|
||||
if (match(b)) {
|
||||
reset();
|
||||
return pos;
|
||||
}
|
||||
int start = dataBuffer.readPosition();
|
||||
int end = dataBuffer.writePosition();
|
||||
int matchPosition = dataBuffer.forEachByte(start, end - start, b -> !this.match(b));
|
||||
if (matchPosition != -1) {
|
||||
reset();
|
||||
}
|
||||
return -1;
|
||||
return matchPosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -129,6 +129,11 @@ public class NettyDataBuffer implements PooledDataBuffer {
|
||||
return this.byteBuf.getByte(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int forEachByte(int index, int length, ByteProcessor processor) {
|
||||
return this.byteBuf.forEachByte(index, length, processor::process);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int capacity() {
|
||||
return this.byteBuf.capacity();
|
||||
@@ -374,7 +379,13 @@ public class NettyDataBuffer implements PooledDataBuffer {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.byteBuf.toString();
|
||||
try {
|
||||
return this.byteBuf.toString();
|
||||
}
|
||||
catch (OutOfMemoryError ex) {
|
||||
throw new DataBufferLimitException(
|
||||
"Failed to convert data buffer to string: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+5
-10
@@ -41,7 +41,6 @@ import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.NavigableSet;
|
||||
@@ -395,7 +394,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
}
|
||||
else {
|
||||
// a single resource with the given name
|
||||
return new Resource[] {getResourceLoader().getResource(locationPattern)};
|
||||
return new Resource[] {getResource(locationPattern)};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -937,14 +936,10 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
}
|
||||
Set<Resource> result = new LinkedHashSet<>(64);
|
||||
NavigableSet<String> entriesCache = new TreeSet<>();
|
||||
Iterator<String> entryIterator = jarFile.stream().map(JarEntry::getName).sorted().iterator();
|
||||
while (entryIterator.hasNext()) {
|
||||
String entryPath = entryIterator.next();
|
||||
int entrySeparatorIndex = entryPath.indexOf(ResourceUtils.JAR_URL_SEPARATOR);
|
||||
if (entrySeparatorIndex >= 0) {
|
||||
entryPath = entryPath.substring(entrySeparatorIndex + ResourceUtils.JAR_URL_SEPARATOR.length());
|
||||
}
|
||||
entriesCache.add(entryPath);
|
||||
for (Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements();) {
|
||||
entriesCache.add(entries.nextElement().getName());
|
||||
}
|
||||
for (String entryPath : entriesCache) {
|
||||
if (entryPath.startsWith(rootEntryPath)) {
|
||||
String relativePath = entryPath.substring(rootEntryPath.length());
|
||||
if (getPathMatcher().match(subPattern, relativePath)) {
|
||||
|
||||
@@ -22,9 +22,7 @@ import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link TaskExecutor} implementation that executes each task <i>synchronously</i>
|
||||
* in the calling thread.
|
||||
*
|
||||
* <p>Mainly intended for testing scenarios.
|
||||
* in the calling thread. Mainly intended for testing scenarios.
|
||||
*
|
||||
* <p>Execution in the calling thread does have the advantage of participating
|
||||
* in its thread context, for example the thread context class loader or the
|
||||
@@ -40,13 +38,13 @@ import org.springframework.util.Assert;
|
||||
public class SyncTaskExecutor implements TaskExecutor, Serializable {
|
||||
|
||||
/**
|
||||
* Executes the given {@code task} synchronously, through direct
|
||||
* invocation of it's {@link Runnable#run() run()} method.
|
||||
* @throws IllegalArgumentException if the given {@code task} is {@code null}
|
||||
* Execute the given {@code task} synchronously, through direct
|
||||
* invocation of its {@link Runnable#run() run()} method.
|
||||
* @throws RuntimeException if propagated from the given {@code Runnable}
|
||||
*/
|
||||
@Override
|
||||
public void execute(Runnable task) {
|
||||
Assert.notNull(task, "Runnable must not be null");
|
||||
Assert.notNull(task, "Task must not be null");
|
||||
task.run();
|
||||
}
|
||||
|
||||
|
||||
@@ -1483,8 +1483,8 @@ public abstract class ClassUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the highest publicly accessible method in the supplied method's type hierarchy that
|
||||
* has a method signature equivalent to the supplied method, if possible.
|
||||
* Get the closest publicly accessible (and exported) method in the supplied method's type
|
||||
* hierarchy that has a method signature equivalent to the supplied method, if possible.
|
||||
* <p>Otherwise, this method recursively searches the class hierarchy and implemented
|
||||
* interfaces for an equivalent method that is {@code public} and declared in a
|
||||
* {@code public} type.
|
||||
@@ -1507,18 +1507,21 @@ public abstract class ClassUtils {
|
||||
* @see #getMostSpecificMethod(Method, Class)
|
||||
*/
|
||||
public static Method getPubliclyAccessibleMethodIfPossible(Method method, @Nullable Class<?> targetClass) {
|
||||
// If the method is not public, we can abort the search immediately.
|
||||
if (!Modifier.isPublic(method.getModifiers())) {
|
||||
Class<?> declaringClass = method.getDeclaringClass();
|
||||
// If the method is not public or its declaring class is public and exported already,
|
||||
// we can abort the search immediately (avoiding reflection as well as cache access).
|
||||
if (!Modifier.isPublic(method.getModifiers()) || (Modifier.isPublic(declaringClass.getModifiers()) &&
|
||||
declaringClass.getModule().isExported(declaringClass.getPackageName(), ClassUtils.class.getModule()))) {
|
||||
return method;
|
||||
}
|
||||
|
||||
Method interfaceMethod = getInterfaceMethodIfPossible(method, targetClass, true);
|
||||
// If we found a method in a public interface, return the interface method.
|
||||
if (interfaceMethod != method) {
|
||||
if (interfaceMethod != method && interfaceMethod.getDeclaringClass().getModule().isExported(
|
||||
interfaceMethod.getDeclaringClass().getPackageName(), ClassUtils.class.getModule())) {
|
||||
return interfaceMethod;
|
||||
}
|
||||
|
||||
Class<?> declaringClass = method.getDeclaringClass();
|
||||
// Bypass cache for java.lang.Object unless it is actually an overridable method declared there.
|
||||
if (declaringClass.getSuperclass() == Object.class && !ReflectionUtils.isObjectMethod(method)) {
|
||||
return method;
|
||||
@@ -1533,19 +1536,20 @@ public abstract class ClassUtils {
|
||||
private static Method findPubliclyAccessibleMethodIfPossible(
|
||||
String methodName, Class<?>[] parameterTypes, Class<?> declaringClass) {
|
||||
|
||||
Method result = null;
|
||||
Class<?> current = declaringClass.getSuperclass();
|
||||
while (current != null) {
|
||||
Method method = getMethodOrNull(current, methodName, parameterTypes);
|
||||
if (method == null) {
|
||||
break;
|
||||
}
|
||||
if (Modifier.isPublic(method.getDeclaringClass().getModifiers())) {
|
||||
result = method;
|
||||
if (Modifier.isPublic(method.getDeclaringClass().getModifiers()) &&
|
||||
method.getDeclaringClass().getModule().isExported(
|
||||
method.getDeclaringClass().getPackageName(), ClassUtils.class.getModule())) {
|
||||
return method;
|
||||
}
|
||||
current = method.getDeclaringClass().getSuperclass();
|
||||
}
|
||||
return result;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,7 +18,6 @@ package org.springframework.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -36,10 +35,10 @@ import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
|
||||
* that can be resolved using a {@link PlaceholderResolver PlaceholderResolver},
|
||||
* <code>${</code> the prefix, and <code>}</code> the suffix.
|
||||
*
|
||||
* <p>A placeholder can also have a default value if its key does not represent a
|
||||
* known property. The default value is separated from the key using a
|
||||
* {@code separator}. For instance {@code ${name:John}} resolves to {@code John} if
|
||||
* the placeholder resolver does not provide a value for the {@code name}
|
||||
* <p>A placeholder can also have a default value if its key does not represent
|
||||
* a known property. The default value is separated from the key using a
|
||||
* {@code separator}. For instance {@code ${name:John}} resolves to {@code John}
|
||||
* if the placeholder resolver does not provide a value for the {@code name}
|
||||
* property.
|
||||
*
|
||||
* <p>Placeholders can also have a more complex structure, and the resolution of
|
||||
@@ -50,13 +49,14 @@ import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
|
||||
* must be rendered as is, the placeholder can be escaped using an {@code escape}
|
||||
* character. For instance {@code \${name}} resolves as {@code ${name}}.
|
||||
*
|
||||
* <p>The prefix, suffix, separator, and escape characters are configurable. Only
|
||||
* the prefix and suffix are mandatory, and the support for default values or
|
||||
* escaping is conditional on providing non-null values for them.
|
||||
* <p>The prefix, suffix, separator, and escape characters are configurable.
|
||||
* Only the prefix and suffix are mandatory, and the support for default values
|
||||
* or escaping is conditional on providing non-null values for them.
|
||||
*
|
||||
* <p>This parser makes sure to resolves placeholders as lazily as possible.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Juergen Hoeller
|
||||
* @since 6.2
|
||||
*/
|
||||
final class PlaceholderParser {
|
||||
@@ -120,51 +120,47 @@ final class PlaceholderParser {
|
||||
* @return the supplied value with placeholders replaced inline
|
||||
*/
|
||||
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
|
||||
Assert.notNull(value, "'value' must not be null");
|
||||
ParsedValue parsedValue = parse(value);
|
||||
List<Part> parts = parse(value, false);
|
||||
if (parts == null) {
|
||||
return value;
|
||||
}
|
||||
ParsedValue parsedValue = new ParsedValue(value, parts);
|
||||
PartResolutionContext resolutionContext = new PartResolutionContext(placeholderResolver,
|
||||
this.prefix, this.suffix, this.ignoreUnresolvablePlaceholders,
|
||||
candidate -> parse(candidate, false));
|
||||
return parsedValue.resolve(resolutionContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the specified value.
|
||||
* @param value the value containing the placeholders to be replaced
|
||||
* @return the different parts that have been identified
|
||||
*/
|
||||
ParsedValue parse(String value) {
|
||||
List<Part> parts = parse(value, false);
|
||||
return new ParsedValue(value, parts);
|
||||
}
|
||||
|
||||
private List<Part> parse(String value, boolean inPlaceholder) {
|
||||
LinkedList<Part> parts = new LinkedList<>();
|
||||
private @Nullable List<Part> parse(String value, boolean inPlaceholder) {
|
||||
int startIndex = nextStartPrefix(value, 0);
|
||||
if (startIndex == -1) {
|
||||
Part part = (inPlaceholder ? createSimplePlaceholderPart(value) : new TextPart(value));
|
||||
parts.add(part);
|
||||
return parts;
|
||||
return null;
|
||||
}
|
||||
List<Part> parts = new ArrayList<>(4);
|
||||
int position = 0;
|
||||
while (startIndex != -1) {
|
||||
int endIndex = nextValidEndPrefix(value, startIndex);
|
||||
if (endIndex == -1) { // Not a valid placeholder, consume the prefix and continue
|
||||
if (endIndex == -1) { // Not a valid placeholder, consume the prefix and continue
|
||||
addText(value, position, startIndex + this.prefix.length(), parts);
|
||||
position = startIndex + this.prefix.length();
|
||||
startIndex = nextStartPrefix(value, position);
|
||||
}
|
||||
else if (isEscaped(value, startIndex)) { // Not a valid index, accumulate and skip the escape character
|
||||
else if (isEscaped(value, startIndex)) { // Not a valid index, accumulate and skip the escape character
|
||||
addText(value, position, startIndex - 1, parts);
|
||||
addText(value, startIndex, startIndex + this.prefix.length(), parts);
|
||||
position = startIndex + this.prefix.length();
|
||||
startIndex = nextStartPrefix(value, position);
|
||||
}
|
||||
else { // Found valid placeholder, recursive parsing
|
||||
else { // Found valid placeholder, recursive parsing
|
||||
addText(value, position, startIndex, parts);
|
||||
String placeholder = value.substring(startIndex + this.prefix.length(), endIndex);
|
||||
List<Part> placeholderParts = parse(placeholder, true);
|
||||
parts.addAll(placeholderParts);
|
||||
if (placeholderParts == null) {
|
||||
parts.add(createSimplePlaceholderPart(placeholder));
|
||||
}
|
||||
else {
|
||||
parts.addAll(placeholderParts);
|
||||
}
|
||||
startIndex = nextStartPrefix(value, endIndex + this.suffix.length());
|
||||
position = endIndex + this.suffix.length();
|
||||
}
|
||||
@@ -241,29 +237,6 @@ final class PlaceholderParser {
|
||||
return new ParsedSection(buffer.toString(), null);
|
||||
}
|
||||
|
||||
private static void addText(String value, int start, int end, LinkedList<Part> parts) {
|
||||
if (start > end) {
|
||||
return;
|
||||
}
|
||||
String text = value.substring(start, end);
|
||||
if (!text.isEmpty()) {
|
||||
if (!parts.isEmpty()) {
|
||||
Part current = parts.removeLast();
|
||||
if (current instanceof TextPart textPart) {
|
||||
parts.add(new TextPart(textPart.text() + text));
|
||||
}
|
||||
else {
|
||||
parts.add(current);
|
||||
parts.add(new TextPart(text));
|
||||
}
|
||||
}
|
||||
else {
|
||||
parts.add(new TextPart(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private int nextStartPrefix(String value, int index) {
|
||||
return value.indexOf(this.prefix, index);
|
||||
}
|
||||
@@ -296,15 +269,46 @@ final class PlaceholderParser {
|
||||
return (this.escape != null && index > 0 && value.charAt(index - 1) == this.escape);
|
||||
}
|
||||
|
||||
record ParsedSection(String key, @Nullable String fallback) {
|
||||
private static void addText(String value, int start, int end, List<Part> parts) {
|
||||
if (start >= end) {
|
||||
return;
|
||||
}
|
||||
String text = value.substring(start, end);
|
||||
if (!parts.isEmpty() && parts.get(parts.size() - 1) instanceof TextPart textPart) {
|
||||
parts.set(parts.size() - 1, new TextPart(textPart.text() + text));
|
||||
}
|
||||
else {
|
||||
parts.add(new TextPart(text));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A representation of the parsing of an input string.
|
||||
* @param text the raw input string
|
||||
* @param parts the parts that appear in the string, in order
|
||||
*/
|
||||
private record ParsedValue(String text, List<Part> parts) {
|
||||
|
||||
public String resolve(PartResolutionContext resolutionContext) {
|
||||
try {
|
||||
return Part.resolveAll(this.parts, resolutionContext);
|
||||
}
|
||||
catch (PlaceholderResolutionException ex) {
|
||||
throw ex.withValue(this.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private record ParsedSection(String key, @Nullable String fallback) {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Provide the necessary context to handle and resolve underlying placeholders.
|
||||
*/
|
||||
static class PartResolutionContext implements PlaceholderResolver {
|
||||
private static class PartResolutionContext implements PlaceholderResolver {
|
||||
|
||||
private final String prefix;
|
||||
|
||||
@@ -319,7 +323,6 @@ final class PlaceholderParser {
|
||||
@Nullable
|
||||
private Set<String> visitedPlaceholders;
|
||||
|
||||
|
||||
PartResolutionContext(PlaceholderResolver resolver, String prefix, String suffix,
|
||||
boolean ignoreUnresolvablePlaceholders, Function<String, List<Part>> parser) {
|
||||
this.prefix = prefix;
|
||||
@@ -352,7 +355,7 @@ final class PlaceholderParser {
|
||||
return this.prefix + text + this.suffix;
|
||||
}
|
||||
|
||||
public List<Part> parse(String text) {
|
||||
public @Nullable List<Part> parse(String text) {
|
||||
return this.parser.apply(text);
|
||||
}
|
||||
|
||||
@@ -367,17 +370,17 @@ final class PlaceholderParser {
|
||||
}
|
||||
|
||||
public void removePlaceholder(String placeholder) {
|
||||
Assert.state(this.visitedPlaceholders != null, "Visited placeholders must not be null");
|
||||
this.visitedPlaceholders.remove(placeholder);
|
||||
if (this.visitedPlaceholders != null) {
|
||||
this.visitedPlaceholders.remove(placeholder);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A part is a section of a String containing placeholders to replace.
|
||||
*/
|
||||
interface Part {
|
||||
private interface Part {
|
||||
|
||||
/**
|
||||
* Resolve this part using the specified {@link PartResolutionContext}.
|
||||
@@ -408,30 +411,12 @@ final class PlaceholderParser {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A representation of the parsing of an input string.
|
||||
* @param text the raw input string
|
||||
* @param parts the parts that appear in the string, in order
|
||||
*/
|
||||
record ParsedValue(String text, List<Part> parts) {
|
||||
|
||||
public String resolve(PartResolutionContext resolutionContext) {
|
||||
try {
|
||||
return Part.resolveAll(this.parts, resolutionContext);
|
||||
}
|
||||
catch (PlaceholderResolutionException ex) {
|
||||
throw ex.withValue(this.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A base {@link Part} implementation.
|
||||
*/
|
||||
abstract static class AbstractPart implements Part {
|
||||
private abstract static class AbstractPart implements Part {
|
||||
|
||||
private final String text;
|
||||
final String text;
|
||||
|
||||
protected AbstractPart(String text) {
|
||||
this.text = text;
|
||||
@@ -454,29 +439,19 @@ final class PlaceholderParser {
|
||||
@Nullable
|
||||
protected String resolveRecursively(PartResolutionContext resolutionContext, String key) {
|
||||
String resolvedValue = resolutionContext.resolvePlaceholder(key);
|
||||
if (resolvedValue != null) {
|
||||
resolutionContext.flagPlaceholderAsVisited(key);
|
||||
// Let's check if we need to recursively resolve that value
|
||||
List<Part> nestedParts = resolutionContext.parse(resolvedValue);
|
||||
String value = toText(nestedParts);
|
||||
if (!isTextOnly(nestedParts)) {
|
||||
value = new ParsedValue(resolvedValue, nestedParts).resolve(resolutionContext);
|
||||
}
|
||||
resolutionContext.removePlaceholder(key);
|
||||
return value;
|
||||
if (resolvedValue == null) {
|
||||
// Not found
|
||||
return null;
|
||||
}
|
||||
// Not found
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isTextOnly(List<Part> parts) {
|
||||
return parts.stream().allMatch(TextPart.class::isInstance);
|
||||
}
|
||||
|
||||
private String toText(List<Part> parts) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
parts.forEach(part -> sb.append(part.text()));
|
||||
return sb.toString();
|
||||
// Let's check if we need to recursively resolve that value
|
||||
List<Part> nestedParts = resolutionContext.parse(resolvedValue);
|
||||
if (nestedParts == null) {
|
||||
return resolvedValue;
|
||||
}
|
||||
resolutionContext.flagPlaceholderAsVisited(key);
|
||||
String value = new ParsedValue(resolvedValue, nestedParts).resolve(resolutionContext);
|
||||
resolutionContext.removePlaceholder(key);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -484,7 +459,7 @@ final class PlaceholderParser {
|
||||
/**
|
||||
* A {@link Part} implementation that does not contain a valid placeholder.
|
||||
*/
|
||||
static class TextPart extends AbstractPart {
|
||||
private static class TextPart extends AbstractPart {
|
||||
|
||||
/**
|
||||
* Create a new instance.
|
||||
@@ -496,7 +471,7 @@ final class PlaceholderParser {
|
||||
|
||||
@Override
|
||||
public String resolve(PartResolutionContext resolutionContext) {
|
||||
return text();
|
||||
return this.text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,7 +480,7 @@ final class PlaceholderParser {
|
||||
* A {@link Part} implementation that represents a single placeholder with
|
||||
* a hard-coded fallback.
|
||||
*/
|
||||
static class SimplePlaceholderPart extends AbstractPart {
|
||||
private static class SimplePlaceholderPart extends AbstractPart {
|
||||
|
||||
private final String key;
|
||||
|
||||
@@ -533,13 +508,13 @@ final class PlaceholderParser {
|
||||
else if (this.fallback != null) {
|
||||
return this.fallback;
|
||||
}
|
||||
return resolutionContext.handleUnresolvablePlaceholder(this.key, text());
|
||||
return resolutionContext.handleUnresolvablePlaceholder(this.key, this.text);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String resolveRecursively(PartResolutionContext resolutionContext) {
|
||||
if (!this.text().equals(this.key)) {
|
||||
String value = resolveRecursively(resolutionContext, this.text());
|
||||
if (!this.text.equals(this.key)) {
|
||||
String value = resolveRecursively(resolutionContext, this.text);
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
@@ -553,7 +528,7 @@ final class PlaceholderParser {
|
||||
* A {@link Part} implementation that represents a single placeholder
|
||||
* containing nested placeholders.
|
||||
*/
|
||||
static class NestedPlaceholderPart extends AbstractPart {
|
||||
private static class NestedPlaceholderPart extends AbstractPart {
|
||||
|
||||
private final List<Part> keyParts;
|
||||
|
||||
@@ -582,7 +557,7 @@ final class PlaceholderParser {
|
||||
else if (this.defaultParts != null) {
|
||||
return Part.resolveAll(this.defaultParts, resolutionContext);
|
||||
}
|
||||
return resolutionContext.handleUnresolvablePlaceholder(resolvedKey, text());
|
||||
return resolutionContext.handleUnresolvablePlaceholder(resolvedKey, this.text);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ public class PropertyPlaceholderHelper {
|
||||
* @param properties the {@code Properties} to use for replacement
|
||||
* @return the supplied value with placeholders replaced inline
|
||||
*/
|
||||
public String replacePlaceholders(String value, final Properties properties) {
|
||||
public String replacePlaceholders(String value, Properties properties) {
|
||||
Assert.notNull(properties, "'properties' must not be null");
|
||||
return replacePlaceholders(value, properties::getProperty);
|
||||
}
|
||||
@@ -111,9 +111,10 @@ public class PropertyPlaceholderHelper {
|
||||
*/
|
||||
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
|
||||
Assert.notNull(value, "'value' must not be null");
|
||||
return parseStringValue(value, placeholderResolver);
|
||||
return this.parser.replacePlaceholders(value, placeholderResolver);
|
||||
}
|
||||
|
||||
@Deprecated(since = "6.2.12", forRemoval = true)
|
||||
protected String parseStringValue(String value, PlaceholderResolver placeholderResolver) {
|
||||
return this.parser.replacePlaceholders(value, placeholderResolver);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@ import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.testfixture.io.buffer.AbstractDataBufferAllocatingTests;
|
||||
|
||||
@@ -1045,4 +1047,35 @@ class DataBufferTests extends AbstractDataBufferAllocatingTests {
|
||||
release(buffer);
|
||||
}
|
||||
|
||||
@ParameterizedDataBufferAllocatingTest
|
||||
void forEachByteProcessAll(DataBufferFactory bufferFactory) {
|
||||
super.bufferFactory = bufferFactory;
|
||||
|
||||
List<Byte> result = new ArrayList<>();
|
||||
DataBuffer buffer = byteBuffer(new byte[]{'a', 'b', 'c', 'd'});
|
||||
int index = buffer.forEachByte(0, 4, b -> {
|
||||
result.add(b);
|
||||
return true;
|
||||
});
|
||||
assertThat(index).isEqualTo(-1);
|
||||
assertThat(result).containsExactly((byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd');
|
||||
release(buffer);
|
||||
}
|
||||
|
||||
|
||||
@ParameterizedDataBufferAllocatingTest
|
||||
void forEachByteProcessSome(DataBufferFactory bufferFactory) {
|
||||
super.bufferFactory = bufferFactory;
|
||||
|
||||
List<Byte> result = new ArrayList<>();
|
||||
DataBuffer buffer = byteBuffer(new byte[]{'a', 'b', 'c', 'd'});
|
||||
int index = buffer.forEachByte(0, 4, b -> {
|
||||
result.add(b);
|
||||
return (b != 'c');
|
||||
});
|
||||
assertThat(index).isEqualTo(2);
|
||||
assertThat(result).containsExactly((byte) 'a', (byte) 'b', (byte) 'c');
|
||||
release(buffer);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import java.lang.reflect.Member;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.net.URLConnection;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -687,13 +688,13 @@ class ClassUtilsTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void publicMethodInObjectClass() throws Exception {
|
||||
void publicMethodInPublicClass() throws Exception {
|
||||
Class<?> originalType = String.class;
|
||||
Method originalMethod = originalType.getDeclaredMethod("hashCode");
|
||||
Method originalMethod = originalType.getDeclaredMethod("toString");
|
||||
|
||||
Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, null);
|
||||
assertThat(publiclyAccessibleMethod.getDeclaringClass()).isEqualTo(Object.class);
|
||||
assertThat(publiclyAccessibleMethod.getName()).isEqualTo("hashCode");
|
||||
assertThat(publiclyAccessibleMethod.getDeclaringClass()).isEqualTo(originalType);
|
||||
assertThat(publiclyAccessibleMethod).isSameAs(originalMethod);
|
||||
assertPubliclyAccessible(publiclyAccessibleMethod);
|
||||
}
|
||||
|
||||
@@ -703,9 +704,20 @@ class ClassUtilsTests {
|
||||
Method originalMethod = originalType.getDeclaredMethod("size");
|
||||
|
||||
Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, null);
|
||||
// Should find the interface method in List.
|
||||
assertThat(publiclyAccessibleMethod.getDeclaringClass()).isEqualTo(List.class);
|
||||
assertThat(publiclyAccessibleMethod.getName()).isEqualTo("size");
|
||||
// Should not find the interface method in List.
|
||||
assertThat(publiclyAccessibleMethod.getDeclaringClass()).isEqualTo(originalType);
|
||||
assertThat(publiclyAccessibleMethod).isSameAs(originalMethod);
|
||||
assertPubliclyAccessible(publiclyAccessibleMethod);
|
||||
}
|
||||
|
||||
@Test
|
||||
void publicMethodInNonExportedClass() throws Exception {
|
||||
Class<?> originalType = getClass().getClassLoader().loadClass("sun.net.www.protocol.http.HttpURLConnection");
|
||||
Method originalMethod = originalType.getDeclaredMethod("getOutputStream");
|
||||
|
||||
Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, null);
|
||||
assertThat(publiclyAccessibleMethod.getDeclaringClass()).isEqualTo(URLConnection.class);
|
||||
assertThat(publiclyAccessibleMethod.getName()).isSameAs(originalMethod.getName());
|
||||
assertPubliclyAccessible(publiclyAccessibleMethod);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,8 +26,6 @@ import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import org.springframework.util.PlaceholderParser.ParsedValue;
|
||||
import org.springframework.util.PlaceholderParser.TextPart;
|
||||
import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -43,10 +41,11 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Sam Brannen
|
||||
* @author Juergen Hoeller
|
||||
*/
|
||||
class PlaceholderParserTests {
|
||||
|
||||
@Nested // Tests with only the basic placeholder feature enabled
|
||||
@Nested // Tests with only the basic placeholder feature enabled
|
||||
class OnlyPlaceholderTests {
|
||||
|
||||
private final PlaceholderParser parser = new PlaceholderParser("${", "}", null, null, true);
|
||||
@@ -82,7 +81,7 @@ class PlaceholderParserTests {
|
||||
Map<String, String> properties = Map.of(
|
||||
"p1", "v1",
|
||||
"p2", "v2",
|
||||
"p3", "${p1}:${p2}", // nested placeholders
|
||||
"p3", "${p1}:${p2}", // nested placeholders
|
||||
"p4", "${p3}", // deeply nested placeholders
|
||||
"p5", "${p1}:${p2}:${bogus}"); // unresolvable placeholder
|
||||
assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected);
|
||||
@@ -154,14 +153,13 @@ class PlaceholderParserTests {
|
||||
@Test
|
||||
void textWithInvalidPlaceholderSyntaxIsMerged() {
|
||||
String text = "test${of${with${and${";
|
||||
ParsedValue parsedValue = this.parser.parse(text);
|
||||
assertThat(parsedValue.parts()).singleElement().isInstanceOfSatisfying(TextPart.class,
|
||||
textPart -> assertThat(textPart.text()).isEqualTo(text));
|
||||
assertThat(this.parser.replacePlaceholders(text,
|
||||
placeholder -> {throw new UnsupportedOperationException();})).isEqualTo(text);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested // Tests with the use of a separator
|
||||
|
||||
@Nested // Tests with the use of a separator
|
||||
class DefaultValueTests {
|
||||
|
||||
private final PlaceholderParser parser = new PlaceholderParser("${", "}", ":", null, true);
|
||||
@@ -195,7 +193,7 @@ class PlaceholderParserTests {
|
||||
Map<String, String> properties = Map.of(
|
||||
"p1", "v1",
|
||||
"p2", "v2",
|
||||
"p3", "${p1}:${p2}", // nested placeholders
|
||||
"p3", "${p1}:${p2}", // nested placeholders
|
||||
"p4", "${p3}", // deeply nested placeholders
|
||||
"p5", "${p1}:${p2}:${bogus}", // unresolvable placeholder
|
||||
"p6", "${p1}:${p2}:${bogus:def}"); // unresolvable w/ default
|
||||
@@ -259,9 +257,9 @@ class PlaceholderParserTests {
|
||||
assertThat(this.parser.replacePlaceholders("${invalid:${firstName}}", resolver)).isEqualTo("John");
|
||||
verifyPlaceholderResolutions(resolver, "invalid", "firstName");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests that use the escape character.
|
||||
*/
|
||||
@@ -341,9 +339,9 @@ class PlaceholderParserTests {
|
||||
Arguments.of("${service/host/${app.environment}/name:\\value}", "https://example.com/qa/name"),
|
||||
Arguments.of("${service/host/${name\\:value}/}", "${service/host/${name:value}/}"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
class ExceptionTests {
|
||||
|
||||
@@ -378,7 +376,6 @@ class PlaceholderParserTests {
|
||||
.withMessage("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\" <-- \"${p3}\"")
|
||||
.withNoCause();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.jdbc.core.metadata;
|
||||
|
||||
import java.sql.DatabaseMetaData;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* The MySQL/MariaDB specific implementation of {@link TableMetaDataProvider}.
|
||||
* Sets {@link #setGeneratedKeysColumnNameArraySupported} to {@code false}.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @since 6.2.12
|
||||
*/
|
||||
public class MySQLTableMetaDataProvider extends GenericTableMetaDataProvider {
|
||||
|
||||
public MySQLTableMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLException {
|
||||
super(databaseMetaData);
|
||||
setGeneratedKeysColumnNameArraySupported(false);
|
||||
}
|
||||
|
||||
}
|
||||
+1
-2
@@ -417,8 +417,7 @@ public class TableMetaDataContext {
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this database support a column name String array for retrieving generated
|
||||
* keys?
|
||||
* Does this database support a column name String array for retrieving generated keys?
|
||||
* @see java.sql.Connection#createStruct(String, Object[])
|
||||
*/
|
||||
public boolean isGeneratedKeysColumnNameArraySupported() {
|
||||
|
||||
+2
-4
@@ -136,16 +136,14 @@ public interface TableMetaDataProvider {
|
||||
String getSimpleQueryForGetGeneratedKey(String tableName, String keyColumnName);
|
||||
|
||||
/**
|
||||
* Does this database support a column name String array for retrieving generated
|
||||
* keys?
|
||||
* Does this database support a column name String array for retrieving generated keys?
|
||||
* @see java.sql.Connection#createStruct(String, Object[])
|
||||
*/
|
||||
boolean isGeneratedKeysColumnNameArraySupported();
|
||||
|
||||
/**
|
||||
* Get the string used to quote SQL identifiers.
|
||||
* <p>This method returns a space ({@code " "}) if identifier quoting is not
|
||||
* supported.
|
||||
* <p>This method returns a space ({@code " "}) if identifier quoting is not supported.
|
||||
* @return database identifier quote string
|
||||
* @since 6.1
|
||||
* @see DatabaseMetaData#getIdentifierQuoteString()
|
||||
|
||||
+3
@@ -66,6 +66,9 @@ public final class TableMetaDataProviderFactory {
|
||||
else if ("HSQL Database Engine".equals(databaseProductName)) {
|
||||
provider = new HsqlTableMetaDataProvider(databaseMetaData);
|
||||
}
|
||||
else if ("MySQL".equals(databaseProductName) || "MariaDB".equals(databaseProductName)) {
|
||||
provider = new MySQLTableMetaDataProvider(databaseMetaData);
|
||||
}
|
||||
else {
|
||||
provider = new GenericTableMetaDataProvider(databaseMetaData);
|
||||
}
|
||||
|
||||
+5
-1
@@ -184,9 +184,13 @@ public abstract class JdbcTransactionObjectSupport implements SavepointManager,
|
||||
// typically on Oracle - ignore
|
||||
}
|
||||
catch (SQLException ex) {
|
||||
if ("3B001".equals(ex.getSQLState())) {
|
||||
// Savepoint already released (HSQLDB, PostgreSQL, DB2) - ignore
|
||||
return;
|
||||
}
|
||||
// ignore Microsoft SQLServerException: This operation is not supported.
|
||||
String msg = ex.getMessage();
|
||||
if (msg == null || !msg.contains("not supported")) {
|
||||
if (msg == null || (!msg.contains("not supported") && !msg.contains("3B001"))) {
|
||||
throw new TransactionSystemException("Could not explicitly release JDBC savepoint", ex);
|
||||
}
|
||||
}
|
||||
|
||||
+36
-27
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.jdbc.support;
|
||||
|
||||
import java.sql.BatchUpdateException;
|
||||
import java.sql.SQLDataException;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.SQLFeatureNotSupportedException;
|
||||
@@ -51,7 +52,10 @@ import org.springframework.lang.Nullable;
|
||||
* <p>Falls back to a standard {@link SQLStateSQLExceptionTranslator} if the JDBC
|
||||
* driver does not actually expose JDBC 4 compliant {@code SQLException} subclasses.
|
||||
*
|
||||
* <p>This translator serves as the default translator as of 6.0.
|
||||
* <p>This translator serves as the default JDBC exception translator as of 6.0.
|
||||
* As of 6.2.12, it specifically introspects {@link java.sql.BatchUpdateException}
|
||||
* to look at the underlying exception, analogous to the former default
|
||||
* {@link SQLErrorCodeSQLExceptionTranslator}.
|
||||
*
|
||||
* @author Thomas Risberg
|
||||
* @author Juergen Hoeller
|
||||
@@ -69,45 +73,50 @@ public class SQLExceptionSubclassTranslator extends AbstractFallbackSQLException
|
||||
@Override
|
||||
@Nullable
|
||||
protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) {
|
||||
if (ex instanceof SQLTransientException) {
|
||||
if (ex instanceof SQLTransientConnectionException) {
|
||||
return new TransientDataAccessResourceException(buildMessage(task, sql, ex), ex);
|
||||
SQLException sqlEx = ex;
|
||||
if (sqlEx instanceof BatchUpdateException && sqlEx.getNextException() != null) {
|
||||
sqlEx = sqlEx.getNextException();
|
||||
}
|
||||
|
||||
if (sqlEx instanceof SQLTransientException) {
|
||||
if (sqlEx instanceof SQLTransientConnectionException) {
|
||||
return new TransientDataAccessResourceException(buildMessage(task, sql, sqlEx), sqlEx);
|
||||
}
|
||||
if (ex instanceof SQLTransactionRollbackException) {
|
||||
if (SQLStateSQLExceptionTranslator.indicatesCannotAcquireLock(ex.getSQLState())) {
|
||||
return new CannotAcquireLockException(buildMessage(task, sql, ex), ex);
|
||||
if (sqlEx instanceof SQLTransactionRollbackException) {
|
||||
if (SQLStateSQLExceptionTranslator.indicatesCannotAcquireLock(sqlEx.getSQLState())) {
|
||||
return new CannotAcquireLockException(buildMessage(task, sql, sqlEx), sqlEx);
|
||||
}
|
||||
return new PessimisticLockingFailureException(buildMessage(task, sql, ex), ex);
|
||||
return new PessimisticLockingFailureException(buildMessage(task, sql, sqlEx), sqlEx);
|
||||
}
|
||||
if (ex instanceof SQLTimeoutException) {
|
||||
return new QueryTimeoutException(buildMessage(task, sql, ex), ex);
|
||||
if (sqlEx instanceof SQLTimeoutException) {
|
||||
return new QueryTimeoutException(buildMessage(task, sql, sqlEx), sqlEx);
|
||||
}
|
||||
}
|
||||
else if (ex instanceof SQLNonTransientException) {
|
||||
if (ex instanceof SQLNonTransientConnectionException) {
|
||||
return new DataAccessResourceFailureException(buildMessage(task, sql, ex), ex);
|
||||
else if (sqlEx instanceof SQLNonTransientException) {
|
||||
if (sqlEx instanceof SQLNonTransientConnectionException) {
|
||||
return new DataAccessResourceFailureException(buildMessage(task, sql, sqlEx), sqlEx);
|
||||
}
|
||||
if (ex instanceof SQLDataException) {
|
||||
return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex);
|
||||
if (sqlEx instanceof SQLDataException) {
|
||||
return new DataIntegrityViolationException(buildMessage(task, sql, sqlEx), sqlEx);
|
||||
}
|
||||
if (ex instanceof SQLIntegrityConstraintViolationException) {
|
||||
if (SQLStateSQLExceptionTranslator.indicatesDuplicateKey(ex.getSQLState(), ex.getErrorCode())) {
|
||||
return new DuplicateKeyException(buildMessage(task, sql, ex), ex);
|
||||
if (sqlEx instanceof SQLIntegrityConstraintViolationException) {
|
||||
if (SQLStateSQLExceptionTranslator.indicatesDuplicateKey(sqlEx.getSQLState(), sqlEx.getErrorCode())) {
|
||||
return new DuplicateKeyException(buildMessage(task, sql, sqlEx), sqlEx);
|
||||
}
|
||||
return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex);
|
||||
return new DataIntegrityViolationException(buildMessage(task, sql, sqlEx), sqlEx);
|
||||
}
|
||||
if (ex instanceof SQLInvalidAuthorizationSpecException) {
|
||||
return new PermissionDeniedDataAccessException(buildMessage(task, sql, ex), ex);
|
||||
if (sqlEx instanceof SQLInvalidAuthorizationSpecException) {
|
||||
return new PermissionDeniedDataAccessException(buildMessage(task, sql, sqlEx), sqlEx);
|
||||
}
|
||||
if (ex instanceof SQLSyntaxErrorException) {
|
||||
return new BadSqlGrammarException(task, (sql != null ? sql : ""), ex);
|
||||
if (sqlEx instanceof SQLSyntaxErrorException) {
|
||||
return new BadSqlGrammarException(task, (sql != null ? sql : ""), sqlEx);
|
||||
}
|
||||
if (ex instanceof SQLFeatureNotSupportedException) {
|
||||
return new InvalidDataAccessApiUsageException(buildMessage(task, sql, ex), ex);
|
||||
if (sqlEx instanceof SQLFeatureNotSupportedException) {
|
||||
return new InvalidDataAccessApiUsageException(buildMessage(task, sql, sqlEx), sqlEx);
|
||||
}
|
||||
}
|
||||
else if (ex instanceof SQLRecoverableException) {
|
||||
return new RecoverableDataAccessException(buildMessage(task, sql, ex), ex);
|
||||
else if (sqlEx instanceof SQLRecoverableException) {
|
||||
return new RecoverableDataAccessException(buildMessage(task, sql, sqlEx), sqlEx);
|
||||
}
|
||||
|
||||
// Fallback to Spring's own SQL state translation...
|
||||
|
||||
+34
-14
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.jdbc.support;
|
||||
|
||||
import java.sql.BatchUpdateException;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -41,7 +42,9 @@ import org.springframework.lang.Nullable;
|
||||
*
|
||||
* <p>This translator is commonly used as a {@link #setFallbackTranslator fallback}
|
||||
* behind a primary translator such as {@link SQLErrorCodeSQLExceptionTranslator} or
|
||||
* {@link SQLExceptionSubclassTranslator}.
|
||||
* {@link SQLExceptionSubclassTranslator}. As of 6.2.12, it specifically introspects
|
||||
* {@link java.sql.BatchUpdateException} to look at the underlying exception
|
||||
* (for alignment when used behind a {@link SQLExceptionSubclassTranslator}).
|
||||
*
|
||||
* @author Rod Johnson
|
||||
* @author Juergen Hoeller
|
||||
@@ -103,43 +106,60 @@ public class SQLStateSQLExceptionTranslator extends AbstractFallbackSQLException
|
||||
@Override
|
||||
@Nullable
|
||||
protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) {
|
||||
// First, the getSQLState check...
|
||||
String sqlState = getSqlState(ex);
|
||||
SQLException sqlEx = ex;
|
||||
String sqlState;
|
||||
if (sqlEx instanceof BatchUpdateException) {
|
||||
// Unwrap BatchUpdateException to expose contained exception
|
||||
// with potentially more specific SQL state.
|
||||
if (sqlEx.getNextException() != null) {
|
||||
SQLException nestedSqlEx = sqlEx.getNextException();
|
||||
if (nestedSqlEx.getSQLState() != null) {
|
||||
sqlEx = nestedSqlEx;
|
||||
}
|
||||
}
|
||||
sqlState = sqlEx.getSQLState();
|
||||
}
|
||||
else {
|
||||
// Expose top-level exception but potentially use nested SQL state.
|
||||
sqlState = getSqlState(sqlEx);
|
||||
}
|
||||
|
||||
// The actual SQL state check...
|
||||
if (sqlState != null && sqlState.length() >= 2) {
|
||||
String classCode = sqlState.substring(0, 2);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Extracted SQL state class '" + classCode + "' from value '" + sqlState + "'");
|
||||
}
|
||||
if (BAD_SQL_GRAMMAR_CODES.contains(classCode)) {
|
||||
return new BadSqlGrammarException(task, (sql != null ? sql : ""), ex);
|
||||
return new BadSqlGrammarException(task, (sql != null ? sql : ""), sqlEx);
|
||||
}
|
||||
else if (DATA_INTEGRITY_VIOLATION_CODES.contains(classCode)) {
|
||||
if (indicatesDuplicateKey(sqlState, ex.getErrorCode())) {
|
||||
return new DuplicateKeyException(buildMessage(task, sql, ex), ex);
|
||||
if (indicatesDuplicateKey(sqlState, sqlEx.getErrorCode())) {
|
||||
return new DuplicateKeyException(buildMessage(task, sql, sqlEx), sqlEx);
|
||||
}
|
||||
return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex);
|
||||
return new DataIntegrityViolationException(buildMessage(task, sql, sqlEx), sqlEx);
|
||||
}
|
||||
else if (PESSIMISTIC_LOCKING_FAILURE_CODES.contains(classCode)) {
|
||||
if (indicatesCannotAcquireLock(sqlState)) {
|
||||
return new CannotAcquireLockException(buildMessage(task, sql, ex), ex);
|
||||
return new CannotAcquireLockException(buildMessage(task, sql, sqlEx), sqlEx);
|
||||
}
|
||||
return new PessimisticLockingFailureException(buildMessage(task, sql, ex), ex);
|
||||
return new PessimisticLockingFailureException(buildMessage(task, sql, sqlEx), sqlEx);
|
||||
}
|
||||
else if (DATA_ACCESS_RESOURCE_FAILURE_CODES.contains(classCode)) {
|
||||
if (indicatesQueryTimeout(sqlState)) {
|
||||
return new QueryTimeoutException(buildMessage(task, sql, ex), ex);
|
||||
return new QueryTimeoutException(buildMessage(task, sql, sqlEx), sqlEx);
|
||||
}
|
||||
return new DataAccessResourceFailureException(buildMessage(task, sql, ex), ex);
|
||||
return new DataAccessResourceFailureException(buildMessage(task, sql, sqlEx), sqlEx);
|
||||
}
|
||||
else if (TRANSIENT_DATA_ACCESS_RESOURCE_CODES.contains(classCode)) {
|
||||
return new TransientDataAccessResourceException(buildMessage(task, sql, ex), ex);
|
||||
return new TransientDataAccessResourceException(buildMessage(task, sql, sqlEx), sqlEx);
|
||||
}
|
||||
}
|
||||
|
||||
// For MySQL: exception class name indicating a timeout?
|
||||
// (since MySQL doesn't throw the JDBC 4 SQLTimeoutException)
|
||||
if (ex.getClass().getName().contains("Timeout")) {
|
||||
return new QueryTimeoutException(buildMessage(task, sql, ex), ex);
|
||||
if (sqlEx.getClass().getName().contains("Timeout")) {
|
||||
return new QueryTimeoutException(buildMessage(task, sql, sqlEx), sqlEx);
|
||||
}
|
||||
|
||||
// Couldn't resolve anything proper - resort to UncategorizedSQLException.
|
||||
|
||||
+33
-26
@@ -43,7 +43,7 @@ import org.springframework.dao.RecoverableDataAccessException;
|
||||
import org.springframework.dao.TransientDataAccessResourceException;
|
||||
import org.springframework.jdbc.BadSqlGrammarException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.jdbc.support.SQLStateSQLExceptionTranslatorTests.buildBatchUpdateException;
|
||||
|
||||
/**
|
||||
* @author Thomas Risberg
|
||||
@@ -51,43 +51,50 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
*/
|
||||
class SQLExceptionSubclassTranslatorTests {
|
||||
|
||||
private final SQLExceptionTranslator translator = new SQLExceptionSubclassTranslator();
|
||||
|
||||
|
||||
@Test
|
||||
void exceptionClassTranslation() {
|
||||
doTest(new SQLDataException("", "", 0), DataIntegrityViolationException.class);
|
||||
doTest(new SQLFeatureNotSupportedException("", "", 0), InvalidDataAccessApiUsageException.class);
|
||||
doTest(new SQLIntegrityConstraintViolationException("", "", 0), DataIntegrityViolationException.class);
|
||||
doTest(new SQLIntegrityConstraintViolationException("", "23505", 0), DuplicateKeyException.class);
|
||||
doTest(new SQLIntegrityConstraintViolationException("", "23000", 1), DuplicateKeyException.class);
|
||||
doTest(new SQLIntegrityConstraintViolationException("", "23000", 1062), DuplicateKeyException.class);
|
||||
doTest(new SQLIntegrityConstraintViolationException("", "23000", 2601), DuplicateKeyException.class);
|
||||
doTest(new SQLIntegrityConstraintViolationException("", "23000", 2627), DuplicateKeyException.class);
|
||||
doTest(new SQLInvalidAuthorizationSpecException("", "", 0), PermissionDeniedDataAccessException.class);
|
||||
doTest(new SQLNonTransientConnectionException("", "", 0), DataAccessResourceFailureException.class);
|
||||
doTest(new SQLRecoverableException("", "", 0), RecoverableDataAccessException.class);
|
||||
doTest(new SQLSyntaxErrorException("", "", 0), BadSqlGrammarException.class);
|
||||
doTest(new SQLTimeoutException("", "", 0), QueryTimeoutException.class);
|
||||
doTest(new SQLTransactionRollbackException("", "", 0), PessimisticLockingFailureException.class);
|
||||
doTest(new SQLTransactionRollbackException("", "40001", 0), CannotAcquireLockException.class);
|
||||
doTest(new SQLTransientConnectionException("", "", 0), TransientDataAccessResourceException.class);
|
||||
assertTranslation(new SQLDataException("", "", 0), DataIntegrityViolationException.class);
|
||||
assertTranslation(new SQLFeatureNotSupportedException("", "", 0), InvalidDataAccessApiUsageException.class);
|
||||
assertTranslation(new SQLIntegrityConstraintViolationException("", "", 0), DataIntegrityViolationException.class);
|
||||
assertTranslation(new SQLIntegrityConstraintViolationException("", "23505", 0), DuplicateKeyException.class);
|
||||
assertTranslation(new SQLIntegrityConstraintViolationException("", "23000", 1), DuplicateKeyException.class);
|
||||
assertTranslation(new SQLIntegrityConstraintViolationException("", "23000", 1062), DuplicateKeyException.class);
|
||||
assertTranslation(new SQLIntegrityConstraintViolationException("", "23000", 2601), DuplicateKeyException.class);
|
||||
assertTranslation(new SQLIntegrityConstraintViolationException("", "23000", 2627), DuplicateKeyException.class);
|
||||
assertTranslation(new SQLInvalidAuthorizationSpecException("", "", 0), PermissionDeniedDataAccessException.class);
|
||||
assertTranslation(new SQLNonTransientConnectionException("", "", 0), DataAccessResourceFailureException.class);
|
||||
assertTranslation(new SQLRecoverableException("", "", 0), RecoverableDataAccessException.class);
|
||||
assertTranslation(new SQLSyntaxErrorException("", "", 0), BadSqlGrammarException.class);
|
||||
assertTranslation(new SQLTimeoutException("", "", 0), QueryTimeoutException.class);
|
||||
assertTranslation(new SQLTransactionRollbackException("", "", 0), PessimisticLockingFailureException.class);
|
||||
assertTranslation(new SQLTransactionRollbackException("", "40001", 0), CannotAcquireLockException.class);
|
||||
assertTranslation(new SQLTransientConnectionException("", "", 0), TransientDataAccessResourceException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void batchExceptionTranslation() {
|
||||
assertTranslation(buildBatchUpdateException("JZ", new SQLIntegrityConstraintViolationException("", "23505", 0)),
|
||||
DuplicateKeyException.class);
|
||||
assertTranslation(buildBatchUpdateException(null, new SQLIntegrityConstraintViolationException("", "23505", 0)),
|
||||
DuplicateKeyException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallbackStateTranslation() {
|
||||
// Test fallback. We assume that no database will ever return this error code,
|
||||
// but 07xxx will be bad grammar picked up by the fallback SQLState translator
|
||||
doTest(new SQLException("", "07xxx", 666666666), BadSqlGrammarException.class);
|
||||
assertTranslation(new SQLException("", "07xxx", 666666666), BadSqlGrammarException.class);
|
||||
// and 08xxx will be data resource failure (non-transient) picked up by the fallback SQLState translator
|
||||
doTest(new SQLException("", "08xxx", 666666666), DataAccessResourceFailureException.class);
|
||||
assertTranslation(new SQLException("", "08xxx", 666666666), DataAccessResourceFailureException.class);
|
||||
}
|
||||
|
||||
|
||||
private void doTest(SQLException ex, Class<?> dataAccessExceptionType) {
|
||||
SQLExceptionTranslator translator = new SQLExceptionSubclassTranslator();
|
||||
DataAccessException dax = translator.translate("task", "SQL", ex);
|
||||
|
||||
assertThat(dax).as("Specific translation must not result in null").isNotNull();
|
||||
assertThat(dax).as("Wrong DataAccessException type returned").isExactlyInstanceOf(dataAccessExceptionType);
|
||||
assertThat(dax.getCause()).as("The exact same original SQLException must be preserved").isSameAs(ex);
|
||||
private void assertTranslation(SQLException ex, Class<?> dataAccessExceptionType) {
|
||||
DataAccessException dae = translator.translate("task", "SQL", ex);
|
||||
SQLStateSQLExceptionTranslatorTests.assertTranslation(dae, ex, dataAccessExceptionType);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+33
-8
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.jdbc.support;
|
||||
|
||||
import java.sql.BatchUpdateException;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -45,6 +46,7 @@ class SQLStateSQLExceptionTranslatorTests {
|
||||
|
||||
private final SQLExceptionTranslator translator = new SQLStateSQLExceptionTranslator();
|
||||
|
||||
|
||||
@Test
|
||||
void translateNullException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> translator.translate("", "", null));
|
||||
@@ -125,6 +127,16 @@ class SQLStateSQLExceptionTranslatorTests {
|
||||
assertTranslation("57014", QueryTimeoutException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void translateWithinQualifiedBatch() {
|
||||
assertTranslation(buildBatchUpdateException("JZ", new SQLException("", "23505", 0)), DuplicateKeyException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void translateWithinUnqualifiedBatch() {
|
||||
assertTranslation(buildBatchUpdateException(null, new SQLException("", "23505", 0)), DuplicateKeyException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void translateUncategorized() {
|
||||
assertTranslation("00000000", null);
|
||||
@@ -142,28 +154,41 @@ class SQLStateSQLExceptionTranslatorTests {
|
||||
*/
|
||||
@Test
|
||||
void malformedSqlStateCodes() {
|
||||
assertTranslation(null, null);
|
||||
assertTranslation((String) null, null);
|
||||
assertTranslation("", null);
|
||||
assertTranslation("I", null);
|
||||
}
|
||||
|
||||
|
||||
private void assertTranslation(@Nullable String sqlState, @Nullable Class<?> dataAccessExceptionType) {
|
||||
assertTranslation(sqlState, 0, dataAccessExceptionType);
|
||||
assertTranslation(new SQLException("reason", sqlState, 0), dataAccessExceptionType);
|
||||
}
|
||||
|
||||
private void assertTranslation(@Nullable String sqlState, int errorCode, @Nullable Class<?> dataAccessExceptionType) {
|
||||
SQLException ex = new SQLException("reason", sqlState, errorCode);
|
||||
DataAccessException dax = translator.translate("task", "SQL", ex);
|
||||
assertTranslation(new SQLException("reason", sqlState, errorCode), dataAccessExceptionType);
|
||||
}
|
||||
|
||||
private void assertTranslation(SQLException ex, @Nullable Class<?> dataAccessExceptionType) {
|
||||
DataAccessException dae = translator.translate("task", "SQL", ex);
|
||||
|
||||
if (dataAccessExceptionType == null) {
|
||||
assertThat(dax).as("Expected translation to null").isNull();
|
||||
assertThat(dae).as("Expected translation to null").isNull();
|
||||
return;
|
||||
}
|
||||
assertTranslation(dae, ex, dataAccessExceptionType);
|
||||
}
|
||||
|
||||
assertThat(dax).as("Specific translation must not result in null").isNotNull();
|
||||
assertThat(dax).as("Wrong DataAccessException type returned").isExactlyInstanceOf(dataAccessExceptionType);
|
||||
assertThat(dax.getCause()).as("The exact same original SQLException must be preserved").isSameAs(ex);
|
||||
static void assertTranslation(DataAccessException dae, SQLException ex, Class<?> dataAccessExceptionType) {
|
||||
assertThat(dae).as("Specific translation must not result in null").isNotNull();
|
||||
assertThat(dae).as("Wrong DataAccessException type returned").isExactlyInstanceOf(dataAccessExceptionType);
|
||||
assertThat(dae.getCause()).as("The exact same original SQLException must be preserved").isSameAs(
|
||||
ex instanceof BatchUpdateException bue ? bue.getNextException() : ex);
|
||||
}
|
||||
|
||||
static BatchUpdateException buildBatchUpdateException(@Nullable String sqlState, SQLException next) {
|
||||
BatchUpdateException ex = new BatchUpdateException("", sqlState, null);
|
||||
ex.setNextException(next);
|
||||
return ex;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-1
@@ -54,7 +54,6 @@ import org.springframework.util.ResourceUtils;
|
||||
* @author Stephane Nicoll
|
||||
* @since 6.0
|
||||
*/
|
||||
@SuppressWarnings("removal") // components index
|
||||
public final class PersistenceManagedTypesScanner {
|
||||
|
||||
private static final String CLASS_RESOURCE_PATTERN = "/**/*.class";
|
||||
|
||||
+12
-11
@@ -74,8 +74,7 @@ public abstract class AbstractTestNGSpringContextTests implements IHookable, App
|
||||
|
||||
private final TestContextManager testContextManager;
|
||||
|
||||
@Nullable
|
||||
private Throwable testException;
|
||||
private final ThreadLocal<Throwable> testException = new ThreadLocal<>();
|
||||
|
||||
|
||||
/**
|
||||
@@ -141,31 +140,33 @@ public abstract class AbstractTestNGSpringContextTests implements IHookable, App
|
||||
public void run(IHookCallBack callBack, ITestResult testResult) {
|
||||
Method testMethod = testResult.getMethod().getConstructorOrMethod().getMethod();
|
||||
boolean beforeCallbacksExecuted = false;
|
||||
Throwable currentException = null;
|
||||
|
||||
try {
|
||||
this.testContextManager.beforeTestExecution(this, testMethod);
|
||||
beforeCallbacksExecuted = true;
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
this.testException = ex;
|
||||
currentException = ex;
|
||||
}
|
||||
|
||||
if (beforeCallbacksExecuted) {
|
||||
callBack.runTestMethod(testResult);
|
||||
this.testException = getTestResultException(testResult);
|
||||
currentException = getTestResultException(testResult);
|
||||
}
|
||||
|
||||
try {
|
||||
this.testContextManager.afterTestExecution(this, testMethod, this.testException);
|
||||
this.testContextManager.afterTestExecution(this, testMethod, currentException);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
if (this.testException == null) {
|
||||
this.testException = ex;
|
||||
if (currentException == null) {
|
||||
currentException = ex;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.testException != null) {
|
||||
throwAsUncheckedException(this.testException);
|
||||
if (currentException != null) {
|
||||
this.testException.set(currentException);
|
||||
throwAsUncheckedException(currentException);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,10 +181,10 @@ public abstract class AbstractTestNGSpringContextTests implements IHookable, App
|
||||
@AfterMethod(alwaysRun = true)
|
||||
protected void springTestContextAfterTestMethod(Method testMethod) throws Exception {
|
||||
try {
|
||||
this.testContextManager.afterTestMethod(this, testMethod, this.testException);
|
||||
this.testContextManager.afterTestMethod(this, testMethod, this.testException.get());
|
||||
}
|
||||
finally {
|
||||
this.testException = null;
|
||||
this.testException.remove();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -67,7 +67,9 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder {
|
||||
ClassLoader loader = DefaultWebTestClientBuilder.class.getClassLoader();
|
||||
reactorNettyClientPresent = ClassUtils.isPresent("reactor.netty.http.client.HttpClient", loader);
|
||||
reactorNetty2ClientPresent = ClassUtils.isPresent("reactor.netty5.http.client.HttpClient", loader);
|
||||
jettyClientPresent = ClassUtils.isPresent("org.eclipse.jetty.client.HttpClient", loader);
|
||||
jettyClientPresent =
|
||||
ClassUtils.isPresent("org.eclipse.jetty.client.HttpClient", loader) &&
|
||||
ClassUtils.isPresent("org.eclipse.jetty.reactive.client.ReactiveRequest", loader);
|
||||
httpComponentsClientPresent =
|
||||
ClassUtils.isPresent("org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient", loader) &&
|
||||
ClassUtils.isPresent("org.apache.hc.core5.reactive.ReactiveDataConsumer", loader);
|
||||
|
||||
+7
-9
@@ -854,16 +854,14 @@ public abstract class AbstractMockHttpServletRequestBuilder<B extends AbstractMo
|
||||
request.setContextPath(this.contextPath);
|
||||
request.setServletPath(this.servletPath);
|
||||
|
||||
if ("".equals(this.pathInfo)) {
|
||||
if (!requestUri.startsWith(this.contextPath + this.servletPath)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid servlet path [" + this.servletPath + "] for request URI [" + requestUri + "]");
|
||||
}
|
||||
String extraPath = requestUri.substring(this.contextPath.length() + this.servletPath.length());
|
||||
this.pathInfo = (StringUtils.hasText(extraPath) ?
|
||||
UrlPathHelper.defaultInstance.decodeRequestString(request, extraPath) : null);
|
||||
String path = this.pathInfo;
|
||||
if ("".equals(path)) {
|
||||
Assert.isTrue(requestUri.startsWith(this.contextPath + this.servletPath),
|
||||
() -> "Invalid servlet path [" + this.servletPath + "] for request URI [" + requestUri + "]");
|
||||
String other = requestUri.substring(this.contextPath.length() + this.servletPath.length());
|
||||
path = (StringUtils.hasText(other) ? UrlPathHelper.defaultInstance.decodeRequestString(request, other) : null);
|
||||
}
|
||||
request.setPathInfo(this.pathInfo);
|
||||
request.setPathInfo(path);
|
||||
}
|
||||
|
||||
private void addRequestParams(MockHttpServletRequest request, MultiValueMap<String, String> map) {
|
||||
|
||||
+3
-3
@@ -145,9 +145,9 @@ class ClassLevelDirtiesContextTestNGTests {
|
||||
testNG.setVerbose(0);
|
||||
testNG.run();
|
||||
|
||||
assertThat(listener.testFailureCount).as("Failures for test class [" + testClass + "].").isEqualTo(expectedTestFailureCount);
|
||||
assertThat(listener.testStartCount).as("Tests started for test class [" + testClass + "].").isEqualTo(expectedTestStartedCount);
|
||||
assertThat(listener.testSuccessCount).as("Successful tests for test class [" + testClass + "].").isEqualTo(expectedTestFinishedCount);
|
||||
assertThat(listener.testFailureCount.get()).as("Failures for test class [" + testClass + "].").isEqualTo(expectedTestFailureCount);
|
||||
assertThat(listener.testStartCount.get()).as("Tests started for test class [" + testClass + "].").isEqualTo(expectedTestStartedCount);
|
||||
assertThat(listener.testSuccessCount.get()).as("Successful tests for test class [" + testClass + "].").isEqualTo(expectedTestFinishedCount);
|
||||
}
|
||||
|
||||
private void assertBehaviorForCleanTestCase() {
|
||||
|
||||
+4
-4
@@ -64,10 +64,10 @@ class FailingBeforeAndAfterMethodsTestNGTests {
|
||||
|
||||
String name = clazz.getSimpleName();
|
||||
|
||||
assertThat(listener.testStartCount).as("tests started for [" + name + "] ==> ").isEqualTo(expectedTestStartCount);
|
||||
assertThat(listener.testSuccessCount).as("successful tests for [" + name + "] ==> ").isEqualTo(expectedTestSuccessCount);
|
||||
assertThat(listener.testFailureCount).as("failed tests for [" + name + "] ==> ").isEqualTo(expectedFailureCount);
|
||||
assertThat(listener.failedConfigurationsCount).as("failed configurations for [" + name + "] ==> ").isEqualTo(expectedFailedConfigurationsCount);
|
||||
assertThat(listener.testStartCount.get()).as("tests started for [" + name + "] ==> ").isEqualTo(expectedTestStartCount);
|
||||
assertThat(listener.testSuccessCount.get()).as("successful tests for [" + name + "] ==> ").isEqualTo(expectedTestSuccessCount);
|
||||
assertThat(listener.testFailureCount.get()).as("failed tests for [" + name + "] ==> ").isEqualTo(expectedFailureCount);
|
||||
assertThat(listener.failedConfigurationsCount.get()).as("failed configurations for [" + name + "] ==> ").isEqualTo(expectedFailedConfigurationsCount);
|
||||
}
|
||||
|
||||
static List<Arguments> testData() {
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.context.testng;
|
||||
|
||||
import org.testng.TestNG;
|
||||
import org.testng.annotations.Test;
|
||||
import org.testng.xml.XmlSuite.ParallelMode;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for concurrent TestNG tests.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @since 6.2.12
|
||||
* @see <a href="https://github.com/spring-projects/spring-framework/issues/35528">gh-35528</a>
|
||||
*/
|
||||
class TestNGConcurrencyTests {
|
||||
|
||||
@org.junit.jupiter.api.Test
|
||||
void runTestsInParallel() throws Exception {
|
||||
TrackingTestNGTestListener listener = new TrackingTestNGTestListener();
|
||||
|
||||
TestNG testNG = new TestNG();
|
||||
testNG.addListener(listener);
|
||||
testNG.setTestClasses(new Class<?>[] { ConcurrentTestCase.class });
|
||||
testNG.setParallel(ParallelMode.METHODS);
|
||||
testNG.setThreadCount(5);
|
||||
testNG.setVerbose(0);
|
||||
testNG.run();
|
||||
|
||||
assertThat(listener.testStartCount.get()).as("tests started").isEqualTo(10);
|
||||
assertThat(listener.testSuccessCount.get()).as("successful tests").isEqualTo(10);
|
||||
assertThat(listener.testFailureCount.get()).as("failed tests").isEqualTo(0);
|
||||
assertThat(listener.failedConfigurationsCount.get()).as("failed configurations").isEqualTo(0);
|
||||
assertThat(listener.throwables).isEmpty();
|
||||
}
|
||||
|
||||
|
||||
@ContextConfiguration
|
||||
static class ConcurrentTestCase extends AbstractTestNGSpringContextTests {
|
||||
|
||||
@Test(expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "Message1")
|
||||
public void message1() {
|
||||
throw new RuntimeException("Message1");
|
||||
}
|
||||
|
||||
@Test(expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "Message2")
|
||||
public void message2() {
|
||||
throw new RuntimeException("Message2");
|
||||
}
|
||||
|
||||
@Test(expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "Message3")
|
||||
public void message3() {
|
||||
throw new RuntimeException("Message3");
|
||||
}
|
||||
|
||||
@Test(expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "Message4")
|
||||
public void message4() {
|
||||
throw new RuntimeException("Message4");
|
||||
}
|
||||
|
||||
@Test(expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "Message5")
|
||||
public void message5() {
|
||||
throw new RuntimeException("Message5");
|
||||
}
|
||||
|
||||
@Test(expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "Message6")
|
||||
public void message6() {
|
||||
throw new RuntimeException("Message6");
|
||||
}
|
||||
|
||||
@Test(expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "Message7")
|
||||
public void message7() {
|
||||
throw new RuntimeException("Message7");
|
||||
}
|
||||
|
||||
@Test(expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "Message8")
|
||||
public void message8() {
|
||||
throw new RuntimeException("Message8");
|
||||
}
|
||||
|
||||
@Test(expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "Message9")
|
||||
public void message9() {
|
||||
throw new RuntimeException("Message9");
|
||||
}
|
||||
|
||||
@Test(expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "Message10")
|
||||
public void message10() {
|
||||
throw new RuntimeException("Message10");
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+3
-1
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.test.context.testng;
|
||||
|
||||
import org.junit.platform.suite.api.IncludeClassNamePatterns;
|
||||
import org.junit.platform.suite.api.IncludeEngines;
|
||||
import org.junit.platform.suite.api.SelectPackages;
|
||||
import org.junit.platform.suite.api.Suite;
|
||||
@@ -40,7 +41,8 @@ import org.junit.platform.suite.api.Suite;
|
||||
* @since 5.3.11
|
||||
*/
|
||||
@Suite
|
||||
@IncludeEngines("testng")
|
||||
@IncludeEngines({"testng", "junit-jupiter"})
|
||||
@SelectPackages("org.springframework.test.context.testng")
|
||||
@IncludeClassNamePatterns(".*Tests?$")
|
||||
class TestNGTestSuite {
|
||||
}
|
||||
|
||||
+19
-8
@@ -16,6 +16,10 @@
|
||||
|
||||
package org.springframework.test.context.testng;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.testng.ITestContext;
|
||||
import org.testng.ITestListener;
|
||||
import org.testng.ITestResult;
|
||||
@@ -29,18 +33,20 @@ import org.testng.ITestResult;
|
||||
*/
|
||||
public class TrackingTestNGTestListener implements ITestListener {
|
||||
|
||||
public int testStartCount = 0;
|
||||
public final List<Throwable> throwables = new ArrayList<>();
|
||||
|
||||
public int testSuccessCount = 0;
|
||||
public final AtomicInteger testStartCount = new AtomicInteger();
|
||||
|
||||
public int testFailureCount = 0;
|
||||
public final AtomicInteger testSuccessCount = new AtomicInteger();
|
||||
|
||||
public int failedConfigurationsCount = 0;
|
||||
public final AtomicInteger testFailureCount = new AtomicInteger();
|
||||
|
||||
public final AtomicInteger failedConfigurationsCount = new AtomicInteger();
|
||||
|
||||
|
||||
@Override
|
||||
public void onFinish(ITestContext testContext) {
|
||||
this.failedConfigurationsCount += testContext.getFailedConfigurations().size();
|
||||
this.failedConfigurationsCount.addAndGet(testContext.getFailedConfigurations().size());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -53,7 +59,12 @@ public class TrackingTestNGTestListener implements ITestListener {
|
||||
|
||||
@Override
|
||||
public void onTestFailure(ITestResult testResult) {
|
||||
this.testFailureCount++;
|
||||
this.testFailureCount.incrementAndGet();
|
||||
|
||||
Throwable throwable = testResult.getThrowable();
|
||||
if (throwable != null) {
|
||||
this.throwables.add(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -62,12 +73,12 @@ public class TrackingTestNGTestListener implements ITestListener {
|
||||
|
||||
@Override
|
||||
public void onTestStart(ITestResult testResult) {
|
||||
this.testStartCount++;
|
||||
this.testStartCount.incrementAndGet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTestSuccess(ITestResult testResult) {
|
||||
this.testSuccessCount++;
|
||||
this.testSuccessCount.incrementAndGet();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+9
@@ -31,6 +31,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
* Tests for {@link AbstractMockHttpServletRequestBuilder}
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Réda Housni Alaoui
|
||||
*/
|
||||
class AbstractMockHttpServletRequestBuilderTests {
|
||||
|
||||
@@ -97,6 +98,14 @@ class AbstractMockHttpServletRequestBuilderTests {
|
||||
}
|
||||
|
||||
|
||||
@Test // gh-35493
|
||||
void pathInfoIsNotMutatedByBuildMethod() {
|
||||
TestRequestBuilder builder = new TestRequestBuilder(HttpMethod.GET).uri("/b");
|
||||
assertThat(buildRequest(builder).getPathInfo()).isEqualTo("/b");
|
||||
builder.uri("/a");
|
||||
assertThat(buildRequest(builder).getPathInfo()).isEqualTo("/a");
|
||||
}
|
||||
|
||||
private MockHttpServletRequest buildRequest(AbstractMockHttpServletRequestBuilder<?> builder) {
|
||||
return builder.buildRequest(this.servletContext);
|
||||
}
|
||||
|
||||
+42
-25
@@ -51,12 +51,14 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
|
||||
/**
|
||||
* {@code HttpMessageWriter} that can write a {@link Resource}.
|
||||
* {@code HttpMessageWriter} that can write a {@link Resource} from both client
|
||||
* and server perspectives.
|
||||
*
|
||||
* <p>Also an implementation of {@code HttpMessageWriter} with support for writing one
|
||||
* or more {@link ResourceRegion}'s based on the HTTP ranges specified in the request.
|
||||
* <p>From a server perspective, the server-side only write method supports
|
||||
* writing one or more {@link ResourceRegion}'s based on HTTP ranges specified
|
||||
* in the request.
|
||||
*
|
||||
* <p>For reading to a Resource, use {@link ResourceDecoder} wrapped with
|
||||
* <p>To read a Resource, use {@link ResourceDecoder} wrapped with
|
||||
* {@link DecoderHttpMessageReader}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
@@ -122,16 +124,19 @@ public class ResourceHttpMessageWriter implements HttpMessageWriter<Resource> {
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
else {
|
||||
Mono<Resource> input = Mono.just(resource);
|
||||
DataBufferFactory factory = message.bufferFactory();
|
||||
Flux<DataBuffer> body = this.encoder.encode(input, factory, type, message.getHeaders().getContentType(), hints)
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
if (logger.isDebugEnabled()) {
|
||||
body = body.doOnNext(buffer -> Hints.touchDataBuffer(buffer, hints, logger));
|
||||
}
|
||||
return message.writeWith(body);
|
||||
|
||||
Mono<Resource> input = Mono.just(resource);
|
||||
DataBufferFactory factory = message.bufferFactory();
|
||||
MediaType contentType = message.getHeaders().getContentType();
|
||||
|
||||
Flux<DataBuffer> body = this.encoder.encode(input, factory, type, contentType, hints)
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
body = body.doOnNext(buffer -> Hints.touchDataBuffer(buffer, hints, logger));
|
||||
}
|
||||
|
||||
return message.writeWith(body);
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -139,7 +144,10 @@ public class ResourceHttpMessageWriter implements HttpMessageWriter<Resource> {
|
||||
* Adds the default headers for the given resource to the given message.
|
||||
* @since 6.1
|
||||
*/
|
||||
public Mono<Void> addDefaultHeaders(ReactiveHttpOutputMessage message, Resource resource, @Nullable MediaType contentType, Map<String, Object> hints) {
|
||||
public Mono<Void> addDefaultHeaders(
|
||||
ReactiveHttpOutputMessage message, Resource resource, @Nullable MediaType contentType,
|
||||
Map<String, Object> hints) {
|
||||
|
||||
return Mono.defer(() -> {
|
||||
HttpHeaders headers = message.getHeaders();
|
||||
MediaType resourceMediaType = getResourceMediaType(contentType, resource, hints);
|
||||
@@ -149,16 +157,15 @@ public class ResourceHttpMessageWriter implements HttpMessageWriter<Resource> {
|
||||
headers.set(HttpHeaders.ACCEPT_RANGES, "bytes");
|
||||
}
|
||||
|
||||
if (headers.getContentLength() < 0) {
|
||||
return lengthOf(resource)
|
||||
.flatMap(contentLength -> {
|
||||
headers.setContentLength(contentLength);
|
||||
return Mono.empty();
|
||||
});
|
||||
}
|
||||
else {
|
||||
if (headers.getContentLength() >= 0) {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
return lengthOf(resource)
|
||||
.flatMap(contentLength -> {
|
||||
headers.setContentLength(contentLength);
|
||||
return Mono.empty();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -226,8 +233,7 @@ public class ResourceHttpMessageWriter implements HttpMessageWriter<Resource> {
|
||||
ranges = request.getHeaders().getRange();
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
response.setStatusCode(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
return response.setComplete();
|
||||
return handleInvalidRange(response);
|
||||
}
|
||||
|
||||
return Mono.from(inputStream).flatMap(resource -> {
|
||||
@@ -235,7 +241,13 @@ public class ResourceHttpMessageWriter implements HttpMessageWriter<Resource> {
|
||||
return writeResource(resource, elementType, mediaType, response, hints);
|
||||
}
|
||||
response.setStatusCode(HttpStatus.PARTIAL_CONTENT);
|
||||
List<ResourceRegion> regions = HttpRange.toResourceRegions(ranges, resource);
|
||||
List<ResourceRegion> regions;
|
||||
try {
|
||||
regions = HttpRange.toResourceRegions(ranges, resource);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
return handleInvalidRange(response);
|
||||
}
|
||||
MediaType resourceMediaType = getResourceMediaType(mediaType, resource, hints);
|
||||
if (regions.size() == 1){
|
||||
ResourceRegion region = regions.get(0);
|
||||
@@ -261,6 +273,11 @@ public class ResourceHttpMessageWriter implements HttpMessageWriter<Resource> {
|
||||
});
|
||||
}
|
||||
|
||||
private static Mono<Void> handleInvalidRange(ServerHttpResponse response) {
|
||||
response.setStatusCode(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
return response.setComplete();
|
||||
}
|
||||
|
||||
private Mono<Void> writeSingleRegion(ResourceRegion region, ReactiveHttpOutputMessage message,
|
||||
Map<String, Object> hints) {
|
||||
|
||||
|
||||
+3
-1
@@ -220,7 +220,9 @@ public class ServletServerHttpRequest implements ServerHttpRequest {
|
||||
|
||||
@Override
|
||||
public InetSocketAddress getRemoteAddress() {
|
||||
return new InetSocketAddress(this.servletRequest.getRemoteHost(), this.servletRequest.getRemotePort());
|
||||
String addressOrHost = this.servletRequest.getRemoteAddr();
|
||||
addressOrHost = (addressOrHost != null ? addressOrHost : this.servletRequest.getRemoteHost());
|
||||
return new InetSocketAddress(addressOrHost, this.servletRequest.getRemotePort());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
-1
@@ -44,7 +44,6 @@ public class SpringWebConstraintValidatorFactory implements ConstraintValidatorF
|
||||
return getWebApplicationContext().getAutowireCapableBeanFactory().createBean(key);
|
||||
}
|
||||
|
||||
// Bean Validation 1.1 releaseInstance method
|
||||
@Override
|
||||
public void releaseInstance(ConstraintValidator<?, ?> instance) {
|
||||
getWebApplicationContext().getAutowireCapableBeanFactory().destroyBean(instance);
|
||||
|
||||
+30
-23
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.web.client;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.PushbackInputStream;
|
||||
@@ -33,7 +34,6 @@ import org.springframework.lang.Nullable;
|
||||
* @author Brian Clozel
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.1.5
|
||||
* @see <a href="https://tools.ietf.org/html/rfc7230#section-3.3.3">RFC 7230 Section 3.3.3</a>
|
||||
*/
|
||||
class IntrospectingClientHttpResponse extends ClientHttpResponseDecorator {
|
||||
|
||||
@@ -47,14 +47,18 @@ class IntrospectingClientHttpResponse extends ClientHttpResponseDecorator {
|
||||
|
||||
|
||||
/**
|
||||
* Indicates whether the response has a message body.
|
||||
* Indicates whether the response might have a message body.
|
||||
* <p>Implementation returns {@code false} for:
|
||||
* <ul>
|
||||
* <li>a response status of {@code 1XX}, {@code 204} or {@code 304}</li>
|
||||
* <li>a {@code Content-Length} header of {@code 0}</li>
|
||||
* </ul>
|
||||
* @return {@code true} if the response has a message body, {@code false} otherwise
|
||||
* <p>In other cases, the server could use a {@code Transfer-Encoding} header or just
|
||||
* write the body and close the response. Reading the message body is then the only way
|
||||
* to check for the presence of a body.
|
||||
* @return {@code true} if the response might have a message body, {@code false} otherwise
|
||||
* @throws IOException in case of I/O errors
|
||||
* @see <a href="https://tools.ietf.org/html/rfc7230#section-3.3.3">RFC 7230 Section 3.3.3</a>
|
||||
*/
|
||||
public boolean hasMessageBody() throws IOException {
|
||||
HttpStatusCode statusCode = getStatusCode();
|
||||
@@ -62,10 +66,7 @@ class IntrospectingClientHttpResponse extends ClientHttpResponseDecorator {
|
||||
statusCode == HttpStatus.NOT_MODIFIED) {
|
||||
return false;
|
||||
}
|
||||
if (getHeaders().getContentLength() == 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return getHeaders().getContentLength() != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,6 +74,7 @@ class IntrospectingClientHttpResponse extends ClientHttpResponseDecorator {
|
||||
* <p>Implementation tries to read the first bytes of the response stream:
|
||||
* <ul>
|
||||
* <li>if no bytes are available, the message body is empty</li>
|
||||
* <li>if an {@link EOFException} is thrown, the body is considered empty</li>
|
||||
* <li>otherwise it is not empty and the stream is reset to its start for further reading</li>
|
||||
* </ul>
|
||||
* @return {@code true} if the response has a zero-length message body, {@code false} otherwise
|
||||
@@ -85,26 +87,31 @@ class IntrospectingClientHttpResponse extends ClientHttpResponseDecorator {
|
||||
if (body == null) {
|
||||
return true;
|
||||
}
|
||||
if (body.markSupported()) {
|
||||
body.mark(1);
|
||||
if (body.read() == -1) {
|
||||
return true;
|
||||
try {
|
||||
if (body.markSupported()) {
|
||||
body.mark(1);
|
||||
if (body.read() == -1) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
body.reset();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
body.reset();
|
||||
return false;
|
||||
this.pushbackInputStream = new PushbackInputStream(body);
|
||||
int b = this.pushbackInputStream.read();
|
||||
if (b == -1) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
this.pushbackInputStream.unread(b);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.pushbackInputStream = new PushbackInputStream(body);
|
||||
int b = this.pushbackInputStream.read();
|
||||
if (b == -1) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
this.pushbackInputStream.unread(b);
|
||||
return false;
|
||||
}
|
||||
catch (EOFException exc) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -43,8 +44,8 @@ import org.springframework.web.util.pattern.PathPattern;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
/**
|
||||
* {@link jakarta.servlet.Filter} that modifies the URL, and then redirects or
|
||||
* wraps the request to apply the change.
|
||||
* {@link jakarta.servlet.Filter} that modifies the URL, and then either
|
||||
* redirects or wraps the request to effect the change.
|
||||
*
|
||||
* <p>To create an instance, you can use the following:
|
||||
*
|
||||
@@ -55,8 +56,8 @@ import org.springframework.web.util.pattern.PathPatternParser;
|
||||
* .build();
|
||||
* </pre>
|
||||
*
|
||||
* <p>This {@code Filter} should be ordered after {@link ForwardedHeaderFilter}
|
||||
* and before any security filters.
|
||||
* <p>This {@code Filter} should be ordered after {@link ForwardedHeaderFilter},
|
||||
* before {@link ServletRequestPathFilter}, and before security filters.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 6.2
|
||||
@@ -74,41 +75,23 @@ public final class UrlHandlerFilter extends OncePerRequestFilter {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilterAsyncDispatch() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilterErrorDispatch() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
RequestPath previousPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE);
|
||||
RequestPath path = previousPath;
|
||||
try {
|
||||
if (path == null) {
|
||||
path = ServletRequestPathUtils.parseAndCache(request);
|
||||
RequestPath path = (ServletRequestPathUtils.hasParsedRequestPath(request) ?
|
||||
ServletRequestPathUtils.getParsedRequestPath(request) :
|
||||
ServletRequestPathUtils.parse(request));
|
||||
|
||||
for (Map.Entry<Handler, List<PathPattern>> entry : this.handlers.entrySet()) {
|
||||
if (!entry.getKey().supports(request, path)) {
|
||||
continue;
|
||||
}
|
||||
for (Map.Entry<Handler, List<PathPattern>> entry : this.handlers.entrySet()) {
|
||||
if (!entry.getKey().supports(request, path)) {
|
||||
continue;
|
||||
for (PathPattern pattern : entry.getValue()) {
|
||||
if (pattern.matches(path)) {
|
||||
entry.getKey().handle(request, response, chain);
|
||||
return;
|
||||
}
|
||||
for (PathPattern pattern : entry.getValue()) {
|
||||
if (pattern.matches(path)) {
|
||||
entry.getKey().handle(request, response, chain);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (previousPath != null) {
|
||||
ServletRequestPathUtils.setParsedRequestPath(previousPath, request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,7 +332,16 @@ public final class UrlHandlerFilter extends OncePerRequestFilter {
|
||||
hasPathInfo ? servletPath : trimTrailingSlash(servletPath),
|
||||
hasPathInfo ? trimTrailingSlash(pathInfo) : pathInfo);
|
||||
|
||||
chain.doFilter(request, response);
|
||||
RequestPath previousPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE);
|
||||
ServletRequestPathUtils.clearParsedRequestPath(request);
|
||||
try {
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
finally {
|
||||
if (previousPath != null) {
|
||||
ServletRequestPathUtils.setParsedRequestPath(previousPath, request);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,22 +371,30 @@ public final class UrlHandlerFilter extends OncePerRequestFilter {
|
||||
|
||||
@Override
|
||||
public String getRequestURI() {
|
||||
return this.requestURI;
|
||||
return (isForward() ? getDelegate().getRequestURI() : this.requestURI);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StringBuffer getRequestURL() {
|
||||
return this.requestURL;
|
||||
return (isForward() ? getDelegate().getRequestURL() : this.requestURL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServletPath() {
|
||||
return this.servletPath;
|
||||
return (isForward() ? getDelegate().getServletPath() : this.servletPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPathInfo() {
|
||||
return this.pathInfo;
|
||||
return (isForward() ? getDelegate().getPathInfo() : this.pathInfo);
|
||||
}
|
||||
|
||||
private boolean isForward() {
|
||||
return (getDispatcherType() == DispatcherType.FORWARD);
|
||||
}
|
||||
|
||||
private HttpServletRequest getDelegate() {
|
||||
return (HttpServletRequest) getRequest();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -21,7 +21,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -121,7 +121,7 @@ public class ExceptionHandlerMethodResolver {
|
||||
if (exceptions.isEmpty()) {
|
||||
throw new IllegalStateException("No exception types mapped to " + method);
|
||||
}
|
||||
Set<MediaType> mediaTypes = new HashSet<>();
|
||||
Set<MediaType> mediaTypes = new LinkedHashSet<>();
|
||||
for (String mediaType : exceptionHandler.produces()) {
|
||||
try {
|
||||
mediaTypes.add(MediaType.parseMediaType(mediaType));
|
||||
|
||||
@@ -52,16 +52,19 @@ public abstract class ServletRequestPathUtils {
|
||||
|
||||
/**
|
||||
* Parse the {@link HttpServletRequest#getRequestURI() requestURI} to a
|
||||
* {@link RequestPath} and save it in the request attribute
|
||||
* {@link #PATH_ATTRIBUTE} for subsequent use with
|
||||
* {@link org.springframework.web.util.pattern.PathPattern parsed patterns}.
|
||||
* {@link RequestPath}.
|
||||
* <p>The returned {@code RequestPath} will have both the contextPath and any
|
||||
* servletPath prefix omitted from the {@link RequestPath#pathWithinApplication()
|
||||
* pathWithinApplication} it exposes.
|
||||
* <p>This method is typically called by the {@code DispatcherServlet} to determine
|
||||
* if any {@code HandlerMapping} indicates that it uses parsed patterns.
|
||||
* After that the pre-parsed and cached {@code RequestPath} can be accessed
|
||||
* through {@link #getParsedRequestPath(ServletRequest)}.
|
||||
* @since 6.2.12
|
||||
*/
|
||||
public static RequestPath parse(HttpServletRequest request) {
|
||||
return ServletRequestPath.parse(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Variant of {@link #parse(HttpServletRequest)} that also saves the parsed
|
||||
* path in the request attribute {@link #PATH_ATTRIBUTE}.
|
||||
*/
|
||||
public static RequestPath parseAndCache(HttpServletRequest request) {
|
||||
RequestPath requestPath = ServletRequestPath.parse(request);
|
||||
|
||||
+9
@@ -156,6 +156,15 @@ class ResourceHttpMessageWriterTests {
|
||||
assertThat(this.response.getStatusCode()).isEqualTo(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
}
|
||||
|
||||
@Test // gh-35536
|
||||
void invalidRangePosition() {
|
||||
|
||||
testWrite(get("/").header(HttpHeaders.RANGE, "bytes=2000-5000").build());
|
||||
|
||||
assertThat(this.response.getHeaders().getFirst(HttpHeaders.ACCEPT_RANGES)).isEqualTo("bytes");
|
||||
assertThat(this.response.getStatusCode()).isEqualTo(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
}
|
||||
|
||||
|
||||
private void testWrite(MockServerHttpRequest request) {
|
||||
Mono<Void> mono = this.writer.write(this.input, null, null, TEXT_PLAIN, request, this.response, HINTS);
|
||||
|
||||
+47
-10
@@ -17,11 +17,18 @@
|
||||
package org.springframework.web.client;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.EOFException;
|
||||
import java.io.InputStream;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.web.testfixture.http.client.MockClientHttpResponse;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
@@ -30,27 +37,57 @@ import static org.mockito.Mockito.mock;
|
||||
/**
|
||||
* Tests for {@link IntrospectingClientHttpResponse}.
|
||||
*
|
||||
* @since 5.3.10
|
||||
* @author Yin-Jui Liao
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
class IntrospectingClientHttpResponseTests {
|
||||
|
||||
private final ClientHttpResponse response = mock();
|
||||
|
||||
private final IntrospectingClientHttpResponse wrappedResponse = new IntrospectingClientHttpResponse(response);
|
||||
@ParameterizedTest
|
||||
@MethodSource("noBodyHttpStatus")
|
||||
void noMessageBodyWhenStatus(HttpStatus status) throws Exception {
|
||||
var response = new MockClientHttpResponse(new byte[0], status);
|
||||
var wrapped = new IntrospectingClientHttpResponse(response);
|
||||
|
||||
assertThat(wrapped.hasMessageBody()).isFalse();
|
||||
}
|
||||
|
||||
static Stream<HttpStatusCode> noBodyHttpStatus() {
|
||||
return Stream.of(HttpStatus.NO_CONTENT, HttpStatus.EARLY_HINTS, HttpStatus.NOT_MODIFIED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void messageBodyDoesNotExist() throws Exception {
|
||||
given(response.getBody()).willReturn(null);
|
||||
assertThat(wrappedResponse.hasEmptyMessageBody()).isTrue();
|
||||
void noMessageBodyWhenContentLength0() throws Exception {
|
||||
var response = new MockClientHttpResponse(new byte[0], HttpStatus.OK);
|
||||
response.getHeaders().setContentLength(0);
|
||||
var wrapped = new IntrospectingClientHttpResponse(response);
|
||||
|
||||
assertThat(wrapped.hasMessageBody()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyMessageWhenNullInputStream() throws Exception {
|
||||
ClientHttpResponse mockResponse = mock();
|
||||
given(mockResponse.getBody()).willReturn(null);
|
||||
var wrappedMock = new IntrospectingClientHttpResponse(mockResponse);
|
||||
assertThat(wrappedMock.hasEmptyMessageBody()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void messageBodyExists() throws Exception {
|
||||
InputStream stream = new ByteArrayInputStream("content".getBytes());
|
||||
given(response.getBody()).willReturn(stream);
|
||||
assertThat(wrappedResponse.hasEmptyMessageBody()).isFalse();
|
||||
var stream = new ByteArrayInputStream("content".getBytes());
|
||||
var response = new MockClientHttpResponse(stream, HttpStatus.OK);
|
||||
var wrapped = new IntrospectingClientHttpResponse(response);
|
||||
assertThat(wrapped.hasEmptyMessageBody()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyMessageWhenEOFException() throws Exception {
|
||||
ClientHttpResponse mockResponse = mock();
|
||||
InputStream stream = mock();
|
||||
given(mockResponse.getBody()).willReturn(stream);
|
||||
given(stream.read()).willThrow(new EOFException());
|
||||
var wrappedMock = new IntrospectingClientHttpResponse(mockResponse);
|
||||
assertThat(wrappedMock.hasEmptyMessageBody()).isTrue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,8 +18,11 @@ package org.springframework.web.filter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@@ -29,6 +32,7 @@ import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.testfixture.servlet.MockFilterChain;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
|
||||
import org.springframework.web.util.ServletRequestPathUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -124,4 +128,88 @@ public class UrlHandlerFilterTests {
|
||||
assertThat(response.isCommitted()).isFalse();
|
||||
}
|
||||
|
||||
@Test // gh-35538
|
||||
void shouldNotFilterErrorAndAsyncDispatches() {
|
||||
UrlHandlerFilter filter = UrlHandlerFilter.trailingSlashHandler("/path/**").wrapRequest().build();
|
||||
|
||||
assertThat(filter.shouldNotFilterAsyncDispatch())
|
||||
.as("Should not filter ASYNC dispatch as wrapped request is reused")
|
||||
.isTrue();
|
||||
|
||||
assertThat(filter.shouldNotFilterErrorDispatch())
|
||||
.as("Should not filter ERROR dispatch as it's an internal, fixed path")
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test // gh-35538
|
||||
void shouldNotCacheParsedPath() throws Exception {
|
||||
UrlHandlerFilter filter = UrlHandlerFilter.trailingSlashHandler("/path/*").wrapRequest().build();
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path/123/");
|
||||
request.setServletPath("/path/123/");
|
||||
|
||||
MockFilterChain chain = new MockFilterChain();
|
||||
filter.doFilterInternal(request, new MockHttpServletResponse(), chain);
|
||||
|
||||
assertThat(ServletRequestPathUtils.hasParsedRequestPath(request))
|
||||
.as("Path with trailing slash should not be cached")
|
||||
.isFalse();
|
||||
}
|
||||
|
||||
@Test // gh-35538
|
||||
void shouldClearPreviouslyCachedPath() throws Exception {
|
||||
UrlHandlerFilter filter = UrlHandlerFilter.trailingSlashHandler("/path/*").wrapRequest().build();
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path/123/");
|
||||
request.setServletPath("/path/123/");
|
||||
|
||||
ServletRequestPathUtils.parseAndCache(request);
|
||||
assertThat(ServletRequestPathUtils.getParsedRequestPath(request).value()).isEqualTo("/path/123/");
|
||||
|
||||
PathServlet servlet = new PathServlet();
|
||||
MockFilterChain chain = new MockFilterChain(servlet);
|
||||
filter.doFilterInternal(request, new MockHttpServletResponse(), chain);
|
||||
|
||||
assertThat(servlet.getParsedPath()).isNull();
|
||||
}
|
||||
|
||||
@Test // gh-35509
|
||||
void shouldRespectForwardedPath() throws Exception {
|
||||
UrlHandlerFilter filter = UrlHandlerFilter.trailingSlashHandler("/requestURI/*").wrapRequest().build();
|
||||
|
||||
String requestURI = "/requestURI/123/";
|
||||
MockHttpServletRequest originalRequest = new MockHttpServletRequest("GET", requestURI);
|
||||
originalRequest.setServletPath(requestURI);
|
||||
|
||||
MockFilterChain chain = new MockFilterChain();
|
||||
filter.doFilterInternal(originalRequest, new MockHttpServletResponse(), chain);
|
||||
|
||||
HttpServletRequest wrapped = (HttpServletRequest) chain.getRequest();
|
||||
assertThat(wrapped).isNotNull().isNotSameAs(originalRequest);
|
||||
assertThat(wrapped.getRequestURI()).isEqualTo("/requestURI/123");
|
||||
|
||||
// Change dispatcher type of underlying requests
|
||||
originalRequest.setDispatcherType(DispatcherType.FORWARD);
|
||||
assertThat(wrapped.getRequestURI())
|
||||
.as("Should delegate to underlying request for the requestURI on FORWARD")
|
||||
.isEqualTo(requestURI);
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private static class PathServlet extends HttpServlet {
|
||||
|
||||
private String parsedPath;
|
||||
|
||||
public String getParsedPath() {
|
||||
return parsedPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
|
||||
this.parsedPath = (ServletRequestPathUtils.hasParsedRequestPath(request) ?
|
||||
ServletRequestPathUtils.getParsedRequestPath(request).value() : null);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+16
-8
@@ -20,6 +20,7 @@ import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.BindException;
|
||||
import java.net.SocketException;
|
||||
import java.util.Set;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@@ -100,14 +101,21 @@ class ExceptionHandlerMethodResolverTests {
|
||||
|
||||
@Test
|
||||
void shouldThrowExceptionWhenAmbiguousExceptionMapping() {
|
||||
assertThatIllegalStateException().isThrownBy(() ->
|
||||
new ExceptionHandlerMethodResolver(AmbiguousController.class));
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> new ExceptionHandlerMethodResolver(AmbiguousController.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowExceptionWhenNoExceptionMapping() {
|
||||
assertThatIllegalStateException().isThrownBy(() ->
|
||||
new ExceptionHandlerMethodResolver(NoExceptionController.class));
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> new ExceptionHandlerMethodResolver(NoExceptionController.class));
|
||||
}
|
||||
|
||||
@Test // gh-35587
|
||||
void shouldRetainOriginalOrderOfProducibleMediaTypes() {
|
||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(MediaTypeController.class);
|
||||
Set<MediaType> producibleTypes = resolver.resolveExceptionMapping(new IllegalArgumentException(), MediaType.TEXT_HTML).getProducibleTypes();
|
||||
assertThat(MediaType.toString(producibleTypes)).isEqualTo("text/html, */*");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -131,15 +139,15 @@ class ExceptionHandlerMethodResolverTests {
|
||||
|
||||
@Test
|
||||
void shouldThrowExceptionWhenInvalidMediaTypeMapping() {
|
||||
assertThatIllegalStateException().isThrownBy(() ->
|
||||
new ExceptionHandlerMethodResolver(InvalidMediaTypeController.class))
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> new ExceptionHandlerMethodResolver(InvalidMediaTypeController.class))
|
||||
.withMessageContaining("Invalid media type [invalid-mediatype] declared on @ExceptionHandler");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowExceptionWhenAmbiguousMediaTypeMapping() {
|
||||
assertThatIllegalStateException().isThrownBy(() ->
|
||||
new ExceptionHandlerMethodResolver(AmbiguousMediaTypeController.class))
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> new ExceptionHandlerMethodResolver(AmbiguousMediaTypeController.class))
|
||||
.withMessageContaining("Ambiguous @ExceptionHandler method mapped for [ExceptionHandler{exceptionType=java.lang.IllegalArgumentException, mediaType=application/json}]")
|
||||
.withMessageContaining("AmbiguousMediaTypeController.handleJson()")
|
||||
.withMessageContaining("AmbiguousMediaTypeController.handleJsonToo()");
|
||||
|
||||
+3
-1
@@ -67,7 +67,9 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
|
||||
ClassLoader loader = DefaultWebClientBuilder.class.getClassLoader();
|
||||
reactorNettyClientPresent = ClassUtils.isPresent("reactor.netty.http.client.HttpClient", loader);
|
||||
reactorNetty2ClientPresent = ClassUtils.isPresent("reactor.netty5.http.client.HttpClient", loader);
|
||||
jettyClientPresent = ClassUtils.isPresent("org.eclipse.jetty.client.HttpClient", loader);
|
||||
jettyClientPresent =
|
||||
ClassUtils.isPresent("org.eclipse.jetty.client.HttpClient", loader) &&
|
||||
ClassUtils.isPresent("org.eclipse.jetty.reactive.client.ReactiveRequest", loader);
|
||||
httpComponentsClientPresent =
|
||||
ClassUtils.isPresent("org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient", loader) &&
|
||||
ClassUtils.isPresent("org.apache.hc.core5.reactive.ReactiveDataConsumer", loader);
|
||||
|
||||
+2
-2
@@ -63,7 +63,7 @@ import reactor.core.publisher.Mono;
|
||||
* </pre>
|
||||
*
|
||||
* <p>If processing inbound and sending outbound messages are independent
|
||||
* streams, they can be joined together with the "zip" operator:
|
||||
* streams, they can be joined together with the "and" operator:
|
||||
*
|
||||
* <pre class="code">
|
||||
* class ExampleHandler implements WebSocketHandler {
|
||||
@@ -83,7 +83,7 @@ import reactor.core.publisher.Mono;
|
||||
* Flux<String> source = ... ;
|
||||
* Mono<Void> output = session.send(source.map(session::textMessage));
|
||||
*
|
||||
* return Mono.zip(input, output).then();
|
||||
* return input.and(output);
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
|
||||
+5
-4
@@ -94,6 +94,7 @@ public class ResponseBodyEmitter {
|
||||
/** Guards access to write operations on the response. */
|
||||
protected final Lock writeLock = new ReentrantLock();
|
||||
|
||||
|
||||
/**
|
||||
* Create a new ResponseBodyEmitter instance.
|
||||
*/
|
||||
@@ -201,10 +202,10 @@ public class ResponseBodyEmitter {
|
||||
* @throws java.lang.IllegalStateException wraps any other errors
|
||||
*/
|
||||
public void send(Object object, @Nullable MediaType mediaType) throws IOException {
|
||||
Assert.state(!this.complete, () -> "ResponseBodyEmitter has already completed" +
|
||||
(this.failure != null ? " with error: " + this.failure : ""));
|
||||
this.writeLock.lock();
|
||||
try {
|
||||
Assert.state(!this.complete, () -> "ResponseBodyEmitter has already completed" +
|
||||
(this.failure != null ? " with error: " + this.failure : ""));
|
||||
if (this.handler != null) {
|
||||
try {
|
||||
this.handler.send(object, mediaType);
|
||||
@@ -235,10 +236,10 @@ public class ResponseBodyEmitter {
|
||||
* @since 6.0.12
|
||||
*/
|
||||
public void send(Set<DataWithMediaType> items) throws IOException {
|
||||
Assert.state(!this.complete, () -> "ResponseBodyEmitter has already completed" +
|
||||
(this.failure != null ? " with error: " + this.failure : ""));
|
||||
this.writeLock.lock();
|
||||
try {
|
||||
Assert.state(!this.complete, () -> "ResponseBodyEmitter has already completed" +
|
||||
(this.failure != null ? " with error: " + this.failure : ""));
|
||||
sendInternal(items);
|
||||
}
|
||||
finally {
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
* provide enough information to decide via {@link #supportsReturnType}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.0
|
||||
* @since 6.2.9
|
||||
*/
|
||||
public class ResponseEntityReturnValueHandler implements HandlerMethodReturnValueHandler {
|
||||
|
||||
|
||||
+1
@@ -44,6 +44,7 @@ public class SseEmitter extends ResponseBodyEmitter {
|
||||
|
||||
private static final MediaType TEXT_PLAIN = new MediaType("text", "plain", StandardCharsets.UTF_8);
|
||||
|
||||
|
||||
/**
|
||||
* Create a new SseEmitter instance.
|
||||
*/
|
||||
|
||||
@@ -431,11 +431,10 @@ public class XsltView extends AbstractUrlBasedView {
|
||||
private Templates loadTemplates() throws ApplicationContextException {
|
||||
Source stylesheetSource = getStylesheetSource();
|
||||
try {
|
||||
Templates templates = getTransformerFactory().newTemplates(stylesheetSource);
|
||||
return templates;
|
||||
return getTransformerFactory().newTemplates(stylesheetSource);
|
||||
}
|
||||
catch (TransformerConfigurationException ex) {
|
||||
throw new ApplicationContextException("Can't load stylesheet from '" + getUrl() + "'", ex);
|
||||
throw new ApplicationContextException("Cannot load stylesheet from '" + getUrl() + "'", ex);
|
||||
}
|
||||
finally {
|
||||
closeSourceIfNecessary(stylesheetSource);
|
||||
@@ -474,7 +473,7 @@ public class XsltView extends AbstractUrlBasedView {
|
||||
return new StreamSource(resource.getInputStream(), resource.getURI().toASCIIString());
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new ApplicationContextException("Can't load XSLT stylesheet from '" + url + "'", ex);
|
||||
throw new ApplicationContextException("Cannot load XSLT stylesheet from '" + url + "'", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+69
-33
@@ -25,6 +25,7 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -108,10 +109,9 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
|
||||
@Nullable
|
||||
private MessageHeaderInitializer headerInitializer;
|
||||
|
||||
@Nullable
|
||||
private Map<String, MessageChannel> orderedHandlingMessageChannels;
|
||||
private final Map<String, SessionInfo> sessions = new ConcurrentHashMap<>();
|
||||
|
||||
private final Map<String, Principal> stompAuthentications = new ConcurrentHashMap<>();
|
||||
private boolean preserveReceiveOrder;
|
||||
|
||||
@Nullable
|
||||
private Boolean immutableMessageInterceptorPresent;
|
||||
@@ -208,7 +208,7 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
|
||||
* @since 6.1
|
||||
*/
|
||||
public void setPreserveReceiveOrder(boolean preserveReceiveOrder) {
|
||||
this.orderedHandlingMessageChannels = (preserveReceiveOrder ? new ConcurrentHashMap<>() : null);
|
||||
this.preserveReceiveOrder = preserveReceiveOrder;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -217,7 +217,7 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
|
||||
* @since 6.1
|
||||
*/
|
||||
public boolean isPreserveReceiveOrder() {
|
||||
return (this.orderedHandlingMessageChannels != null);
|
||||
return this.preserveReceiveOrder;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -252,7 +252,7 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
|
||||
*/
|
||||
@Override
|
||||
public void handleMessageFromClient(WebSocketSession session,
|
||||
WebSocketMessage<?> webSocketMessage, MessageChannel targetChannel) {
|
||||
WebSocketMessage<?> webSocketMessage, MessageChannel channel) {
|
||||
|
||||
List<Message<byte[]>> messages;
|
||||
try {
|
||||
@@ -295,35 +295,36 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
|
||||
return;
|
||||
}
|
||||
|
||||
MessageChannel channelToUse = targetChannel;
|
||||
if (this.orderedHandlingMessageChannels != null) {
|
||||
channelToUse = this.orderedHandlingMessageChannels.computeIfAbsent(
|
||||
session.getId(), id -> new OrderedMessageChannelDecorator(targetChannel, logger));
|
||||
}
|
||||
SessionInfo info = this.sessions.get(session.getId());
|
||||
MessageChannel channelToUse = (info != null ? info.getMessageChannelToUse() : null);
|
||||
|
||||
for (Message<byte[]> message : messages) {
|
||||
StompHeaderAccessor headerAccessor =
|
||||
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||
StompHeaderAccessor headerAccessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||
Assert.state(headerAccessor != null, "No StompHeaderAccessor");
|
||||
|
||||
StompCommand command = headerAccessor.getCommand();
|
||||
boolean isConnect = StompCommand.CONNECT.equals(command) || StompCommand.STOMP.equals(command);
|
||||
|
||||
boolean isConnect = (StompCommand.CONNECT.equals(command) || StompCommand.STOMP.equals(command));
|
||||
String sessionId = session.getId();
|
||||
boolean sent = false;
|
||||
try {
|
||||
|
||||
headerAccessor.setSessionId(session.getId());
|
||||
try {
|
||||
if (isConnect) {
|
||||
channelToUse = (this.preserveReceiveOrder ? new OrderedMessageChannelDecorator(channel, logger) : channel);
|
||||
info = new SessionInfo(channelToUse, session.getPrincipal());
|
||||
SessionInfo prevInfo = this.sessions.putIfAbsent(sessionId, info);
|
||||
Assert.state(prevInfo == null, "Session already exists");
|
||||
headerAccessor.setUserChangeCallback(info);
|
||||
}
|
||||
else {
|
||||
Assert.state(channelToUse != null, "Unknown session: " + sessionId);
|
||||
}
|
||||
|
||||
headerAccessor.setSessionId(sessionId);
|
||||
headerAccessor.setSessionAttributes(session.getAttributes());
|
||||
headerAccessor.setUser(getUser(session));
|
||||
if (isConnect) {
|
||||
headerAccessor.setUserChangeCallback(user -> {
|
||||
if (user != null && user != session.getPrincipal()) {
|
||||
this.stompAuthentications.put(session.getId(), user);
|
||||
}
|
||||
});
|
||||
}
|
||||
headerAccessor.setHeader(SimpMessageHeaderAccessor.HEART_BEAT_HEADER, headerAccessor.getHeartbeat());
|
||||
if (!detectImmutableMessageInterceptor(targetChannel)) {
|
||||
|
||||
if (!detectImmutableMessageInterceptor(channel)) {
|
||||
headerAccessor.setImmutable();
|
||||
}
|
||||
|
||||
@@ -363,24 +364,29 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Failed to send message to MessageChannel in session " + session.getId(), ex);
|
||||
logger.debug("Failed to send message to MessageChannel in session " + sessionId, ex);
|
||||
}
|
||||
else if (logger.isErrorEnabled()) {
|
||||
// Skip for unsent CONNECT or SUBSCRIBE (likely authentication/authorization issues)
|
||||
if (sent || !(isConnect || StompCommand.SUBSCRIBE.equals(command))) {
|
||||
logger.error("Failed to send message to MessageChannel in session " +
|
||||
session.getId() + ":" + ex.getMessage());
|
||||
sessionId + ":" + ex.getMessage());
|
||||
}
|
||||
}
|
||||
handleError(session, ex, message);
|
||||
}
|
||||
|
||||
if (!sent && isConnect) {
|
||||
this.sessions.remove(sessionId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Principal getUser(WebSocketSession session) {
|
||||
Principal user = this.stompAuthentications.get(session.getId());
|
||||
return (user != null ? user : session.getPrincipal());
|
||||
SessionInfo info = this.sessions.get(session.getId());
|
||||
return (info != null ? info.getUser() : session.getPrincipal());
|
||||
}
|
||||
|
||||
private void handleError(WebSocketSession session, Throwable ex, @Nullable Message<byte[]> clientMessage) {
|
||||
@@ -685,10 +691,7 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
|
||||
outputChannel.send(message);
|
||||
}
|
||||
finally {
|
||||
if (this.orderedHandlingMessageChannels != null) {
|
||||
this.orderedHandlingMessageChannels.remove(session.getId());
|
||||
}
|
||||
this.stompAuthentications.remove(session.getId());
|
||||
this.sessions.remove(session.getId());
|
||||
SimpAttributesContextHolder.resetAttributes();
|
||||
simpAttributes.sessionCompleted();
|
||||
}
|
||||
@@ -718,6 +721,39 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
|
||||
}
|
||||
|
||||
|
||||
private static class SessionInfo implements Consumer<Principal> {
|
||||
|
||||
private final MessageChannel channel;
|
||||
|
||||
@Nullable
|
||||
private final Principal webSocketUser;
|
||||
|
||||
@Nullable
|
||||
private volatile Principal stompUser;
|
||||
|
||||
SessionInfo(MessageChannel channel, @Nullable Principal user) {
|
||||
this.channel = channel;
|
||||
this.webSocketUser = user;
|
||||
}
|
||||
|
||||
public MessageChannel getMessageChannelToUse() {
|
||||
return this.channel;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Principal getUser() {
|
||||
return (this.stompUser != null ? this.stompUser : this.webSocketUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(@Nullable Principal stompUser) {
|
||||
if (stompUser != null && stompUser != this.webSocketUser) {
|
||||
this.stompUser = stompUser;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Contract for access to session counters.
|
||||
* @since 5.2
|
||||
|
||||
+9
@@ -101,6 +101,9 @@ class WebSocketMessageBrokerConfigurationSupportTests {
|
||||
session.setOpen(true);
|
||||
webSocketHandler.afterConnectionEstablished(session);
|
||||
|
||||
webSocketHandler.handleMessage(session,
|
||||
StompTextMessageBuilder.create(StompCommand.CONNECT).headers("destination:/foo").build());
|
||||
|
||||
webSocketHandler.handleMessage(session,
|
||||
StompTextMessageBuilder.create(StompCommand.SEND).headers("destination:/foo").build());
|
||||
|
||||
@@ -108,6 +111,12 @@ class WebSocketMessageBrokerConfigurationSupportTests {
|
||||
StompHeaderAccessor accessor = StompHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||
assertThat(accessor).isNotNull();
|
||||
assertThat(accessor.isMutable()).isFalse();
|
||||
assertThat(accessor.getMessageType()).isEqualTo(SimpMessageType.CONNECT);
|
||||
|
||||
message = channel.messages.get(1);
|
||||
accessor = StompHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||
assertThat(accessor).isNotNull();
|
||||
assertThat(accessor.isMutable()).isFalse();
|
||||
assertThat(accessor.getMessageType()).isEqualTo(SimpMessageType.MESSAGE);
|
||||
assertThat(accessor.getDestination()).isEqualTo("/foo");
|
||||
}
|
||||
|
||||
+3
-2
@@ -89,9 +89,10 @@ class StompWebSocketIntegrationTests extends AbstractWebSocketIntegrationTests {
|
||||
|
||||
super.setup(server, webSocketClient, testInfo);
|
||||
|
||||
TextMessage message = create(StompCommand.SEND).headers("destination:/app/simple").build();
|
||||
TextMessage m1 = create(StompCommand.CONNECT).headers("accept-version:1.1").build();
|
||||
TextMessage m2 = create(StompCommand.SEND).headers("destination:/app/simple").build();
|
||||
|
||||
try (WebSocketSession session = execute(new TestClientWebSocketHandler(0, message), "/ws").get()) {
|
||||
try (WebSocketSession session = execute(new TestClientWebSocketHandler(0, m1, m2), "/ws").get()) {
|
||||
assertThat(session).isNotNull();
|
||||
SimpleController controller = this.wac.getBean(SimpleController.class);
|
||||
assertThat(controller.latch.await(TIMEOUT, TimeUnit.SECONDS)).isTrue();
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
<suppress files="src[\\/]test[\\/]java[\\/]org[\\/]springframework[\\/]test[\\/]web[\\/](client|reactive|servlet)[\\/].+Tests" checks="IllegalImport" id="bannedHamcrestImports"/>
|
||||
<suppress files="src[\\/]test[\\/]java[\\/]org[\\/]springframework[\\/]test[\\/]context[\\/](aot|junit4)" checks="SpringJUnit5"/>
|
||||
<suppress files="AutowiredConfigurationErrorsIntegrationTests" checks="SpringJUnit5" message="Lifecycle method .+ should not be private"/>
|
||||
<suppress files="org[\\/]springframework[\\/]test[\\/]context[\\/].+[\\/](ExpectedExceptionSpringRunnerTests|StandardJUnit4FeaturesTests|ProgrammaticTxMgmtTestNGTests)" checks="RegexpSinglelineJava" id="expectedExceptionAnnotation"/>
|
||||
<suppress files="org[\\/]springframework[\\/]test[\\/]context[\\/].+[\\/](ExpectedExceptionSpringRunnerTests|StandardJUnit4FeaturesTests|TestNGConcurrencyTests|ProgrammaticTxMgmtTestNGTests)" checks="RegexpSinglelineJava" id="expectedExceptionAnnotation"/>
|
||||
|
||||
<!-- spring-web -->
|
||||
<suppress files="SpringHandlerInstantiator" checks="JavadocStyle"/>
|
||||
|
||||
Reference in New Issue
Block a user