Compare commits

..

64 Commits

Author SHA1 Message Date
Brian Clozel e354390837 Release v6.2.12 2025-10-16 08:56:30 +02:00
Juergen Hoeller c53132ecdf Polishing 2025-10-15 19:40:09 +02:00
rstoyanchev 03c3ec1577 Fall back on remoteHost if remoteAddress is null
See gh-35589
2025-10-15 16:42:40 +01:00
rstoyanchev c88bfc54c9 Refactor state management in StompSubProtocolHandler
Closes gh-35591
2025-10-15 12:31:42 +01:00
Sam Brannen a96558c965 Revise commit 01acb80501
See gh-35631
2025-10-15 12:48:52 +02:00
Sam Brannen 1612b7c5db Remove test prefixes 2025-10-15 11:28:49 +02:00
Sam Brannen 01acb80501 Improve exception handling in ConfigurationClassBeanDefinitionReader
Thanks to a proposal from @wilkinsona, this commit introduces a
try-catch block in loadBeanDefinitions(...) which throws an
IllegalStateException that provides context regarding the configuration
class and cause of the failure.

Closes gh-35631

Co-authored-by: Andy Wilkinson <andy.wilkinson@broadcom.com>
2025-10-15 11:26:06 +02:00
Juergen Hoeller f4438ce9e3 Upgrade to Reactor 2024.0.11 and Micrometer 1.14.12
Includes Netty 4.1.128

Closes gh-35638
Closes gh-35640
2025-10-14 19:21:34 +02:00
Juergen Hoeller 5033b9d3c5 Reset by-type cache for Object after registering a new singleton
Closes gh-35634
2025-10-14 19:20:57 +02:00
Juergen Hoeller d733023a29 Consistently apply fallback attribute to proxy definition as well
See gh-35627
2025-10-14 19:17:57 +02:00
Schäfer, H.H. (Hans Hosea) df67c1cf2d 35626: defaultCandidate for scoped proxies
Signed-off-by: Schäfer, H.H. (Hans Hosea) <HansHosea.Schaefer@ing.de>
2025-10-14 19:07:03 +02:00
rstoyanchev 05814f7a42 Polishing contribution
Closes gh-35625
2025-10-14 16:49:10 +01:00
Marius Lichtblau ba2bb08589 Release DataBuffer in AbstractCharSequenceDecoder
if String creation fails

See gh-35625

Signed-off-by: Marius Lichtblau <marius@lichtblau.io>
2025-10-14 16:49:10 +01:00
Edgar Asatryan 141df5291d Prevent NoClassDefFoundError when Jetty Reactive HttpClient not on classpath
Closes gh-35608

Signed-off-by: Edgar Asatryan <nstdio@gmail.com>
2025-10-14 16:49:10 +01:00
Sébastien Deleuze dcfe33f427 Fix Hibernate Javadoc to avoid a redirect 2025-10-14 17:40:44 +02:00
Juergen Hoeller 3a61460f91 Use JarFile#entries() instead of #stream() for consistent entry paths
Closes gh-35617
2025-10-14 11:23:34 +02:00
Brian Clozel ee284f2ee6 Add "forEachByte" variant to DataBuffer
As reported in gh-34651, `DataBuffer#getByte` can be inefficient for
some implementations, as bound checks are performed for each call.

This commit introduces a new `forEachByte` method that helps with
traversing operations without paying the bound check cost for each byte.

Closes gh-35623
2025-10-13 18:28:34 +02:00
Sam Brannen 2591cab561 Polishing 2025-10-13 15:12:21 +02:00
Sam Brannen 8456cd1e74 Polish contribution
See gh-35587
2025-10-13 15:12:21 +02:00
Igor Murzich 7699b4af9c Retain order of produces media types in @⁠ExceptionHandler
Closes gh-35587

Signed-off-by: Igor Murzich <imurzich@gmail.com>
2025-10-13 15:12:21 +02:00
Juergen Hoeller 6c5de48059 Ignore SQL state 3B001 in HSQLDB exception message as well
Closes gh-35618
See gh-35564
2025-10-13 15:10:20 +02:00
Juergen Hoeller 9f678ce698 Upgrade to Jetty 12.0.28, Undertow 2.3.20, RxJava 3.1.12, Aalto 1.3.4, Gson 2.13.2, Selenium 4.36, HtmlUnit 4.17, Mockito 5.20, AssertJ 3.27.6 2025-10-11 15:12:21 +02:00
Juergen Hoeller 467a484df6 Polishing
See gh-35460
2025-10-11 15:11:16 +02:00
Juergen Hoeller 83efe8cff4 Remove deprecation on CandidateComponentsIndex(Loader)
See gh-35472
2025-10-11 15:10:42 +02:00
rstoyanchev 2c83144946 Use getRemoteAddr in ServletServerHttpRequest
Closes gh-35589
2025-10-10 17:24:48 +01:00
rstoyanchev c165dd5e0e Update Javadoc of reactive WebSocketHandler
Replace suggestion to use zip with and operator.

Closes gh-35572
2025-10-10 17:23:03 +01:00
Juergen Hoeller 449b85f446 Avoid overhead for parsing plain values and simple placeholders
Closes gh-35594
2025-10-10 17:03:34 +02:00
Juergen Hoeller c2a66e723f Polishing 2025-10-09 20:34:03 +02:00
Juergen Hoeller 2ee34a5632 Add MySQLTableMetaDataProvider for correct generated-keys support
Closes gh-35593
2025-10-09 20:30:25 +02:00
Juergen Hoeller 1bc82d241a Remove deprecation on CandidateComponentsIndex(Loader)
Closes gh-35472
2025-10-09 20:26:59 +02:00
Juergen Hoeller 80e7ee321e Mark bootstrap thread for entire finishBeanFactoryInitialization phase
Closes gh-35398
2025-10-06 14:08:03 +02:00
Juergen Hoeller ecd3dd8883 Consistent local synchronization in getObjectFromFactoryBean
Closes gh-35545
2025-10-03 17:29:14 +02:00
Juergen Hoeller 332953c9a4 Align BatchUpdateException handling among SQLExceptionTranslator variants
Closes gh-35547
2025-10-03 14:51:15 +02:00
Sam Brannen 1cdd56bf02 Log multiple primary bean detection in DefaultListableBeanFactory
Prior to this commit, a NoUniqueBeanDefinitionException was thrown when
multiple primary beans were detected within a given set of beans, but
nothing was logged. For use cases where the exception is handled by
infrastructure code, it may not be obvious to the developer what the
problem is.

To address that, a TRACE message is now logged whenever multiple
competing primary beans are detected in DefaultListableBeanFactory.

Closes gh-35550
2025-10-03 14:09:52 +02:00
Juergen Hoeller 3041071269 Consistently apply useCaches for single Resource lookup
Closes gh-35465
2025-10-02 21:53:47 +02:00
Juergen Hoeller 2da821389c Remove redundant object allocation in cglib proxy method calls
Reformatted with Spring patch markers and backported to 6.2.12

Closes gh-35543
2025-10-02 21:53:34 +02:00
Juergen Hoeller d484e4f3ff Ignore SQL state 3B001 in releaseSavepoint (for HSQLDB)
Closes gh-35564
2025-10-02 13:01:30 +02:00
Juergen Hoeller 74dc61b8c4 Fall back to local synchronization in getObjectFromFactoryBean
Closes gh-35545
2025-10-02 13:01:03 +02:00
Sam Brannen 717358b56b Short circuit ClassUtils.findPubliclyAccessibleMethodIfPossible(...)
Once we find a publicly accessible method, there is no need to continue
traversing the type hierarchy.

See gh-35556
2025-10-02 11:44:54 +02:00
Sébastien Deleuze 836634c47f Upgrade CI workflow from Java 24 to Java 25 2025-10-02 10:19:42 +02:00
Juergen Hoeller a6f6ecfe6c Revise getPubliclyAccessibleMethodIfPossible to rely on Module#isExported
This avoids reflection and cache access for regular public and exported types.

Closes gh-35556
2025-10-01 19:56:23 +02:00
Juergen Hoeller e3da26ebbd Clarify event parameter type for multiple mapped classes
Closes gh-35506
2025-10-01 19:55:42 +02:00
Maziyar Bahramian e1c008f5a3 Fix URI Patterns docs for WebMVC and WebFlux
Closes gh-35551
Signed-off-by: Maziyar Bahramian <maziyar.bahramian@gmail.com>
2025-10-01 19:39:57 +02:00
Sébastien Deleuze cb849a7071 Add a test for CGLIB Enhancer Kotlin refinements
This commit adds a reproducer for the change of behavior introduced via
https://youtrack.jetbrains.com/issue/KT-76667. The test is only broken
with Kotlin 2.2.20+ without the related fix (see previous commit).

Closes gh-35487
2025-10-01 19:21:40 +02:00
Juergen Hoeller 3e37279db6 Make CGLIB Enhancer compatible with Kotlin 2.2.20+
This commit refines Enhancer#emitMethods to support the changes
introduced by https://youtrack.jetbrains.com/issue/KT-76667.

See gh-35487
2025-10-01 19:14:14 +02:00
rstoyanchev b3264ec2a8 Fix typo 2025-10-01 16:13:40 +01:00
rstoyanchev df860fd3cd Polishing contribution
Closes gh-35493
2025-10-01 14:45:18 +01:00
Réda Housni Alaoui 636523a2f5 AbstractMockHttpServletRequestBuilder#buildRequest is not idempotent
See gh-35493

Signed-off-by: Réda Housni Alaoui <reda-alaoui@hey.com>
2025-10-01 14:44:23 +01:00
rstoyanchev a19b51b7e0 Handle invalid position in ResourceHttpMessageWriter
Closes gh-35536
2025-10-01 14:44:05 +01:00
rstoyanchev fbdece6759 Polishing in ResourceHttpMessageWriter
See gh-35536
2025-10-01 14:42:01 +01:00
Sam Brannen 64d42fefda Fix idref example in reference manual
Closes gh-35560
2025-10-01 13:19:54 +02:00
Sam Brannen 447cfa18e9 Polishing 2025-10-01 13:19:25 +02:00
Sam Brannen ec3d9d6253 Upgrade to JUnit 5.14
Closes gh-35555
2025-09-30 12:43:53 +02:00
Sam Brannen 5cd2cb38e1 Make AbstractTestNGSpringContextTests thread-safe regarding tracked exceptions
Prior to this commit, AbstractTestNGSpringContextTests was not
thread-safe with regard to tracked exceptions.

To address that, AbstractTestNGSpringContextTests now tracks the test
exception via a ThreadLocal.

Closes gh-35528
2025-09-25 17:51:39 +02:00
rstoyanchev e9fb5eb38a Respect forwarded path in UrlHandlerFilter
Closes gh-35509
2025-09-25 12:19:56 +01:00
rstoyanchev 5a858915ea Revise parsed path handling in UrlHandlerFilter
Closes gh-35538
2025-09-24 19:08:55 +01:00
Brian Clozel d85a020e4e Improve Task Javadoc about Runnable wrapping
Closes gh-35394
2025-09-19 18:05:02 +02:00
Brian Clozel 0cc79ba366 Catch EOFException in IntrospectingClientHttpResponse
Prior to this commit, the `IntrospectingClientHttpResponse` would try
and read the HTTP response stream in order to check for the presence of
a non-empty message body.

Developers reported that in some cases, an `EOFException` is thrown
instead of returning -1 from the `read()` method. This commit ensures
that this case is taken into account and that we report the response as
an empty body in these cases.

Closes gh-35361
2025-09-19 17:49:12 +02:00
Sam Brannen 1e29911292 Upgrade to AssertJ 3.27.5 2025-09-19 11:23:08 +02:00
Juergen Hoeller 931686a5ee Upgrade to SnakeYAML 2.5, Protobuf 4.32.1, ActiveMQ 5.17.7 and Artemis 2.42, EasyMock 5.6, AssertJ 3.27.4, XMLUnit 2.10.4, Dom4J 2.2 2025-09-17 18:18:35 +02:00
Juergen Hoeller bf715ac23e Polishing 2025-09-17 18:10:19 +02:00
Brian Clozel b213344d25 Fix synchronization in ResponseBodyEmitter
See gh-35423
Fixes gh-35466

(cherry picked from commit 20e1149dde)
2025-09-17 18:07:45 +02:00
Stéphane Nicoll 0a48984fab Suppress deprecating warnings for getInstanceSupplier() method as well
This commit fixes code generation for a bean produced by a protected
factory method. Previously only the code generated for public methods,
i.e. without a dedicated instance supplier method, was handled.

Closes gh-35486
2025-09-16 16:08:15 +02:00
Brian Clozel cbdd107799 Next development version (v6.2.12-SNAPSHOT) 2025-09-11 09:40:23 +02:00
97 changed files with 2493 additions and 1517 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
toolchain: false
- version: 21
toolchain: true
- version: 24
- version: 25
toolchain: true
exclude:
- os:
+2 -2
View File
@@ -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/",
@@ -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"
+29 -29
View File
@@ -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
View File
@@ -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);
@@ -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();
}
}
@@ -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,
)
}
@@ -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()));
@@ -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;
@@ -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;
@@ -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) {
@@ -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"));
}
@@ -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) -> {})));
@@ -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();
}
}
@@ -38,4 +38,9 @@ public class DeprecatedMemberConfiguration {
return new DeprecatedBean();
}
@SuppressWarnings("deprecation")
DeprecatedBean deprecatedReturnTypeProtected() {
return new DeprecatedBean();
}
}
@@ -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 {
@@ -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,
@@ -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);
}
}
@@ -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());
@@ -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 {};
@@ -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;
@@ -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 {
/**
@@ -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)) {
@@ -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;
@@ -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
@@ -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
@@ -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);
@@ -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
@@ -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 {
@@ -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));
}
@@ -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();
@@ -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");
}
@@ -32,8 +32,6 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
*
* @author Stephane Nicoll
*/
@Deprecated
@SuppressWarnings("removal")
public class CandidateComponentsIndexLoaderTests {
@Test
@@ -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 {
@@ -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);
}
}
@@ -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);
}
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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() {
@@ -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()
@@ -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);
}
@@ -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);
}
}
@@ -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...
@@ -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.
@@ -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);
}
}
@@ -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;
}
}
@@ -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";
@@ -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();
}
}
@@ -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);
@@ -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) {
@@ -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() {
@@ -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() {
@@ -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 {
}
}
}
@@ -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 {
}
@@ -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();
}
}
@@ -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);
}
@@ -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) {
@@ -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
@@ -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);
@@ -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();
}
}
@@ -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);
@@ -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);
@@ -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);
}
}
}
@@ -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()");
@@ -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);
@@ -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&lt;String&gt; source = ... ;
* Mono&lt;Void&gt; output = session.send(source.map(session::textMessage));
*
* return Mono.zip(input, output).then();
* return input.and(output);
* }
* }
* </pre>
@@ -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 {
@@ -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 {
@@ -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);
}
}
@@ -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
@@ -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");
}
@@ -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();
+1 -1
View File
@@ -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"/>