Compare commits

...

58 Commits

Author SHA1 Message Date
Brian Clozel daf9551578 Next development version (v6.2.20-SNAPSHOT) 2026-06-08 19:16:50 +02:00
Sam Brannen 76a36dfd70 Track operations during SpEL expression evaluation
This commit introduces support for tracking operations during SpEL
expression evaluation. If the maximum number of operations is exceeded,
a SpelEvaluationException is thrown.

The limit can be configured either on a per-use-case basis via
SpelParserConfiguration supplied to the SpelExpressionParser or
globally as a JVM system property or Spring property named
`spring.expression.maxOperations`.

Closes gh-36801
2026-06-08 15:13:12 +02:00
Sam Brannen 3d47da9746 Ensure getters have non-void return types in SpEL
Closes gh-36800
2026-06-08 15:13:12 +02:00
Sam Brannen 519d7332eb Improve additional error messages in SpEL
This commit picks up where 987d6cca6d left off.

See gh-36756
2026-06-08 15:13:12 +02:00
Sam Brannen ec89834fa8 Further improve pattern caching in SpEL
See gh-36755
2026-06-08 15:13:12 +02:00
Sam Brannen b294371a46 Avoid too many character access attempts in AntPathMatcher
Closes gh-36799
2026-06-08 15:13:12 +02:00
Sébastien Deleuze 1829b42b9c Ensure consistent JSP tag attribute processing
Closes gh-36798
2026-06-08 15:13:12 +02:00
Sébastien Deleuze 86d99790db Refine JavaScriptUtils#javaScriptEscape
Closes gh-36796
2026-06-08 15:13:12 +02:00
Sébastien Deleuze 3aaec98765 Prevent special prefixes in default view name resolution
This commit updates the default view name generation logic in
both Spring WebMVC and Spring WebFlux to prevent "redirect:"
and "forward:" (for MVC) prefixes from the incoming request path.

Closes gh-36794
2026-06-08 15:13:12 +02:00
Sébastien Deleuze ee4e79002b Add trusted packages to MappingJackson2MessageConverter
This commit introduces trusted packages, specified via the related
setter for untrusted use cases. It allows explicit configuration
of which Java packages are allowed to be deserialized.

Closes gh-36792
2026-06-08 15:13:12 +02:00
rstoyanchev ce90e4b11f RfcParser rejects invalid IPv6 host
Closes gh-36787
2026-06-08 15:13:12 +02:00
Brian Clozel 224e647e1b Upgrade to Reactor 2024.0.18
Closes gh-36882
2026-06-08 12:19:46 +02:00
Brian Clozel c0d399ab88 Upgrade to Micrometer 1.15.12
Closes gh-36881
2026-06-08 10:25:24 +02:00
Sam Brannen 5c4497a96c Fix additional links to Selenium documentation
See gh-36875

(cherry picked from commit b4a378186f)
2026-06-05 14:58:22 +02:00
leestana01 0fadafa9bc Fix broken links to Selenium documentation
The links to docs.seleniumhq.org no longer resolve, since the host was
retired.

This commit changes those links to use the current Selenium
documentation at selenium.dev.

Closes gh-36875

Signed-off-by: leestana01 <leestana01@naver.com>
(cherry picked from commit 220fcaa1e3)
2026-06-05 14:58:14 +02:00
Brian Clozel f161a419bd Polishing contribution
This commit adds further fixes in the same area, since there were
similar bugs in the WriteCompletionHandler:
* databuffers were not always emitted when fully read in the onNext hook
* on completion, the iterator was closed too early, before it was fully
  read
* on completion, writing the next bytebuffers from the iterator would
  always reuse the first one and not update the attachment

Closes gh-36714
2026-06-04 12:20:04 +02:00
KimDaehyeon 244f6ec390 Fix data loss in DataBufferUtils synchronous write
Prior to this commit, WritableByteChannelSubscriber.hookOnNext() called
iterator.next() exactly once. If a DataBuffer consisted of multiple NIO
ByteBuffers (e.g., NettyDataBuffer wrapping a CompositeByteBuf), only
the first buffer was written to the channel, and the remaining buffers
were silently ignored and lost.

This commit adds the missing while (iterator.hasNext()) outer loop to
ensure all fragmented buffers exposed by the iterator are completely
and safely written to the synchronous channel.

See gh-36714

Signed-off-by: KimDaehyeon <daehyeon3351@gmail.com>
2026-06-04 12:19:52 +02:00
Brian Clozel cca70d4ad2 Polishing contribution
This fixes a potential regression introduced by the previous commit.
Because the current value was not updated after the temporal was rolled
forward, there were new cases where entire days would be skipped.

Closes gh-36865
2026-06-04 10:38:53 +02:00
arno 96b44949a8 Fix CronExpression day skip on midnight DST gap
After rollForward, BitsCronField always searched for the next
matching bit from zero. When daylight saving creates a gap at
the start of a period (e.g. Africa/Cairo), the temporal lands on
a non-zero field value and matching from zero could advance an
entire period too far, skipping the calendar day.

Search from the actual field value in the new period instead,
falling back to zero only when no bit matches in that period.

See gh-36865

Signed-off-by: arno <me@zmovo.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 10:38:17 +02:00
Juergen Hoeller 2555655ccf Fix concurrency issue against shared cookie field in setLocaleContext
Closes gh-36869

(cherry picked from commit 2e653246b5)
2026-06-03 23:10:36 +02:00
Brian Clozel ecfa498ea9 Support multi-line comments in Server Sent Events
Prior to this commit, comments sent with Server Sent Events could break
the wire format when sent over the network when comments contained line
breaks.
While comments are mainly used for sending keepalive messages, they can
also be used for sending debug data. This commit ensures that line
breaks are properly handled in comments.

Fixes gh-36866
2026-06-03 11:22:49 +02:00
Juergen Hoeller e2646fe324 Fix applicability note on setAutoGrowCollectionLimit
Closes gh-36863

(cherry picked from commit ecc847c493)
2026-06-02 17:38:09 +02:00
Sam Brannen a22818564b Update antora-extensions to 1.14.12
Closes gh-36851

(cherry picked from commit 6985d00fce)
2026-05-28 11:00:21 +02:00
Juergen Hoeller c37ba027e7 Upgrade to Netty 4.1.134 2026-05-27 16:53:44 +02:00
Juergen Hoeller 2dd550d80f Force initialization of configuration class in mainline thread
Closes gh-36844

(cherry picked from commit af2b96192d)
2026-05-27 16:52:15 +02:00
Juergen Hoeller 9ca396e424 Remove scanned class only when conflicting with imported class
Closes gh-36835

(cherry picked from commit 121c0ac285)
2026-05-27 16:52:11 +02:00
Juergen Hoeller 2eb0ba3843 Fix MethodParameter nestingLevel documentation
Closes gh-36826

(cherry picked from commit 3e585830d7)
2026-05-27 16:52:07 +02:00
Sam Brannen cc48200b65 Upgrade Antora dependencies
(cherry picked from commit b95caa8331)
2026-05-27 12:17:18 +02:00
Sam Brannen 44150f2523 Pin Node.js version to 24.15.0
Prior to this commit, the `antora` Gradle task silently failed to build
the reference documentation, since Antora uses the latest LTS release
for Node.js by default, and the latest LTS apparently does not work for
us.

(cherry picked from commit 7651d5841f)
2026-05-27 12:05:48 +02:00
Sam Brannen a0a3fe41f6 Polish contribution
See gh-36833

(cherry picked from commit 6e122d3aaa)
2026-05-26 16:45:16 +02:00
seonwoo_jung de18f74c53 Expose ClassLoader from DefaultDeserializer
Add a public accessor for the ClassLoader configured on a
DefaultDeserializer instance so that callers no longer need to read the
private field via reflection in order to forward it to a
ConfigurableObjectInputStream subclass.

See gh-36827
Closes gh-36833

Signed-off-by: seonwoo_jung <laborlawseon@kap.kr>
(cherry picked from commit f7be796c1c)
2026-05-26 16:45:16 +02:00
dependabot[bot] 89ecaea937 Upgrade fast-xml-parser to 5.7.0
Closes gh-36691

(cherry picked from commit ac24766510)
2026-05-20 16:54:45 +02:00
Juergen Hoeller 444877a475 Polishing
(cherry picked from commit 8fe1de4595)
2026-05-13 19:57:29 +02:00
Juergen Hoeller 57b62dd73a Restrict SpringVersion.getVersion() to "major.minor.patch" format
Closes gh-36785

(cherry picked from commit c048074436)
2026-05-12 16:25:05 +02:00
Yanming Zhou 5f661f1b64 Fix typos for validateExistingTransaction
Closes gh-36767

Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>

(cherry picked from commit cfb8dc6cc8)
2026-05-09 16:41:51 +02:00
Juergen Hoeller b0137977d8 Upgrade to Netty 4.1.133 2026-05-08 16:11:11 +02:00
Juergen Hoeller 425ce8625a Consistently expose map key quotes
Closes gh-36765

(cherry picked from commit d3152c11c7)
2026-05-08 16:10:16 +02:00
Juergen Hoeller 85c578b929 Avoid ResolvableType#forType contention for implicit cache cleanup
Closes gh-36745

(cherry picked from commit 856e1d5dc8)
2026-05-08 16:10:02 +02:00
Sam Brannen cf727bd7c8 Fix error message for invalid regex in SpEL
Closes gh-36756

(cherry picked from commit 987d6cca6d)
2026-05-06 13:55:18 +02:00
shenjianeng e8f08b4158 Refactor regex pattern caching using computeIfAbsent
Closes gh-36755

Signed-off-by: shenjianeng <ishenjianeng@qq.com>

(cherry picked from commit ec21dc0f91)
2026-05-06 13:55:03 +02:00
Brian Clozel c6b485c31d Do not warn against Root Servlet context location
Closes gh-36693
2026-05-05 11:54:02 +02:00
rstoyanchev b8ddd2c690 Avoid race in InMemoryWebSession
Closes gh-36742
2026-05-01 15:23:59 +01:00
rstoyanchev a42a6e0c6a Switch to JdkIdGenerator in AbstractWebSocketSession
Closes gh-36740
2026-05-01 14:23:00 +01:00
Brian Clozel 8a93a88962 Fix parsing failure for MIME types with quoted pairs
Prior to this commit, MIME types with parameter values that contain a
quoted pair would sometimes fail and parse an incomplete parameter
value.

This commit ensures that the quoted section of the parameter value is
correctly handled.

Fixes gh-36730
2026-04-30 16:26:12 +02:00
Juergen Hoeller fdcf4d50f0 Consistent wrapping of BeanCreationExceptions from instance suppliers
Closes gh-36725

(cherry picked from commit 08c5280843)
2026-04-30 14:50:04 +02:00
Juergen Hoeller 3c023114fb Polishing
(cherry picked from commit cd5fee5347)
2026-04-29 22:03:04 +02:00
Juergen Hoeller 8b5cbb7496 Detect custom deserialized NullValue instances
Closes gh-36727

(cherry picked from commit 916cb64581)
2026-04-29 22:02:13 +02:00
Brian Clozel a58fdeaf3f Fix PartGenerator token request while creating tmp file
Prior to this commit, the `PartGenerator` would allow requesting
additional part tokens while in the `CreateFileState`. This is invalid
as any new token emitted would be rejected and would fail the entire
process.
This would only happen if the tmp file creation is slow enough for a new
token to be parsed and emitted.

This commit ensures that no new part token is requested while creating
the temporary file.
This change also fixes lifecycle issues and ensures that buffer
resources are cleaned in case of errors.

Fixes gh-36694
2026-04-28 23:30:01 +02:00
Sigurd Gerke e4bfdfa229 Fix a regression on value class parameter handling
This commit fixes a regression introduced by gh-36449 for
nullable value class with an non-null value.

Closes gh-36720
Signed-off-by: Sigurd Gerke <sigurd.gerke@onedata.de>
2026-04-28 11:08:06 +02:00
Brian Clozel 46867fad81 Avoid cache collisions in CachingResourceResolver
Prior to this commit, there could be cache collisions in the
`CachingResourceResolver` because the cache key generation was
incomplete. This commit expands the cache key generation to avoid such
cases.

Fixes gh-36713
2026-04-28 09:30:39 +02:00
Sam Brannen 371dcc5c13 Upgrade to JUnit 5.14.4
Closes gh-36707
2026-04-27 14:09:59 +03:00
Yanming Zhou dc032c1e33 Remove unnecessary invocations of toString()
Closes gh-36709

Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>

(cherry picked from commit bfb88cfc1c)
2026-04-27 14:06:20 +03:00
Yanming Zhou 626907bbfd Update copyright headers in remaining source files
Closes gh-36703

Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>

(cherry picked from commit c4dfb24116)
2026-04-27 14:05:50 +03:00
Brian Clozel 8d51f47357 Enfoce single version removal in content versioning
Prior to this commit, content based version strategies would remove all
instances of the version string in the request path when trying to
resolve the original resource with the chain.
This can cause issues in rare cases where there is a collision between
the content version and some other version string in the request path.

Because this strategy is based on the contents of the file itself, we
should only remove the last instance of the version string and then
attempt to resolve the original file.

Fixes gh-36698
2026-04-23 15:08:06 +02:00
Brian Clozel fb0c78ae27 Warn against unsafe static resource locations
Prior to this commit, `ResourceHandlerUtils` would perform resource
location checks to ensure that the configured location is valid. This
commit also ensures that we log a WARN message if the application
chooses a well-known unsafe location like "classpath:" or the root
Servlet context for serving static resources.

Closes gh-36693
2026-04-23 10:47:34 +02:00
Juergen Hoeller 9f431e2eac Polishing
(cherry picked from commit 8965d9bccf)
2026-04-21 20:41:11 +02:00
Juergen Hoeller cba8f225d9 Consistently ignore exceptions for Xerces-specific properties
Closes gh-36682

(cherry picked from commit af7a5716e8)
2026-04-21 20:41:06 +02:00
Stéphane Nicoll 515152b645 Next development version (v6.2.19-SNAPSHOT) 2026-04-17 08:32:54 +02:00
172 changed files with 1995 additions and 688 deletions
+4
View File
@@ -17,6 +17,10 @@ antora {
]
}
node {
version = '24.15.0'
}
tasks.named("generateAntoraYml") {
asciidocAttributes = project.provider( {
return ["spring-version": project.version ]
@@ -74,6 +74,11 @@ expressions used in XML bean definitions, `@Value`, etc.
| The mode to use when compiling expressions for the
xref:core/expressions/evaluation.adoc#expressions-compiler-configuration[Spring Expression Language].
| `spring.expression.maxOperations`
| The default maximum number of operations permitted during
xref:core/expressions/evaluation.adoc#expressions-parser-configuration[Spring Expression Language]
expression evaluation.
| `spring.getenv.ignore`
| Instructs Spring to ignore operating system environment variables if a Spring
`Environment` property -- for example, a placeholder in a configuration String -- isn't
@@ -395,6 +395,16 @@ set a JVM system property or Spring property named `spring.context.expression.ma
to the maximum expression length needed by your application (see
xref:appendix.adoc#appendix-spring-properties[Supported Spring Properties]).
Similarly, the number of operations performed during the evaluation of a SpEL expression
cannot exceed 10,000 by default; however, the `maxOperations` value is configurable. If
you create a `SpelExpressionParser` programmatically (the recommend approach), you can
specify a custom `maxOperations` value when creating the `SpelParserConfiguration` that
you provide to the `SpelExpressionParser`. If you are not able to configure an explicit
value for `maxOperations` via `SpelParserConfiguration`, you can set a JVM system
property or Spring property named `spring.expression.maxOperations` to the maximum number
of operations required by your application (see
xref:appendix.adoc#appendix-spring-properties[Supported Spring Properties]).
[[expressions-spel-compilation]]
== SpEL Compilation
@@ -22,7 +22,7 @@ where all the underlying resources have to participate in the service-level tran
NOTE: By default, a participating transaction joins the characteristics of the outer scope,
silently ignoring the local isolation level, timeout value, or read-only flag (if any).
Consider switching the `validateExistingTransactions` flag to `true` on your transaction
Consider switching the `validateExistingTransaction` flag to `true` on your transaction
manager if you want isolation level declarations to be rejected when participating in
an existing transaction with a different isolation level. This non-lenient mode also
rejects read-only mismatches (that is, an inner read-write transaction that tries to participate
@@ -3,7 +3,7 @@
In the previous sections, we have seen how to use MockMvc in conjunction with the raw
HtmlUnit APIs. In this section, we use additional abstractions within the Selenium
https://docs.seleniumhq.org/projects/webdriver/[WebDriver] to make things even easier.
https://www.selenium.dev/documentation/webdriver/[WebDriver] to make things even easier.
[[mockmvc-server-htmlunit-webdriver-why]]
== Why WebDriver and MockMvc?
@@ -12,8 +12,8 @@ We can already use HtmlUnit and MockMvc, so why would we want to use WebDriver?
Selenium WebDriver provides a very elegant API that lets us easily organize our code. To
better show how it works, we explore an example in this section.
NOTE: Despite being a part of https://docs.seleniumhq.org/[Selenium], WebDriver does not
require a Selenium Server to run your tests.
NOTE: Despite being a part of https://www.selenium.dev/documentation/[Selenium],
WebDriver does not require a Selenium Server to run your tests.
Suppose we need to ensure that a message is created properly. The tests involve finding
the HTML form input elements, filling them out, and making various assertions.
@@ -308,7 +308,7 @@ interested. These are of type `WebElement`. WebDriver's
https://github.com/SeleniumHQ/selenium/wiki/PageFactory[`PageFactory`] lets us remove a
lot of code from the HtmlUnit version of `CreateMessagePage` by automatically resolving
each `WebElement`. The
https://seleniumhq.github.io/selenium/docs/api/java/org/openqa/selenium/support/PageFactory.html#initElements-org.openqa.selenium.WebDriver-java.lang.Class-[`PageFactory#initElements(WebDriver,Class<T>)`]
https://www.selenium.dev/selenium/docs/api/java/org/openqa/selenium/support/PageFactory.html#initElements-org.openqa.selenium.WebDriver-java.lang.Class-[`PageFactory#initElements(WebDriver,Class<T>)`]
method automatically resolves each `WebElement` by using the field name and looking it up
by the `id` or `name` of the element within the HTML page.
<3> We can use the
@@ -352,7 +352,7 @@ interested. These are of type `WebElement`. WebDriver's
https://github.com/SeleniumHQ/selenium/wiki/PageFactory[`PageFactory`] lets us remove a
lot of code from the HtmlUnit version of `CreateMessagePage` by automatically resolving
each `WebElement`. The
https://seleniumhq.github.io/selenium/docs/api/java/org/openqa/selenium/support/PageFactory.html#initElements-org.openqa.selenium.WebDriver-java.lang.Class-[`PageFactory#initElements(WebDriver,Class<T>)`]
https://www.selenium.dev/selenium/docs/api/java/org/openqa/selenium/support/PageFactory.html#initElements-org.openqa.selenium.WebDriver-java.lang.Class-[`PageFactory#initElements(WebDriver,Class<T>)`]
method automatically resolves each `WebElement` by using the field name and looking it up
by the `id` or `name` of the element within the HTML page.
<3> We can use the
+5 -5
View File
@@ -1,11 +1,11 @@
{
"dependencies": {
"antora": "3.2.0-alpha.11",
"antora": "3.2.0-alpha.12",
"@antora/atlas-extension": "1.0.0-alpha.5",
"@antora/collector-extension": "1.0.2",
"@antora/collector-extension": "1.0.3",
"@asciidoctor/tabs": "1.0.0-beta.6",
"@springio/antora-extensions": "1.14.7",
"fast-xml-parser": "5.3.8",
"@springio/asciidoctor-extensions": "1.0.0-alpha.17"
"@springio/antora-extensions": "1.14.12",
"fast-xml-parser": "5.7.0",
"@springio/asciidoctor-extensions": "1.0.0-alpha.18"
}
}
+4 -4
View File
@@ -8,10 +8,10 @@ javaPlatform {
dependencies {
api(platform("com.fasterxml.jackson:jackson-bom:2.18.5"))
api(platform("io.micrometer:micrometer-bom:1.15.11"))
api(platform("io.netty:netty-bom:4.1.132.Final"))
api(platform("io.micrometer:micrometer-bom:1.15.12"))
api(platform("io.netty:netty-bom:4.1.134.Final"))
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
api(platform("io.projectreactor:reactor-bom:2024.0.17"))
api(platform("io.projectreactor:reactor-bom:2024.0.18"))
api(platform("io.rsocket:rsocket-bom:1.1.5"))
api(platform("org.apache.groovy:groovy-bom:4.0.31"))
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
@@ -20,7 +20,7 @@ dependencies {
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.33"))
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.14.3"))
api(platform("org.junit:junit-bom:5.14.4"))
api(platform("org.mockito:mockito-bom:5.23.0"))
constraints {
+1 -1
View File
@@ -1,4 +1,4 @@
version=6.2.18-SNAPSHOT
version=6.2.20-SNAPSHOT
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
@@ -171,7 +171,7 @@ public class MethodInvocationProceedingJoinPoint implements ProceedingJoinPoint,
@Override
public String toString() {
return "execution(" + getSignature().toString() + ")";
return "execution(" + getSignature() + ")";
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* 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.
@@ -967,8 +967,8 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
actualName = propertyName.substring(0, keyStart);
}
String key = propertyName.substring(keyStart + PROPERTY_KEY_PREFIX.length(), keyEnd);
if (key.length() > 1 && (key.startsWith("'") && key.endsWith("'")) ||
(key.startsWith("\"") && key.endsWith("\""))) {
if (key.length() > 1 && ((key.startsWith("'") && key.endsWith("'")) ||
(key.startsWith("\"") && key.endsWith("\"")))) {
key = key.substring(1, key.length() - 1);
}
keys.add(key);
@@ -63,12 +63,14 @@ public interface ConfigurablePropertyAccessor extends PropertyAccessor, Property
* <p>If {@code true}, a {@code null} path location will be populated
* with a default object value and traversed instead of resulting in a
* {@link NullValueInNestedPathException}.
* <p>Default is {@code false} on a plain PropertyAccessor instance.
* <p>Default is {@code false} on a plain accessor.
* @since 4.1
*/
void setAutoGrowNestedPaths(boolean autoGrowNestedPaths);
/**
* Return whether "auto-growing" of nested paths has been activated.
* @since 4.1
*/
boolean isAutoGrowNestedPaths();
@@ -154,7 +154,8 @@ public abstract class PropertyAccessorUtils {
PropertyAccessor.PROPERTY_KEY_SUFFIX, keyStart + PropertyAccessor.PROPERTY_KEY_PREFIX.length());
if (keyEnd != -1) {
String key = sb.substring(keyStart + PropertyAccessor.PROPERTY_KEY_PREFIX.length(), keyEnd);
if ((key.startsWith("'") && key.endsWith("'")) || (key.startsWith("\"") && key.endsWith("\""))) {
if (key.length() > 1 && ((key.startsWith("'") && key.endsWith("'")) ||
(key.startsWith("\"") && key.endsWith("\"")))) {
sb.delete(keyStart + 1, keyStart + 2);
sb.delete(keyEnd - 2, keyEnd - 1);
keyEnd = keyEnd - 2;
@@ -31,6 +31,7 @@ public class AotBeanProcessingException extends AotProcessingException {
private final RootBeanDefinition beanDefinition;
/**
* Create an instance with the {@link RegisteredBean} that fails to be
* processed, a detail message, and an optional root cause.
@@ -64,6 +65,7 @@ public class AotBeanProcessingException extends AotProcessingException {
return sb.toString();
}
/**
* Return the bean definition of the bean that failed to be processed.
*/
@@ -249,7 +249,7 @@ public abstract class YamlProcessor {
}
else {
// It has to be a map key in this case
result.put("[" + key.toString() + "]", value);
result.put("[" + key + "]", value);
}
});
return result;
@@ -610,9 +610,7 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac
if (ex instanceof BeanCreationException bce && beanName.equals(bce.getBeanName())) {
throw bce;
}
else {
throw new BeanCreationException(mbd.getResourceDescription(), beanName, ex.getMessage(), ex);
}
throw new BeanCreationException(mbd.getResourceDescription(), beanName, ex.getMessage(), ex);
}
if (earlySingletonExposure) {
@@ -1257,8 +1255,8 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac
instance = obtainInstanceFromSupplier(supplier, beanName, mbd);
}
catch (Throwable ex) {
if (ex instanceof BeansException beansException) {
throw beansException;
if (ex instanceof BeanCreationException bce && beanName.equals(bce.getBeanName())) {
throw bce;
}
throw new BeanCreationException(beanName, "Instantiation of supplied bean failed", ex);
}
@@ -1164,12 +1164,19 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
if (mbd.isBackgroundInit()) {
Executor executor = getBootstrapExecutor();
if (executor != null) {
// Force initialization of depends-on beans in mainline thread.
String[] dependsOn = mbd.getDependsOn();
if (dependsOn != null) {
for (String dep : dependsOn) {
getBean(dep);
}
}
// Force initialization of factory reference in mainline thread.
String factoryBeanName = mbd.getFactoryBeanName();
if (factoryBeanName != null) {
getBean(factoryBeanName);
}
// Instantiate current bean in background thread.
CompletableFuture<?> future = CompletableFuture.runAsync(
() -> instantiateSingletonInBackgroundThread(beanName), executor);
addSingletonFactory(beanName, () -> {
@@ -1413,6 +1413,8 @@ abstract class AbstractPropertyAccessorTests {
assertThat(accessor.getPropertyValue("map[key5[foo]].name")).isEqualTo("name8");
assertThat(accessor.getPropertyValue("map['key5[foo]'].name")).isEqualTo("name8");
assertThat(accessor.getPropertyValue("map[\"key5[foo]\"].name")).isEqualTo("name8");
assertThat(accessor.getPropertyValue("map['].name")).isEqualTo("name9");
assertThat(accessor.getPropertyValue("map[\"].name")).isEqualTo("name9");
assertThat(accessor.getPropertyValue("iterableMap[key1].name")).isEqualTo("nameC");
assertThat(accessor.getPropertyValue("iterableMap[key2][0].name")).isEqualTo("nameA");
assertThat(accessor.getPropertyValue("iterableMap[key2][1].name")).isEqualTo("nameB");
@@ -79,6 +79,10 @@ class PropertyAccessorUtilsTests {
assertThat(PropertyAccessorUtils.canonicalPropertyName("map[key1].name")).isEqualTo("map[key1].name");
assertThat(PropertyAccessorUtils.canonicalPropertyName("map['key1'].name")).isEqualTo("map[key1].name");
assertThat(PropertyAccessorUtils.canonicalPropertyName("map[\"key1\"].name")).isEqualTo("map[key1].name");
assertThat(PropertyAccessorUtils.canonicalPropertyName("map['key1]")).isEqualTo("map['key1]");
assertThat(PropertyAccessorUtils.canonicalPropertyName("map[\"key1]")).isEqualTo("map[\"key1]");
assertThat(PropertyAccessorUtils.canonicalPropertyName("map[']")).isEqualTo("map[']");
assertThat(PropertyAccessorUtils.canonicalPropertyName("map[\"]")).isEqualTo("map[\"]");
}
@Test
@@ -76,6 +76,7 @@ public class IndexedTestBean {
TestBean tb6 = new TestBean("name6", 0);
TestBean tb7 = new TestBean("name7", 0);
TestBean tb8 = new TestBean("name8", 0);
TestBean tb9 = new TestBean("name9", 0);
TestBean tbA = new TestBean("nameA", 0);
TestBean tbB = new TestBean("nameB", 0);
TestBean tbC = new TestBean("nameC", 0);
@@ -104,6 +105,8 @@ public class IndexedTestBean {
list.add(tbY);
this.map.put("key4", list);
this.map.put("key5[foo]", tb8);
this.map.put("'", tb9);
this.map.put("\"", tb9);
this.myTestBeans = new MyTestBeans(tbZ);
}
@@ -63,7 +63,7 @@ class TypeHelper {
if (type instanceof DeclaredType declaredType) {
Element enclosingElement = declaredType.asElement().getEnclosingElement();
if (enclosingElement instanceof TypeElement) {
return getQualifiedName(enclosingElement) + "$" + declaredType.asElement().getSimpleName().toString();
return getQualifiedName(enclosingElement) + "$" + declaredType.asElement().getSimpleName();
}
else {
return getQualifiedName(declaredType.asElement());
@@ -201,13 +201,13 @@ public class SpringCacheAnnotationParser implements CacheAnnotationParser, Seria
private void validateCacheOperation(AnnotatedElement ae, CacheOperation operation) {
if (StringUtils.hasText(operation.getKey()) && StringUtils.hasText(operation.getKeyGenerator())) {
throw new IllegalStateException("Invalid cache annotation configuration on '" +
ae.toString() + "'. Both 'key' and 'keyGenerator' attributes have been set. " +
ae + "'. Both 'key' and 'keyGenerator' attributes have been set. " +
"These attributes are mutually exclusive: either set the SpEL expression used to" +
"compute the key at runtime or set the name of the KeyGenerator bean to use.");
}
if (StringUtils.hasText(operation.getCacheManager()) && StringUtils.hasText(operation.getCacheResolver())) {
throw new IllegalStateException("Invalid cache annotation configuration on '" +
ae.toString() + "'. Both 'cacheManager' and 'cacheResolver' attributes have been set. " +
ae + "'. Both 'cacheManager' and 'cacheResolver' attributes have been set. " +
"These attributes are mutually exclusive: the cache manager is used to configure a" +
"default cache resolver if none is set. If a cache resolver is set, the cache manager" +
"won't be used.");
@@ -223,7 +223,7 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser {
if (StringUtils.hasText(builder.getKey()) && StringUtils.hasText(builder.getKeyGenerator())) {
throw new IllegalStateException("Invalid cache advice configuration on '" +
element.toString() + "'. Both 'key' and 'keyGenerator' attributes have been set. " +
element + "'. Both 'key' and 'keyGenerator' attributes have been set. " +
"These attributes are mutually exclusive: either set the SpEL expression used to" +
"compute the key at runtime or set the name of the KeyGenerator bean to use.");
}
@@ -87,7 +87,7 @@ public abstract class AbstractValueAdaptingCache implements Cache {
*/
@Nullable
protected Object fromStoreValue(@Nullable Object storeValue) {
if (this.allowNullValues && storeValue == NullValue.INSTANCE) {
if (this.allowNullValues && storeValue instanceof NullValue) {
return null;
}
return storeValue;
@@ -885,16 +885,13 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean
});
GeneratedMethod generateMethod = generatedClass.getMethods().add("apply", method -> {
method.addJavadoc("Apply resource autowiring.");
method.addModifiers(javax.lang.model.element.Modifier.PUBLIC,
javax.lang.model.element.Modifier.STATIC);
method.addModifiers(javax.lang.model.element.Modifier.PUBLIC, javax.lang.model.element.Modifier.STATIC);
method.addParameter(RegisteredBean.class, REGISTERED_BEAN_PARAMETER);
method.addParameter(this.target, INSTANCE_PARAMETER);
method.returns(this.target);
method.addCode(generateMethodCode(generatedClass.getName(),
generationContext.getRuntimeHints()));
method.addCode(generateMethodCode(generatedClass.getName(), generationContext.getRuntimeHints()));
});
beanRegistrationCode.addInstancePostProcessor(generateMethod.toMethodReference());
registerHints(generationContext.getRuntimeHints());
}
@@ -258,9 +258,11 @@ class ConfigurationClassParser {
return;
}
else if (configClass.isScanned()) {
String beanName = configClass.getBeanName();
if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
this.registry.removeBeanDefinition(beanName);
if (existingClass.isImported()) {
String beanName = configClass.getBeanName();
if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
this.registry.removeBeanDefinition(beanName);
}
}
// An implicitly scanned bean definition should not override an explicit import.
return;
@@ -195,7 +195,7 @@ public class InstrumentationLoadTimeWeaver implements LoadTimeWeaver {
@Override
public String toString() {
return "FilteringClassFileTransformer for: " + this.targetTransformer.toString();
return "FilteringClassFileTransformer for: " + this.targetTransformer;
}
}
@@ -177,7 +177,11 @@ final class BitsCronField extends CronField {
int next = nextSetBit(current);
if (next == -1) {
temporal = type().rollForward(temporal);
next = nextSetBit(0);
next = nextSetBit(type().get(temporal));
if (next == -1) {
temporal = type().rollForward(temporal);
next = nextSetBit(0);
}
}
if (next == current) {
return temporal;
@@ -191,7 +195,12 @@ final class BitsCronField extends CronField {
next = nextSetBit(current);
if (next == -1) {
temporal = type().rollForward(temporal);
next = nextSetBit(0);
next = nextSetBit(type().get(temporal));
if (next == -1) {
temporal = type().rollForward(temporal);
next = nextSetBit(0);
}
current = type().get(temporal);
}
}
if (count >= CronExpression.MAX_ATTEMPTS) {
@@ -286,8 +286,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
* Specify the limit for array and collection auto-growing.
* <p>Default is 256, preventing OutOfMemoryErrors in case of large indexes.
* Raise this limit if your auto-growing needs are unusually high.
* <p>Used for setter/field injection via {@link #bind(PropertyValues)}, and not
* applicable to constructor binding via {@link #construct}.
* <p>Used for setter injection via {@link #bind(PropertyValues)};
* not applicable to field injection, and not to constructor binding
* via {@link #construct} either.
* @see #initBeanPropertyAccess()
* @see org.springframework.beans.BeanWrapper#setAutoGrowCollectionLimit
*/
@@ -197,6 +197,16 @@ class BackgroundBootstrapTests {
}
}
@Test
@Timeout(10)
@EnabledForTestGroups(LONG_RUNNING)
void bootstrapWithCustomExecutorAndLazyConfig() {
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CustomExecutorLazyBeanConfig.class);
assertThat(ctx.getBeanFactory().containsSingleton("testBean1")).isTrue();
assertThat(ctx.getBeanFactory().containsSingleton("testBean2")).isTrue();
ctx.close();
}
@Configuration(proxyBeanMethods = false)
static class UnmanagedThreadBeanConfig {
@@ -542,4 +552,36 @@ class BackgroundBootstrapTests {
}
}
@Configuration(proxyBeanMethods = false)
static class CustomExecutorLazyBeanConfig {
@Bean
public ThreadPoolTaskExecutor bootstrapExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("Custom-");
executor.setCorePoolSize(2);
executor.initialize();
return executor;
}
@Configuration(proxyBeanMethods = false)
@Lazy
static class LazyBeanConfig {
@Bean(bootstrap = BACKGROUND)
public TestBean testBean1() throws InterruptedException {
Thread.sleep(6000);
return new TestBean();
}
@Bean(bootstrap = BACKGROUND)
@Lazy
public TestBean testBean2() throws InterruptedException {
Thread.sleep(6000);
return new TestBean();
}
}
}
}
@@ -26,6 +26,7 @@ import example.scannable.CustomComponent;
import example.scannable.CustomStereotype;
import example.scannable.DefaultNamedComponent;
import example.scannable.FooService;
import example.scannable.FooServiceImpl;
import example.scannable.MessageBean;
import example.scannable.ScopedProxyTestBean;
import example.scannable_implicitbasepackage.ComponentScanAnnotatedConfigWithImplicitBasePackage;
@@ -43,6 +44,7 @@ import org.springframework.beans.factory.annotation.CustomAutowireConfigurer;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.ResourceLoaderAware;
@@ -84,6 +86,17 @@ class ComponentScanAnnotationIntegrationTests {
assertContextContainsBean(ctx, "fooServiceImpl");
}
@Test
void controlScanWithExplicitRegistration() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.registerBeanDefinition("myFooService", new RootBeanDefinition(FooServiceImpl.class));
ctx.scan(example.scannable.PackageMarker.class.getPackage().getName());
ctx.refresh();
assertContextContainsBean(ctx, "myFooService");
assertContextContainsBean(ctx, "fooServiceImpl");
}
@Test
void viaContextRegistration() {
ApplicationContext ctx = new AnnotationConfigApplicationContext(ComponentScanAnnotatedConfig.class);
@@ -289,7 +289,7 @@ class AutowiredConfigurationTests {
@Bean
public TestBean testBean(Color color, List<Color> colors) {
return new TestBean(color.toString() + "-" + colors.get(0).toString());
return new TestBean(color + "-" + colors.get(0));
}
}
@@ -303,7 +303,7 @@ class AutowiredConfigurationTests {
return new TestBean("");
}
else {
return new TestBean(color.get() + "-" + colors.get().get(0).toString());
return new TestBean(color.get() + "-" + colors.get().get(0));
}
}
}
@@ -16,6 +16,7 @@
package org.springframework.scheduling.support;
import java.time.ZonedDateTime;
import java.util.Arrays;
import org.assertj.core.api.Condition;
@@ -29,6 +30,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
*
* @author Arjen Poutsma
* @author Sam Brannen
* @author Brian Clozel
*/
class BitsCronFieldTests {
@@ -112,6 +114,24 @@ class BitsCronFieldTests {
.has(clear(0)).has(setRange(1, 7));
}
@Test
void nextOrSameWithMidnightGap() {
BitsCronField field = BitsCronField.parseHours("0-23/2");
ZonedDateTime last = ZonedDateTime.parse("2025-04-24T23:00:00+02:00[Africa/Cairo]");
ZonedDateTime expected = ZonedDateTime.parse("2025-04-25T02:00:00+03:00[Africa/Cairo]");
ZonedDateTime actual = field.nextOrSame(last);
assertThat(actual).isEqualTo(expected);
}
@Test
void nextOrSameWithGapAfterRollForward() {
BitsCronField field = BitsCronField.parseHours("0,2");
ZonedDateTime last = ZonedDateTime.parse("2026-03-08T01:00:00-05:00[America/New_York]");
ZonedDateTime expected = ZonedDateTime.parse("2026-03-09T00:00:00-04:00[America/New_York]");
ZonedDateTime actual = field.nextOrSame(last);
assertThat(actual).isEqualTo(expected);
}
private static Condition<BitsCronField> set(int... indices) {
return new Condition<>(String.format("set bits %s", Arrays.toString(indices))) {
@@ -1367,6 +1367,14 @@ class CronExpressionTests {
actual = cronExpression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
cronExpression = CronExpression.parse("0 0 */2 * * ?");
last = ZonedDateTime.parse("2025-04-24T22:00:00+02:00[Africa/Cairo]");
expected = ZonedDateTime.parse("2025-04-25T02:00:00+03:00[Africa/Cairo]");
actual = cronExpression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
}
@Test
@@ -24,7 +24,9 @@ import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeanUtils;
import org.springframework.cache.Cache;
import org.springframework.cache.support.NullValue;
import static org.assertj.core.api.Assertions.assertThat;
@@ -72,6 +74,12 @@ public abstract class AbstractCacheTests<T extends Cache> {
assertThat(cache.get(key).get()).isNull();
assertThat(cache.get(key, String.class)).isNull();
assertThat(cache.get(key, Object.class)).isNull();
cache.put(key, BeanUtils.instantiateClass(NullValue.class));
assertThat(cache.get(key)).isNotNull();
assertThat(cache.get(key).get()).isNull();
assertThat(cache.get(key, String.class)).isNull();
assertThat(cache.get(key, Object.class)).isNull();
}
@Test
@@ -36,10 +36,9 @@ public abstract class AbstractValueAdaptingCacheTests<T extends AbstractValueAda
protected void cachePutNullValueAllowNullFalse() {
T cache = getCache(false);
String key = createRandomKey();
assertThatIllegalArgumentException().isThrownBy(() ->
cache.put(key, null))
.withMessageContaining(CACHE_NAME_NO_NULL)
.withMessageContaining("is configured to not allow null values but null was provided");
assertThatIllegalArgumentException().isThrownBy(() -> cache.put(key, null))
.withMessageContaining(CACHE_NAME_NO_NULL)
.withMessageContaining("is configured to not allow null values but null was provided");
}
}
@@ -145,7 +145,7 @@ implements CallbackGenerator
private static void superHelper(CodeEmitter e, MethodInfo method, Context context)
{
if (TypeUtils.isAbstract(method.getModifiers())) {
e.throw_exception(ABSTRACT_METHOD_ERROR, method.toString() + " is abstract" );
e.throw_exception(ABSTRACT_METHOD_ERROR, method + " is abstract" );
} else {
e.load_this();
context.emitLoadArgsAndInvoke(e, method);
@@ -129,9 +129,10 @@ public abstract class CoroutinesUtils {
Object arg = args[index];
if (!(parameter.isOptional() && arg == null)) {
KType type = parameter.getType();
if (!type.isMarkedNullable() &&
if (!(type.isMarkedNullable() && arg == null) &&
type.getClassifier() instanceof KClass<?> kClass &&
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass))) {
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass)) &&
!JvmClassMappingKt.getJavaClass(kClass).isInstance(arg)) {
arg = box(kClass, arg);
}
argMap.put(parameter, arg);
@@ -161,9 +162,10 @@ public abstract class CoroutinesUtils {
private static Object box(KClass<?> kClass, @Nullable Object arg) {
KFunction<?> constructor = Objects.requireNonNull(KClasses.getPrimaryConstructor(kClass));
KType type = constructor.getParameters().get(0).getType();
if (!type.isMarkedNullable() &&
if (!(type.isMarkedNullable() && arg == null) &&
type.getClassifier() instanceof KClass<?> parameterClass &&
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(parameterClass))) {
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(parameterClass)) &&
!JvmClassMappingKt.getJavaClass(parameterClass).isInstance(arg)) {
arg = box(parameterClass, arg);
}
if (!KCallablesJvm.isAccessible(constructor)) {
@@ -121,8 +121,8 @@ public class MethodParameter {
* return type; 0 for the first method parameter; 1 for the second method
* parameter, etc.
* @param nestingLevel the nesting level of the target type
* (typically 1; for example, in case of a List of Lists, 1 would indicate the
* nested List, whereas 2 would indicate the element of the nested List)
* (1 for the top-level type; in case of a List of Lists, 2 would indicate
* the nested List, whereas 3 would indicate the element of the nested List)
*/
public MethodParameter(Method method, int parameterIndex, int nestingLevel) {
Assert.notNull(method, "Method must not be null");
@@ -145,8 +145,8 @@ public class MethodParameter {
* @param constructor the Constructor to specify a parameter for
* @param parameterIndex the index of the parameter
* @param nestingLevel the nesting level of the target type
* (typically 1; for example, in case of a List of Lists, 1 would indicate the
* nested List, whereas 2 would indicate the element of the nested List)
* (1 for the top-level type; in case of a List of Lists, 2 would indicate
* the nested List, whereas 3 would indicate the element of the nested List)
*/
public MethodParameter(Constructor<?> constructor, int parameterIndex, int nestingLevel) {
Assert.notNull(constructor, "Constructor must not be null");
@@ -296,8 +296,8 @@ public class MethodParameter {
/**
* Return the nesting level of the target type
* (typically 1; for example, in case of a List of Lists, 1 would indicate the
* nested List, whereas 2 would indicate the element of the nested List).
* (1 for the top-level type; in case of a List of Lists, 2 would indicate
* the nested List, whereas 3 would indicate the element of the nested List).
*/
public int getNestingLevel() {
return this.nestingLevel;
@@ -1548,9 +1548,6 @@ public class ResolvableType implements Serializable {
return new ResolvableType(type, null, typeProvider, variableResolver);
}
// Purge empty entries on access since we don't have a clean-up thread or the like.
cache.purgeUnreferencedEntries();
// Check the cache - we may have a ResolvableType which has been resolved before...
ResolvableType resultType = new ResolvableType(type, typeProvider, variableResolver);
ResolvableType cachedType = cache.get(resultType);
@@ -44,6 +44,7 @@ import org.springframework.lang.Nullable;
* @see org.springframework.beans.factory.support.DefaultListableBeanFactory#STRICT_LOCKING_PROPERTY_NAME
* @see org.springframework.core.env.AbstractEnvironment#IGNORE_GETENV_PROPERTY_NAME
* @see org.springframework.expression.spel.SpelParserConfiguration#SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
* @see org.springframework.expression.spel.SpelParserConfiguration#SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
* @see org.springframework.jdbc.core.StatementCreatorUtils#IGNORE_GETPARAMETERTYPE_PROPERTY_NAME
* @see org.springframework.jndi.JndiLocatorDelegate#IGNORE_JNDI_PROPERTY_NAME
* @see org.springframework.objenesis.SpringObjenesis#IGNORE_OBJENESIS_PROPERTY_NAME
@@ -38,14 +38,28 @@ public final class SpringVersion {
/**
* Return the full version string of the present Spring codebase,
* Return the "major.minor.patch" version string of the present Spring codebase,
* or {@code null} if it cannot be determined.
* @see Package#getImplementationVersion()
*/
@Nullable
public static String getVersion() {
Package pkg = SpringVersion.class.getPackage();
return (pkg != null ? pkg.getImplementationVersion() : null);
String version = (pkg != null ? pkg.getImplementationVersion() : null);
if (version != null) {
int idx = version.indexOf('.'); // after major
if (idx != -1) {
idx = version.indexOf('.', idx + 1); // after minor
if (idx != -1) {
idx = version.indexOf('.', idx + 1); // after patch
if (idx != -1) {
// Ignore anything beyond "major.minor.patch"
version = version.substring(0, idx);
}
}
}
}
return version;
}
}
@@ -1138,9 +1138,11 @@ public abstract class DataBufferUtils {
protected void hookOnNext(DataBuffer dataBuffer) {
try {
try (DataBuffer.ByteBufferIterator iterator = dataBuffer.readableByteBuffers()) {
ByteBuffer byteBuffer = iterator.next();
while (byteBuffer.hasRemaining()) {
this.channel.write(byteBuffer);
while (iterator.hasNext()) {
ByteBuffer byteBuffer = iterator.next();
while (byteBuffer.hasRemaining()) {
this.channel.write(byteBuffer);
}
}
}
this.sink.next(dataBuffer);
@@ -1213,6 +1215,11 @@ public abstract class DataBufferUtils {
failed(ex, attachment);
}
}
else {
iterator.close();
this.sink.next(dataBuffer);
request(1);
}
}
@Override
@@ -1236,7 +1243,6 @@ public abstract class DataBufferUtils {
@Override
public void completed(Integer written, Attachment attachment) {
DataBuffer.ByteBufferIterator iterator = attachment.iterator();
iterator.close();
long pos = this.position.addAndGet(written);
ByteBuffer byteBuffer = attachment.byteBuffer();
@@ -1246,9 +1252,11 @@ public abstract class DataBufferUtils {
}
else if (iterator.hasNext()) {
ByteBuffer next = iterator.next();
this.channel.write(next, pos, attachment, this);
Attachment nextAttachment = new Attachment(next, attachment.dataBuffer(), iterator);
this.channel.write(next, pos, nextAttachment, this);
}
else {
iterator.close();
this.sink.next(attachment.dataBuffer());
this.writing.set(false);
@@ -59,6 +59,18 @@ public class DefaultDeserializer implements Deserializer<Object> {
}
/**
* Return the {@link ClassLoader} to use for deserialization, or {@code null}
* to use the "latest user-defined ClassLoader".
* @since 6.2.19
* @see ConfigurableObjectInputStream#ConfigurableObjectInputStream(InputStream, ClassLoader)
*/
@Nullable
public ClassLoader getClassLoader() {
return this.classLoader;
}
/**
* Read from the supplied {@code InputStream} and deserialize the contents
* into an object.
@@ -725,7 +725,7 @@ public class AntPathMatcher implements PathMatcher {
return this.caseSensitive ? this.rawPattern.equals(str) : this.rawPattern.equalsIgnoreCase(str);
}
else if (this.pattern != null) {
Matcher matcher = this.pattern.matcher(str);
Matcher matcher = this.pattern.matcher(new MaxAttemptsCharSequence(str));
if (matcher.matches()) {
if (uriTemplateVariables != null) {
if (this.variableNames.size() != matcher.groupCount()) {
@@ -750,6 +750,60 @@ public class AntPathMatcher implements PathMatcher {
return false;
}
private static class MaxAttemptsCharSequence implements CharSequence {
private static final int MAX_ATTEMPTS = 1_000_000;
private final String text;
private final Counter counter;
MaxAttemptsCharSequence(String text) {
this(text, new Counter());
}
private MaxAttemptsCharSequence(String text, Counter counter) {
this.text = text;
this.counter = counter;
}
@Override
public int length() {
return this.text.length();
}
@Override
public char charAt(int index) {
if (this.counter.value++ >= MAX_ATTEMPTS) {
throw new IllegalStateException(
"Too many character access attempts encountered during pattern matching");
}
return this.text.charAt(index);
}
@Override
public boolean isEmpty() {
return this.text.isEmpty();
}
@Override
public CharSequence subSequence(int start, int end) {
return new MaxAttemptsCharSequence(this.text.substring(start, end), this.counter);
}
@Override
public String toString() {
return this.text;
}
private static class Counter {
private int value;
}
}
}
@@ -102,7 +102,7 @@ public final class ConcurrentLruCache<K, V> {
if (this.capacity == 0) {
return this.generator.apply(key);
}
final Node<K, V> node = this.cache.get(key);
Node<K, V> node = this.cache.get(key);
if (node == null) {
V value = this.generator.apply(key);
put(key, value);
@@ -115,9 +115,9 @@ public final class ConcurrentLruCache<K, V> {
private void put(K key, V value) {
Assert.notNull(key, "key must not be null");
Assert.notNull(value, "value must not be null");
final CacheEntry<V> cacheEntry = new CacheEntry<>(value, CacheEntryState.ACTIVE);
final Node<K, V> node = new Node<>(key, cacheEntry);
final Node<K, V> prior = this.cache.putIfAbsent(node.key, node);
CacheEntry<V> cacheEntry = new CacheEntry<>(value, CacheEntryState.ACTIVE);
Node<K, V> node = new Node<>(key, cacheEntry);
Node<K, V> prior = this.cache.putIfAbsent(node.key, node);
if (prior == null) {
processWrite(new AddTask(node));
}
@@ -128,7 +128,7 @@ public final class ConcurrentLruCache<K, V> {
private void processRead(Node<K, V> node) {
boolean drainRequested = this.readOperations.recordRead(node);
final DrainStatus status = this.drainStatus.get();
DrainStatus status = this.drainStatus.get();
if (status.shouldDrainBuffers(drainRequested)) {
drainOperations();
}
@@ -229,7 +229,7 @@ public final class ConcurrentLruCache<K, V> {
*/
@Nullable
public boolean remove(K key) {
final Node<K, V> node = this.cache.remove(key);
Node<K, V> node = this.cache.remove(key);
if (node == null) {
return false;
}
@@ -238,27 +238,29 @@ public final class ConcurrentLruCache<K, V> {
return true;
}
/*
/**
* Transition the node from the {@code active} state to the {@code pending removal} state,
* if the transition is valid.
*/
private void markForRemoval(Node<K, V> node) {
for (; ; ) {
final CacheEntry<V> current = node.get();
while (true) {
CacheEntry<V> current = node.get();
if (!current.isActive()) {
return;
}
final CacheEntry<V> pendingRemoval = new CacheEntry<>(current.value, CacheEntryState.PENDING_REMOVAL);
CacheEntry<V> pendingRemoval = new CacheEntry<>(current.value, CacheEntryState.PENDING_REMOVAL);
if (node.compareAndSet(current, pendingRemoval)) {
return;
}
}
}
/**
* Write operation recorded when a new entry is added to the cache.
*/
private final class AddTask implements Runnable {
final Node<K, V> node;
AddTask(Node<K, V> node) {
@@ -276,7 +278,7 @@ public final class ConcurrentLruCache<K, V> {
private void evictEntries() {
while (currentSize.get() > capacity) {
final Node<K, V> node = evictionQueue.poll();
Node<K, V> node = evictionQueue.poll();
if (node == null) {
return;
}
@@ -284,7 +286,6 @@ public final class ConcurrentLruCache<K, V> {
markAsRemoved(node);
}
}
}
@@ -292,6 +293,7 @@ public final class ConcurrentLruCache<K, V> {
* Write operation recorded when an entry is removed to the cache.
*/
private final class RemovalTask implements Runnable {
final Node<K, V> node;
RemovalTask(Node<K, V> node) {
@@ -311,7 +313,7 @@ public final class ConcurrentLruCache<K, V> {
*/
private enum DrainStatus {
/*
/**
* No drain operation currently running.
*/
IDLE {
@@ -321,7 +323,7 @@ public final class ConcurrentLruCache<K, V> {
}
},
/*
/**
* A drain operation is required due to a pending write modification.
*/
REQUIRED {
@@ -331,7 +333,7 @@ public final class ConcurrentLruCache<K, V> {
}
},
/*
/**
* A drain operation is in progress.
*/
PROCESSING {
@@ -368,12 +370,6 @@ public final class ConcurrentLruCache<K, V> {
private static final int BUFFER_COUNT = detectNumberOfBuffers();
private static int detectNumberOfBuffers() {
int availableProcessors = Runtime.getRuntime().availableProcessors();
int nextPowerOfTwo = 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(availableProcessors - 1));
return Math.min(4, nextPowerOfTwo);
}
private static final int BUFFERS_MASK = BUFFER_COUNT - 1;
private static final int MAX_PENDING_OPERATIONS = 32;
@@ -384,19 +380,13 @@ public final class ConcurrentLruCache<K, V> {
private static final int BUFFER_INDEX_MASK = BUFFER_SIZE - 1;
/*
* Number of operations recorded, for each buffer
*/
// Number of operations recorded, for each buffer
private final AtomicLongArray recordedCount = new AtomicLongArray(BUFFER_COUNT);
/*
* Number of operations read, for each buffer
*/
// Number of operations read, for each buffer
private final long[] readCount = new long[BUFFER_COUNT];
/*
* Number of operations processed, for each buffer
*/
// Number of operations processed, for each buffer
private final AtomicLongArray processedCount = new AtomicLongArray(BUFFER_COUNT);
@SuppressWarnings("rawtypes")
@@ -404,10 +394,11 @@ public final class ConcurrentLruCache<K, V> {
private final EvictionQueue<K, V> evictionQueue;
@SuppressWarnings("rawtypes")
ReadOperations(EvictionQueue<K, V> evictionQueue) {
this.evictionQueue = evictionQueue;
for (int i = 0; i < BUFFER_COUNT; i++) {
this.buffers[i] = new AtomicReferenceArray<>(BUFFER_SIZE);
this.buffers[i] = new AtomicReferenceArray(BUFFER_SIZE);
}
}
@@ -459,6 +450,12 @@ public final class ConcurrentLruCache<K, V> {
}
this.processedCount.lazySet(bufferIndex, writeCount);
}
private static int detectNumberOfBuffers() {
int availableProcessors = Runtime.getRuntime().availableProcessors();
int nextPowerOfTwo = 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(availableProcessors - 1));
return Math.min(4, nextPowerOfTwo);
}
}
@@ -544,10 +541,9 @@ public final class ConcurrentLruCache<K, V> {
if (this.first == null) {
return null;
}
final Node<K, V> f = this.first;
final Node<K, V> next = f.getNext();
Node<K, V> f = this.first;
Node<K, V> next = f.getNext();
f.setNext(null);
this.first = next;
if (next == null) {
this.last = null;
@@ -566,13 +562,12 @@ public final class ConcurrentLruCache<K, V> {
}
private boolean contains(Node<K, V> e) {
return (e.getPrevious() != null) || (e.getNext() != null) || (e == this.first);
return (e.getPrevious() != null || e.getNext() != null || e == this.first);
}
private void linkLast(final Node<K, V> e) {
final Node<K, V> l = this.last;
private void linkLast(Node<K, V> e) {
Node<K, V> l = this.last;
this.last = e;
if (l == null) {
this.first = e;
}
@@ -583,8 +578,8 @@ public final class ConcurrentLruCache<K, V> {
}
private void unlink(Node<K, V> e) {
final Node<K, V> prev = e.getPrevious();
final Node<K, V> next = e.getNext();
Node<K, V> prev = e.getPrevious();
Node<K, V> next = e.getNext();
if (prev == null) {
this.first = next;
}
@@ -517,10 +517,17 @@ public class ConcurrentReferenceHashMap<K, V> extends AbstractMap<K, V> implemen
}
/**
* Remove any entries that have been garbage collected and are no longer referenced.
* Under normal circumstances garbage collected entries are automatically purged as
* items are added or removed from the Map. This method can be used to force a purge,
* and is useful when the Map is read frequently but updated less often.
* Remove any entries that have been garbage-collected and are no longer referenced.
* Note that this call implies segment locking and can lead to thread contention.
* <p>Under normal circumstances, garbage-collected entries are automatically purged as
* items are added or removed from the Map. This method can be used to force a purge
* which is useful when the Map is read frequently but hardly ever updated anymore.
* <p>Note that it may be preferable to simply {@link #clear() clear} the entire cache at
* certain points of the lifecycle, not just dropping unreferenced entries but even the
* entire cache content: assuming that most entries in the cache won't be needed anymore
* after certain processing phases, therefore rather rebuilding the cache going forward.
* @since 4.1.1
* @see #clear()
*/
public void purgeUnreferencedEntries() {
for (Segment segment : this.segments) {
@@ -737,7 +744,7 @@ public class ConcurrentReferenceHashMap<K, V> extends AbstractMap<K, V> implemen
int currCount = this.count.get();
boolean needsResize = allowResize && (currCount > 0 && currCount >= this.resizeThreshold);
Reference<K, V> ref = this.referenceManager.pollForPurge();
if (ref != null || (needsResize)) {
if (ref != null || needsResize) {
restructure(allowResize, ref);
}
}
@@ -249,7 +249,7 @@ public abstract class MimeTypeUtils {
break;
}
}
else if (ch == '"') {
else if (ch == '"' && mimeType.charAt(nextIndex - 1) != '\\') {
quoted = !quoted;
}
nextIndex++;
@@ -59,7 +59,7 @@ class GeneratedMethodsTests {
@Test
void createWithExistingGeneratorUsesGenerator() {
Function<MethodName, String> generator = name -> "__" + name.toString();
Function<MethodName, String> generator = name -> "__" + name;
GeneratedMethods methods = new GeneratedMethods(TEST_CLASS_NAME, generator);
assertThat(methods.add("test", methodSpecCustomizer).getName()).hasToString("__test");
}
@@ -338,6 +338,27 @@ class DataBufferUtilsTests extends AbstractDataBufferAllocatingTests {
channel.close();
}
@ParameterizedDataBufferAllocatingTest
void writeWritableByteChannelWithJoinedBuffer(DataBufferFactory bufferFactory) throws Exception {
super.bufferFactory = bufferFactory;
DataBuffer foo = stringBuffer("foo");
DataBuffer bar = stringBuffer("bar");
DataBuffer joined = bufferFactory.join(List.of(foo, bar));
WritableByteChannel channel = Files.newByteChannel(tempFile, StandardOpenOption.WRITE);
Flux<DataBuffer> writeResult = DataBufferUtils.write(Flux.just(joined), channel);
StepVerifier.create(writeResult)
.consumeNextWith(stringConsumer("foobar"))
.verifyComplete();
String result = String.join("", Files.readAllLines(tempFile));
assertThat(result).isEqualTo("foobar");
channel.close();
}
@ParameterizedDataBufferAllocatingTest
void writeWritableByteChannelErrorInFlux(DataBufferFactory bufferFactory) throws Exception {
super.bufferFactory = bufferFactory;
@@ -445,6 +466,27 @@ class DataBufferUtilsTests extends AbstractDataBufferAllocatingTests {
assertThat(result).isEqualTo("foobarbazqux");
}
@ParameterizedDataBufferAllocatingTest
void writeAsynchronousFileChannelWithJoinedBuffer(DataBufferFactory bufferFactory) throws Exception {
super.bufferFactory = bufferFactory;
DataBuffer foo = stringBuffer("foo");
DataBuffer bar = stringBuffer("bar");
DataBuffer joined = bufferFactory.join(List.of(foo, bar));
AsynchronousFileChannel channel = AsynchronousFileChannel.open(tempFile, StandardOpenOption.WRITE);
Flux<DataBuffer> writeResult = DataBufferUtils.write(Flux.just(joined), channel);
StepVerifier.create(writeResult)
.consumeNextWith(stringConsumer("foobar"))
.verifyComplete();
String result = String.join("", Files.readAllLines(tempFile));
assertThat(result).isEqualTo("foobar");
channel.close();
}
@ParameterizedDataBufferAllocatingTest
void writeAsynchronousFileChannelErrorInFlux(DataBufferFactory bufferFactory) throws Exception {
super.bufferFactory = bufferFactory;
@@ -78,6 +78,17 @@ class SerializerTests {
assertThat(deserializer.expectedInputStream).isNotNull();
}
@Test // gh-36833
void defaultDeserializerExposesNullClassLoaderByDefault() {
assertThat(new DefaultDeserializer().getClassLoader()).isNull();
}
@Test // gh-36833
void defaultDeserializerExposesConfiguredClassLoader() {
ClassLoader classLoader = getClass().getClassLoader();
assertThat(new DefaultDeserializer(classLoader).getClassLoader()).isSameAs(classLoader);
}
@Test
void serializationDelegateWithExplicitSerializerAndDeserializer() throws IOException {
SerializationDelegate delegate = new SerializationDelegate(new DefaultSerializer(), new DefaultDeserializer());
@@ -98,7 +98,7 @@ class MimeTypeTests {
}
@Test
void parseQuotedSeparator() {
void parseQuotedParameterValue() {
String s = "application/xop+xml;charset=utf-8;type=\"application/soap+xml;action=\\\"https://x.y.z\\\"\"";
MimeType mimeType = MimeType.valueOf(s);
assertThat(mimeType.getType()).as("Invalid type").isEqualTo("application");
@@ -107,6 +107,15 @@ class MimeTypeTests {
assertThat(mimeType.getParameter("type")).isEqualTo("\"application/soap+xml;action=\\\"https://x.y.z\\\"\"");
}
@Test
void parseParameterWithQuotedPair() {
String s = "text/plain;twelve=\"1\\\"2\"";
MimeType mimeType = MimeType.valueOf(s);
assertThat(mimeType.getType()).as("Invalid type").isEqualTo("text");
assertThat(mimeType.getSubtype()).as("Invalid subtype").isEqualTo("plain");
assertThat(mimeType.getParameter("twelve")).isEqualTo("\"1\\\"2\"");
}
@Test
void withConversionService() {
ConversionService conversionService = new DefaultConversionService();
@@ -244,6 +244,13 @@ class CoroutinesUtilsTests {
Assertions.assertThat(mono.awaitSingleOrNull()).isEqualTo("foo")
}
@Test
suspend fun invokeSuspendingFunctionWithNullableValueClassParameterAndUnderlyingValue() {
val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithNullableValueClass") }
val mono = CoroutinesUtils.invokeSuspendingFunction(method, this, "foo", null) as Mono
Assertions.assertThat(mono.awaitSingleOrNull()).isEqualTo("foo")
}
@Test
fun invokeSuspendingFunctionWithNullableValueClassParameter() {
val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithNullableValueClass") }
@@ -67,6 +67,8 @@ public class ExpressionState {
@Nullable
private Deque<VariableScope> variableScopes;
private int operationsCount;
// When entering a new scope there is a new base object which should be used
// for '#this' references (or to act as a target for unqualified references).
// This ArrayDeque captures those objects at each nested scope level.
@@ -146,6 +148,7 @@ public class ExpressionState {
* @see EvaluationContext#assignVariable(String, Supplier)
*/
public TypedValue assignVariable(String name, Supplier<TypedValue> valueSupplier) {
trackOperation();
return this.relatedContext.assignVariable(name, valueSupplier);
}
@@ -170,6 +173,7 @@ public class ExpressionState {
* @see #setVariable(String, Object)
*/
public TypedValue lookupVariable(String name) {
trackOperation();
Object value = this.relatedContext.lookupVariable(name);
return (value != null ? new TypedValue(value) : TypedValue.NULL);
}
@@ -301,6 +305,7 @@ public class ExpressionState {
public TypedValue operate(Operation op, @Nullable Object left, @Nullable Object right) throws EvaluationException {
OperatorOverloader overloader = this.relatedContext.getOperatorOverloader();
if (overloader.overridesOperation(op, left, right)) {
trackOperation();
Object returnValue = overloader.operate(op, left, right);
return new TypedValue(returnValue);
}
@@ -323,6 +328,18 @@ public class ExpressionState {
return this.configuration;
}
/**
* Track an operation during expression evaluation.
* @since 6.2.19
* @see SpelParserConfiguration#getMaximumOperations()
*/
public void trackOperation() {
int maxOperations = this.configuration.getMaximumOperations();
if (++this.operationsCount >= maxOperations) {
throw new SpelEvaluationException(SpelMessage.MAX_OPERATIONS_EXCEEDED, maxOperations);
}
}
/**
* A new local variable scope is entered when a new expression scope is
@@ -303,8 +303,11 @@ public enum SpelMessage {
/** @since 6.2 */
EXCEPTION_DURING_INDEX_WRITE(Kind.ERROR, 1084,
"A problem occurred while attempting to write index ''{0}'' in ''{1}''");
"A problem occurred while attempting to write index ''{0}'' in ''{1}''"),
/** @since 6.2.19 */
MAX_OPERATIONS_EXCEEDED(Kind.ERROR, 1085,
"SpEL expression evaluation exceeded the threshold of ''{0}'' operations");
private final Kind kind;
@@ -20,6 +20,8 @@ import java.util.Locale;
import org.springframework.core.SpringProperties;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Configuration object for the SpEL expression parser.
@@ -39,9 +41,38 @@ public class SpelParserConfiguration {
*/
public static final int DEFAULT_MAX_EXPRESSION_LENGTH = 10_000;
/** System property to configure the default compiler mode for SpEL expression parsers: {@value}. */
/**
* Default maximum number of operations permitted during SpEL expression evaluation: {@value}.
* @since 6.2.19
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
*/
public static final int DEFAULT_MAX_OPERATIONS = 10_000;
/**
* System property to configure the default compiler mode for SpEL expression parsers: {@value}.
* <p><strong>NOTE</strong>: Instead of relying on a global default, applications
* and frameworks should ideally set an explicit custom value via the
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
* constructor which provides complete configuration control and the ability
* to override global defaults per use case.
* <p>Can also be configured via the {@link SpringProperties} mechanism.
*/
public static final String SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME = "spring.expression.compiler.mode";
/**
* System property to configure the default maximum number of operations permitted
* during SpEL expression evaluation: {@value}.
* <p><strong>NOTE</strong>: Instead of relying on a global default, applications
* and frameworks should ideally set an explicit custom value via the
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
* constructor which provides complete configuration control and the ability
* to override global defaults per use case.
* <p>Can also be configured via the {@link SpringProperties} mechanism.
* @since 6.2.19
* @see #DEFAULT_MAX_OPERATIONS
*/
public static final String SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME = "spring.expression.maxOperations";
private static final SpelCompilerMode defaultCompilerMode;
@@ -65,9 +96,17 @@ public class SpelParserConfiguration {
private final int maximumExpressionLength;
private final int maximumOperations;
/**
* Create a new {@code SpelParserConfiguration} instance with default settings.
* <p><strong>NOTE</strong>: Favor the
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
* constructor for complete configuration control and the ability to override
* global defaults per use case.
* @see #SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
*/
public SpelParserConfiguration() {
this(null, null, false, false, Integer.MAX_VALUE);
@@ -75,8 +114,16 @@ public class SpelParserConfiguration {
/**
* Create a new {@code SpelParserConfiguration} instance.
* @param compilerMode the compiler mode for the parser
* @param compilerClassLoader the ClassLoader to use as the basis for expression compilation
* <p><strong>NOTE</strong>: Favor the
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
* constructor for complete configuration control and the ability to override
* global defaults per use case.
* @param compilerMode the compiler mode that parsers using this configuration
* should use; or {@code null} to use the default mode
* @param compilerClassLoader the {@code ClassLoader} to use as the basis for
* expression compilation; or {@code null} to use the default {@code ClassLoader}
* @see #SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
*/
public SpelParserConfiguration(@Nullable SpelCompilerMode compilerMode, @Nullable ClassLoader compilerClassLoader) {
this(compilerMode, compilerClassLoader, false, false, Integer.MAX_VALUE);
@@ -84,9 +131,14 @@ public class SpelParserConfiguration {
/**
* Create a new {@code SpelParserConfiguration} instance.
* <p><strong>NOTE</strong>: Favor the
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
* constructor for complete configuration control and the ability to override
* global defaults per use case.
* @param autoGrowNullReferences if null references should automatically grow
* @param autoGrowCollections if collections should automatically grow
* @see #SpelParserConfiguration(boolean, boolean, int)
* @see #SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
*/
public SpelParserConfiguration(boolean autoGrowNullReferences, boolean autoGrowCollections) {
this(null, null, autoGrowNullReferences, autoGrowCollections, Integer.MAX_VALUE);
@@ -94,9 +146,15 @@ public class SpelParserConfiguration {
/**
* Create a new {@code SpelParserConfiguration} instance.
* <p><strong>NOTE</strong>: Favor the
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
* constructor for complete configuration control and the ability to override
* global defaults per use case.
* @param autoGrowNullReferences if null references should automatically grow
* @param autoGrowCollections if collections should automatically grow
* @param maximumAutoGrowSize the maximum size that the collection can auto grow
* @param maximumAutoGrowSize the maximum size to which a collection can auto grow
* @see #SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
*/
public SpelParserConfiguration(boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize) {
this(null, null, autoGrowNullReferences, autoGrowCollections, maximumAutoGrowSize);
@@ -104,11 +162,19 @@ public class SpelParserConfiguration {
/**
* Create a new {@code SpelParserConfiguration} instance.
* @param compilerMode the compiler mode that parsers using this configuration object should use
* @param compilerClassLoader the ClassLoader to use as the basis for expression compilation
* <p><strong>NOTE</strong>: Favor the
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
* constructor for complete configuration control and the ability to override
* global defaults per use case.
* @param compilerMode the compiler mode that parsers using this configuration
* should use; or {@code null} to use the default mode
* @param compilerClassLoader the {@code ClassLoader} to use as the basis for
* expression compilation; or {@code null} to use the default {@code ClassLoader}
* @param autoGrowNullReferences if null references should automatically grow
* @param autoGrowCollections if collections should automatically grow
* @param maximumAutoGrowSize the maximum size that the collection can auto grow
* @param maximumAutoGrowSize the maximum size to which a collection can auto grow
* @see #SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
*/
public SpelParserConfiguration(@Nullable SpelCompilerMode compilerMode, @Nullable ClassLoader compilerClassLoader,
boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize) {
@@ -119,24 +185,60 @@ public class SpelParserConfiguration {
/**
* Create a new {@code SpelParserConfiguration} instance.
* @param compilerMode the compiler mode that parsers using this configuration object should use
* @param compilerClassLoader the ClassLoader to use as the basis for expression compilation
* <p><strong>NOTE</strong>: Favor the
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
* constructor for complete configuration control and the ability to override
* global defaults per use case.
* @param compilerMode the compiler mode that parsers using this configuration
* should use; or {@code null} to use the default mode
* @param compilerClassLoader the {@code ClassLoader} to use as the basis for
* expression compilation; or {@code null} to use the default {@code ClassLoader}
* @param autoGrowNullReferences if null references should automatically grow
* @param autoGrowCollections if collections should automatically grow
* @param maximumAutoGrowSize the maximum size that a collection can auto grow
* @param maximumAutoGrowSize the maximum size to which a collection can auto grow
* @param maximumExpressionLength the maximum length of a SpEL expression;
* must be a positive number
* @since 5.2.25
* @see #SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
*/
public SpelParserConfiguration(@Nullable SpelCompilerMode compilerMode, @Nullable ClassLoader compilerClassLoader,
boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize, int maximumExpressionLength) {
this.compilerMode = (compilerMode != null ? compilerMode : defaultCompilerMode);
this((compilerMode != null ? compilerMode : defaultCompilerMode), compilerClassLoader, autoGrowNullReferences,
autoGrowCollections, maximumAutoGrowSize, maximumExpressionLength, retrieveMaxOperations());
}
/**
* Create a new {@code SpelParserConfiguration} instance.
* @param compilerMode the compiler mode that parsers using this configuration
* should use; must not be {@code null}
* @param compilerClassLoader the {@code ClassLoader} to use as the basis for
* expression compilation; or {@code null} to use the default {@code ClassLoader}
* @param autoGrowNullReferences if null references should automatically grow
* @param autoGrowCollections if collections should automatically grow
* @param maximumAutoGrowSize the maximum size to which a collection can auto grow
* @param maximumExpressionLength the maximum length of a SpEL expression;
* must be a positive number
* @param maximumOperations the maximum number of operations permitted during
* SpEL expression evaluation; must be a positive number
* @since 6.2.19
*/
public SpelParserConfiguration(SpelCompilerMode compilerMode, @Nullable ClassLoader compilerClassLoader,
boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize, int maximumExpressionLength,
int maximumOperations) {
Assert.notNull(compilerMode, "'compilerMode' must not be null");
Assert.isTrue(maximumExpressionLength > 0, "'maximumExpressionLength' must be a positive number");
Assert.isTrue(maximumOperations > 0, "'maximumOperations' must be a positive number");
this.compilerMode = compilerMode;
this.compilerClassLoader = compilerClassLoader;
this.autoGrowNullReferences = autoGrowNullReferences;
this.autoGrowCollections = autoGrowCollections;
this.maximumAutoGrowSize = maximumAutoGrowSize;
this.maximumExpressionLength = maximumExpressionLength;
this.maximumOperations = maximumOperations;
}
@@ -148,7 +250,7 @@ public class SpelParserConfiguration {
}
/**
* Return the ClassLoader to use as the basis for expression compilation.
* Return the {@code ClassLoader} to use as the basis for expression compilation.
*/
@Nullable
public ClassLoader getCompilerClassLoader() {
@@ -170,7 +272,7 @@ public class SpelParserConfiguration {
}
/**
* Return the maximum size that a collection can auto grow.
* Return the maximum size to which a collection can auto grow.
*/
public int getMaximumAutoGrowSize() {
return this.maximumAutoGrowSize;
@@ -184,4 +286,32 @@ public class SpelParserConfiguration {
return this.maximumExpressionLength;
}
/**
* Return the maximum number of operations permitted during SpEL expression
* evaluation.
* @since 6.2.19
*/
public int getMaximumOperations() {
return this.maximumOperations;
}
private static int retrieveMaxOperations() {
String value = SpringProperties.getProperty(SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME);
if (!StringUtils.hasText(value)) {
return DEFAULT_MAX_OPERATIONS;
}
try {
int maxOperations = Integer.parseInt(value.trim());
Assert.isTrue(maxOperations > 0, () -> "Value [" + maxOperations + "] for system property [" +
SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME + "] must be positive");
return maxOperations;
}
catch (NumberFormatException ex) {
throw new IllegalArgumentException("Failed to parse value for system property [" +
SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME + "]: " + ex.getMessage(), ex);
}
}
}
@@ -44,6 +44,7 @@ public class Assign extends SpelNodeImpl {
if (!state.getEvaluationContext().isAssignmentEnabled()) {
throw new SpelEvaluationException(getStartPosition(), SpelMessage.NOT_ASSIGNABLE, toStringAST());
}
state.trackOperation();
return this.children[0].setValueInternal(state, () -> this.children[1].getValueInternal(state));
}
@@ -65,6 +65,7 @@ public class BeanReference extends SpelNodeImpl {
getStartPosition(), SpelMessage.NO_BEAN_RESOLVER_REGISTERED, this.beanName);
}
state.trackOperation();
try {
return new TypedValue(beanResolver.resolve(state.getEvaluationContext(), this.beanName));
}
@@ -110,6 +110,7 @@ public class ConstructorReference extends SpelNodeImpl {
*/
@Override
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
state.trackOperation();
if (this.isArrayConstructor) {
return createArray(state);
}
@@ -51,6 +51,7 @@ public class Elvis extends SpelNodeImpl {
*/
@Override
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
state.trackOperation();
TypedValue value = this.children[0].getValueInternal(state);
// If this check is changed, the generateCode method will need changing too
if (value.getValue() != null && !"".equals(value.getValue())) {
@@ -84,6 +84,7 @@ public class FunctionReference extends SpelNodeImpl {
// Note: "javaMethod" cannot be named "method" due to a bug in Checkstyle.
if (function instanceof Method javaMethod) {
try {
state.trackOperation();
return executeFunctionViaMethod(state, javaMethod);
}
catch (SpelEvaluationException ex) {
@@ -95,6 +96,7 @@ public class FunctionReference extends SpelNodeImpl {
// Function registered via a MethodHandle.
if (function instanceof MethodHandle methodHandle) {
try {
state.trackOperation();
return executeFunctionViaMethodHandle(state, methodHandle);
}
catch (SpelEvaluationException ex) {
@@ -220,14 +220,14 @@ public class Indexer extends SpelNodeImpl {
if (target.getClass().isArray()) {
int intIndex = convertIndexToInt(state, index);
this.indexedType = IndexedType.ARRAY;
return new ArrayIndexingValueRef(state.getTypeConverter(), target, intIndex, targetDescriptor);
return new ArrayIndexingValueRef(state, state.getTypeConverter(), target, intIndex, targetDescriptor);
}
// Indexing into a List
if (target instanceof List<?> list) {
int intIndex = convertIndexToInt(state, index);
this.indexedType = IndexedType.LIST;
return new CollectionIndexingValueRef(list, intIndex, targetDescriptor,
return new CollectionIndexingValueRef(state, list, intIndex, targetDescriptor,
state.getTypeConverter(), state.getConfiguration().isAutoGrowCollections(),
state.getConfiguration().getMaximumAutoGrowSize());
}
@@ -240,14 +240,14 @@ public class Indexer extends SpelNodeImpl {
key = state.convertValue(key, mapKeyTypeDescriptor);
}
this.indexedType = IndexedType.MAP;
return new MapIndexingValueRef(state.getTypeConverter(), map, key, targetDescriptor);
return new MapIndexingValueRef(state, state.getTypeConverter(), map, key, targetDescriptor);
}
// Indexing into a String
if (target instanceof String string) {
int intIndex = convertIndexToInt(state, index);
this.indexedType = IndexedType.STRING;
return new StringIndexingValueRef(string, intIndex, targetDescriptor);
return new StringIndexingValueRef(state, string, intIndex, targetDescriptor);
}
// Check for a custom IndexAccessor.
@@ -259,7 +259,7 @@ public class Indexer extends SpelNodeImpl {
for (IndexAccessor indexAccessor : accessorsToTry) {
if (indexAccessor.canRead(evalContext, target, index)) {
this.indexedType = IndexedType.CUSTOM;
return new IndexAccessorValueRef(target, index, evalContext, targetDescriptor);
return new IndexAccessorValueRef(state, target, index, evalContext, targetDescriptor);
}
}
}
@@ -274,7 +274,7 @@ public class Indexer extends SpelNodeImpl {
for (IndexAccessor indexAccessor : accessorsToTry) {
if (indexAccessor.canWrite(evalContext, target, index)) {
this.indexedType = IndexedType.CUSTOM;
return new IndexAccessorValueRef(target, index, evalContext, targetDescriptor);
return new IndexAccessorValueRef(state, target, index, evalContext, targetDescriptor);
}
}
}
@@ -288,7 +288,7 @@ public class Indexer extends SpelNodeImpl {
// Fallback indexing support for collections
if (target instanceof Collection<?> collection) {
int intIndex = convertIndexToInt(state, index);
return new CollectionIndexingValueRef(collection, intIndex, targetDescriptor,
return new CollectionIndexingValueRef(state, collection, intIndex, targetDescriptor,
state.getTypeConverter(), state.getConfiguration().isAutoGrowCollections(),
state.getConfiguration().getMaximumAutoGrowSize());
}
@@ -298,7 +298,7 @@ public class Indexer extends SpelNodeImpl {
if (valueType != null && String.class == valueType.getType()) {
this.indexedType = IndexedType.OBJECT;
return new PropertyAccessorValueRef(
target, (String) index, state.getEvaluationContext(), targetDescriptor);
state, target, (String) index, state.getEvaluationContext(), targetDescriptor);
}
throw new SpelEvaluationException(
@@ -502,6 +502,8 @@ public class Indexer extends SpelNodeImpl {
private class ArrayIndexingValueRef implements ValueRef {
private final ExpressionState expressionState;
private final TypeConverter typeConverter;
private final Object array;
@@ -510,7 +512,10 @@ public class Indexer extends SpelNodeImpl {
private final TypeDescriptor typeDescriptor;
ArrayIndexingValueRef(TypeConverter typeConverter, Object array, int index, TypeDescriptor typeDescriptor) {
ArrayIndexingValueRef(ExpressionState expressionState, TypeConverter typeConverter,
Object array, int index, TypeDescriptor typeDescriptor) {
this.expressionState = expressionState;
this.typeConverter = typeConverter;
this.array = array;
this.index = index;
@@ -536,6 +541,7 @@ public class Indexer extends SpelNodeImpl {
}
private Object getArrayElement(Object ctx, int idx) throws SpelEvaluationException {
this.expressionState.trackOperation();
Class<?> arrayComponentType = ctx.getClass().componentType();
if (arrayComponentType == boolean.class) {
boolean[] array = (boolean[]) ctx;
@@ -606,6 +612,7 @@ public class Indexer extends SpelNodeImpl {
private void setArrayElement(TypeConverter converter, Object ctx, int idx, @Nullable Object newValue,
Class<?> arrayComponentType) throws EvaluationException {
this.expressionState.trackOperation();
if (arrayComponentType == boolean.class) {
boolean[] array = (boolean[]) ctx;
checkAccess(array.length, idx);
@@ -669,13 +676,14 @@ public class Indexer extends SpelNodeImpl {
}
return result;
}
}
@SuppressWarnings({"rawtypes", "unchecked"})
private class MapIndexingValueRef implements ValueRef {
private final ExpressionState expressionState;
private final TypeConverter typeConverter;
private final Map map;
@@ -685,9 +693,10 @@ public class Indexer extends SpelNodeImpl {
private final TypeDescriptor mapEntryDescriptor;
public MapIndexingValueRef(
TypeConverter typeConverter, Map map, @Nullable Object key, TypeDescriptor mapEntryDescriptor) {
public MapIndexingValueRef(ExpressionState expressionState, TypeConverter typeConverter,
Map map, @Nullable Object key, TypeDescriptor mapEntryDescriptor) {
this.expressionState = expressionState;
this.typeConverter = typeConverter;
this.map = map;
this.key = key;
@@ -696,6 +705,7 @@ public class Indexer extends SpelNodeImpl {
@Override
public TypedValue getValue() {
this.expressionState.trackOperation();
Object value = this.map.get(this.key);
exitTypeDescriptor = CodeFlow.toDescriptor(Object.class);
return new TypedValue(value, this.mapEntryDescriptor.getMapValueTypeDescriptor(value));
@@ -707,6 +717,7 @@ public class Indexer extends SpelNodeImpl {
newValue = this.typeConverter.convertValue(newValue, TypeDescriptor.forObject(newValue),
this.mapEntryDescriptor.getMapValueTypeDescriptor());
}
this.expressionState.trackOperation();
this.map.put(this.key, newValue);
}
@@ -719,6 +730,8 @@ public class Indexer extends SpelNodeImpl {
private class PropertyAccessorValueRef implements ValueRef {
private final ExpressionState expressionState;
private final Object targetObject;
private final String name;
@@ -727,9 +740,10 @@ public class Indexer extends SpelNodeImpl {
private final TypeDescriptor targetObjectTypeDescriptor;
public PropertyAccessorValueRef(Object targetObject, String name,
public PropertyAccessorValueRef(ExpressionState expressionState, Object targetObject, String name,
EvaluationContext evaluationContext, TypeDescriptor targetObjectTypeDescriptor) {
this.expressionState = expressionState;
this.targetObject = targetObject;
this.name = name;
this.evaluationContext = evaluationContext;
@@ -747,6 +761,7 @@ public class Indexer extends SpelNodeImpl {
// Is it OK to use the cached accessor?
if (cachedPropertyName.equals(this.name) && cachedTargetType.equals(targetType)) {
PropertyAccessor accessor = cachedPropertyReadState.accessor;
this.expressionState.trackOperation();
return accessor.read(this.evaluationContext, this.targetObject, this.name);
}
// If the above code block did not use a cached accessor and return a value,
@@ -761,6 +776,7 @@ public class Indexer extends SpelNodeImpl {
accessor = reflectivePropertyAccessor.createOptimalAccessor(
this.evaluationContext, this.targetObject, this.name);
}
this.expressionState.trackOperation();
TypedValue result = accessor.read(this.evaluationContext, this.targetObject, this.name);
Indexer.this.cachedPropertyReadState = new CachedPropertyState(accessor, targetType, this.name);
if (accessor instanceof CompilablePropertyAccessor compilablePropertyAccessor) {
@@ -789,6 +805,7 @@ public class Indexer extends SpelNodeImpl {
// Is it OK to use the cached accessor?
if (cachedPropertyName.equals(this.name) && cachedTargetType.equals(targetType)) {
PropertyAccessor accessor = cachedPropertyWriteState.accessor;
this.expressionState.trackOperation();
accessor.write(this.evaluationContext, this.targetObject, this.name, newValue);
return;
}
@@ -800,6 +817,7 @@ public class Indexer extends SpelNodeImpl {
AccessorUtils.getAccessorsToTry(targetType, this.evaluationContext.getPropertyAccessors());
for (PropertyAccessor accessor : accessorsToTry) {
if (accessor.canWrite(this.evaluationContext, this.targetObject, this.name)) {
this.expressionState.trackOperation();
accessor.write(this.evaluationContext, this.targetObject, this.name, newValue);
Indexer.this.cachedPropertyWriteState = new CachedPropertyState(accessor, targetType, this.name);
return;
@@ -824,6 +842,8 @@ public class Indexer extends SpelNodeImpl {
@SuppressWarnings({"rawtypes", "unchecked"})
private class CollectionIndexingValueRef implements ValueRef {
private final ExpressionState expressionState;
private final Collection collection;
private final int index;
@@ -836,9 +856,11 @@ public class Indexer extends SpelNodeImpl {
private final int maximumSize;
public CollectionIndexingValueRef(Collection collection, int index, TypeDescriptor collectionEntryDescriptor,
TypeConverter typeConverter, boolean growCollection, int maximumSize) {
public CollectionIndexingValueRef(ExpressionState expressionState, Collection collection, int index,
TypeDescriptor collectionEntryDescriptor, TypeConverter typeConverter,
boolean growCollection, int maximumSize) {
this.expressionState = expressionState;
this.collection = collection;
this.index = index;
this.collectionEntryDescriptor = collectionEntryDescriptor;
@@ -851,6 +873,7 @@ public class Indexer extends SpelNodeImpl {
public TypedValue getValue() {
growCollectionIfNecessary();
if (this.collection instanceof List list) {
this.expressionState.trackOperation();
Object o = list.get(this.index);
exitTypeDescriptor = CodeFlow.toDescriptor(Object.class);
return new TypedValue(o, this.collectionEntryDescriptor.elementTypeDescriptor(o));
@@ -858,6 +881,7 @@ public class Indexer extends SpelNodeImpl {
int pos = 0;
for (Object o : this.collection) {
this.expressionState.trackOperation();
if (pos == this.index) {
return new TypedValue(o, this.collectionEntryDescriptor.elementTypeDescriptor(o));
}
@@ -879,6 +903,7 @@ public class Indexer extends SpelNodeImpl {
newValue = this.typeConverter.convertValue(newValue, TypeDescriptor.forObject(newValue),
this.collectionEntryDescriptor.getElementTypeDescriptor());
}
this.expressionState.trackOperation();
list.set(this.index, newValue);
}
@@ -900,6 +925,7 @@ public class Indexer extends SpelNodeImpl {
Constructor<?> ctor = getDefaultConstructor(elementType.getType());
int newElements = this.index - this.collection.size();
while (newElements >= 0) {
this.expressionState.trackOperation();
// Insert a null value if the element type does not have a default constructor.
this.collection.add(ctor != null ? ctor.newInstance() : null);
newElements--;
@@ -930,13 +956,18 @@ public class Indexer extends SpelNodeImpl {
private class StringIndexingValueRef implements ValueRef {
private final ExpressionState expressionState;
private final String target;
private final int index;
private final TypeDescriptor typeDescriptor;
public StringIndexingValueRef(String target, int index, TypeDescriptor typeDescriptor) {
public StringIndexingValueRef(
ExpressionState expressionState, String target, int index, TypeDescriptor typeDescriptor) {
this.expressionState = expressionState;
this.target = target;
this.index = index;
this.typeDescriptor = typeDescriptor;
@@ -948,6 +979,7 @@ public class Indexer extends SpelNodeImpl {
throw new SpelEvaluationException(getStartPosition(), SpelMessage.STRING_INDEX_OUT_OF_BOUNDS,
this.target.length(), this.index);
}
this.expressionState.trackOperation();
return new TypedValue(String.valueOf(this.target.charAt(this.index)));
}
@@ -966,6 +998,8 @@ public class Indexer extends SpelNodeImpl {
private class IndexAccessorValueRef implements ValueRef {
private final ExpressionState expressionState;
private final Object target;
private final Object index;
@@ -975,9 +1009,10 @@ public class Indexer extends SpelNodeImpl {
private final TypeDescriptor typeDescriptor;
IndexAccessorValueRef(Object target, Object index, EvaluationContext evaluationContext,
TypeDescriptor typeDescriptor) {
IndexAccessorValueRef(ExpressionState expressionState, Object target, Object index,
EvaluationContext evaluationContext, TypeDescriptor typeDescriptor) {
this.expressionState = expressionState;
this.target = target;
this.index = index;
this.evaluationContext = evaluationContext;
@@ -999,6 +1034,7 @@ public class Indexer extends SpelNodeImpl {
IndexAccessor accessor = cachedIndexReadState.accessor;
if (this.evaluationContext.getIndexAccessors().contains(accessor)) {
try {
this.expressionState.trackOperation();
return accessor.read(this.evaluationContext, this.target, this.index);
}
catch (Exception ex) {
@@ -1017,6 +1053,7 @@ public class Indexer extends SpelNodeImpl {
AccessorUtils.getAccessorsToTry(this.target, this.evaluationContext.getIndexAccessors());
for (IndexAccessor indexAccessor : accessorsToTry) {
if (indexAccessor.canRead(this.evaluationContext, this.target, this.index)) {
this.expressionState.trackOperation();
TypedValue result = indexAccessor.read(this.evaluationContext, this.target, this.index);
Indexer.this.cachedIndexReadState = new CachedIndexState(indexAccessor, targetType, this.index);
if (indexAccessor instanceof CompilableIndexAccessor compilableIndexAccessor) {
@@ -1053,6 +1090,7 @@ public class Indexer extends SpelNodeImpl {
IndexAccessor accessor = cachedIndexWriteState.accessor;
if (this.evaluationContext.getIndexAccessors().contains(accessor)) {
try {
this.expressionState.trackOperation();
accessor.write(this.evaluationContext, this.target, this.index, newValue);
return;
}
@@ -1072,6 +1110,7 @@ public class Indexer extends SpelNodeImpl {
AccessorUtils.getAccessorsToTry(this.target, this.evaluationContext.getIndexAccessors());
for (IndexAccessor indexAccessor : accessorsToTry) {
if (indexAccessor.canWrite(this.evaluationContext, this.target, this.index)) {
this.expressionState.trackOperation();
indexAccessor.write(this.evaluationContext, this.target, this.index, newValue);
Indexer.this.cachedIndexWriteState = new CachedIndexState(indexAccessor, targetType, this.index);
return;
@@ -27,7 +27,7 @@ import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.CodeFlow;
import org.springframework.expression.spel.ExpressionState;
import org.springframework.expression.spel.SpelNode;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -42,69 +42,64 @@ import org.springframework.util.Assert;
*/
public class InlineList extends SpelNodeImpl {
private final boolean isConstant;
@Nullable
private final TypedValue constant;
private volatile TypedValue constant;
public InlineList(int startPos, int endPos, SpelNodeImpl... args) {
super(startPos, endPos, args);
this.constant = computeConstantValue();
this.isConstant = determineIfConstant();
}
/**
* If all the components of the list are constants, or lists that themselves
* contain constants, then a constant list can be built to represent this node.
* <p>This will speed up later getValue calls and reduce the amount of garbage
* created.
* Determine whether this list is structurally eligible to be a constant
* value: whether all of its components are themselves constants or lists
* that contain only constants.
* <p>The actual constant value is created lazily on the first call to
* {@link #getValueInternal(ExpressionState)}.
*/
@Nullable
private TypedValue computeConstantValue() {
private boolean determineIfConstant() {
for (int c = 0, max = getChildCount(); c < max; c++) {
SpelNode child = getChild(c);
if (!(child instanceof Literal)) {
if (child instanceof InlineList inlineList) {
if (!inlineList.isConstant()) {
return null;
}
}
else if (!(child instanceof OpMinus opMinus) || !opMinus.isNegativeNumberLiteral()) {
return null;
}
if (child instanceof Literal) {
continue;
}
if (child instanceof InlineList inlineList && inlineList.isConstant()) {
continue;
}
if (child instanceof OpMinus opMinus && opMinus.isNegativeNumberLiteral()) {
continue;
}
return false;
}
List<Object> constantList = new ArrayList<>();
int childcount = getChildCount();
ExpressionState expressionState = new ExpressionState(new StandardEvaluationContext());
for (int c = 0; c < childcount; c++) {
SpelNode child = getChild(c);
if (child instanceof Literal literal) {
constantList.add(literal.getLiteralValue().getValue());
}
else if (child instanceof InlineList inlineList) {
constantList.add(inlineList.getConstantValue());
}
else if (child instanceof OpMinus) {
constantList.add(child.getValue(expressionState));
}
}
return new TypedValue(Collections.unmodifiableList(constantList));
return true;
}
@Override
public TypedValue getValueInternal(ExpressionState expressionState) throws EvaluationException {
if (this.constant != null) {
return this.constant;
TypedValue result = this.constant;
if (result != null) {
return result;
}
else {
int childCount = getChildCount();
List<Object> returnValue = new ArrayList<>(childCount);
for (int c = 0; c < childCount; c++) {
returnValue.add(getChild(c).getValue(expressionState));
}
return new TypedValue(returnValue);
result = createList(expressionState);
if (this.isConstant) {
this.constant = result;
}
return result;
}
private TypedValue createList(ExpressionState expressionState) throws EvaluationException {
int childCount = getChildCount();
expressionState.trackOperation();
List<Object> list = new ArrayList<>(childCount);
for (int c = 0; c < childCount; c++) {
expressionState.trackOperation();
list.add(getChild(c).getValue(expressionState));
}
return new TypedValue(this.isConstant ? Collections.unmodifiableList(list) : list);
}
@Override
@@ -118,22 +113,38 @@ public class InlineList extends SpelNodeImpl {
}
/**
* Return whether this list is a constant value.
* Return whether this list is structurally a constant value.
* <p>Note that the resulting constant value is created lazily on the
* first call to {@link #getValueInternal(ExpressionState)} or
* {@link #getConstantValue()}.
*/
public boolean isConstant() {
return (this.constant != null);
return this.isConstant;
}
/**
* Return the cached constant {@link List} value for this inline list,
* lazily creating it on first access.
* @see #isConstant()
* @deprecated as of Spring Framework 6.2.19; this method was only intended for
* testing purposes and will be removed in a future version of the framework
*/
@SuppressWarnings("unchecked")
@Deprecated(since = "6.2.19")
@Nullable
public List<Object> getConstantValue() {
Assert.state(this.constant != null, "No constant");
return (List<Object>) this.constant.getValue();
Assert.state(this.isConstant, "Not a constant");
TypedValue result = this.constant;
if (result == null) {
result = createList(new ExpressionState(SimpleEvaluationContext.forReadOnlyDataBinding().build()));
this.constant = result;
}
return (List<Object>) result.getValue();
}
@Override
public boolean isCompilable() {
return isConstant();
return this.isConstant;
}
@Override
@@ -24,7 +24,7 @@ import org.springframework.expression.EvaluationException;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.ExpressionState;
import org.springframework.expression.spel.SpelNode;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -39,105 +39,80 @@ import org.springframework.util.Assert;
*/
public class InlineMap extends SpelNodeImpl {
private final boolean isConstant;
@Nullable
private final TypedValue constant;
private volatile TypedValue constant;
public InlineMap(int startPos, int endPos, SpelNodeImpl... args) {
super(startPos, endPos, args);
this.constant = computeConstantValue();
this.isConstant = determineIfConstant();
}
/**
* If all the components of the map are constants, or lists/maps that themselves
* contain constants, then a constant map can be built to represent this node.
* <p>This will speed up later getValue calls and reduce the amount of garbage
* created.
* Determine whether this map is structurally eligible to be a constant
* value: whether all of its components are themselves constants or lists/maps
* that contain only constants.
* <p>The actual constant value is created lazily on the first call to
* {@link #getValueInternal(ExpressionState)}.
*/
@Nullable
private TypedValue computeConstantValue() {
for (int c = 0, max = getChildCount(); c < max; c++) {
SpelNode child = getChild(c);
if (!(child instanceof Literal)) {
if (child instanceof InlineList inlineList) {
if (!inlineList.isConstant()) {
return null;
}
}
else if (child instanceof InlineMap inlineMap) {
if (!inlineMap.isConstant()) {
return null;
}
}
else if (!(c % 2 == 0 && child instanceof PropertyOrFieldReference)) {
if (!(child instanceof OpMinus opMinus) || !opMinus.isNegativeNumberLiteral()) {
return null;
}
}
}
}
Map<Object, Object> constantMap = new LinkedHashMap<>();
private boolean determineIfConstant() {
int childCount = getChildCount();
ExpressionState expressionState = new ExpressionState(new StandardEvaluationContext());
for (int c = 0; c < childCount; c++) {
SpelNode keyChild = getChild(c++);
Object key;
if (keyChild instanceof Literal literal) {
key = literal.getLiteralValue().getValue();
SpelNode child = getChild(c);
if (child instanceof Literal) {
continue;
}
else if (keyChild instanceof PropertyOrFieldReference propertyOrFieldReference) {
key = propertyOrFieldReference.getName();
if (child instanceof InlineList inlineList && inlineList.isConstant()) {
continue;
}
else if (keyChild instanceof OpMinus) {
key = keyChild.getValue(expressionState);
if (child instanceof InlineMap inlineMap && inlineMap.isConstant()) {
continue;
}
else {
return null;
if (c % 2 == 0 && child instanceof PropertyOrFieldReference) {
continue;
}
SpelNode valueChild = getChild(c);
Object value = null;
if (valueChild instanceof Literal literal) {
value = literal.getLiteralValue().getValue();
if (child instanceof OpMinus opMinus && opMinus.isNegativeNumberLiteral()) {
continue;
}
else if (valueChild instanceof InlineList inlineList) {
value = inlineList.getConstantValue();
}
else if (valueChild instanceof InlineMap inlineMap) {
value = inlineMap.getConstantValue();
}
else if (valueChild instanceof OpMinus) {
value = valueChild.getValue(expressionState);
}
constantMap.put(key, value);
return false;
}
return new TypedValue(Collections.unmodifiableMap(constantMap));
return true;
}
@Override
public TypedValue getValueInternal(ExpressionState expressionState) throws EvaluationException {
if (this.constant != null) {
return this.constant;
TypedValue result = this.constant;
if (result != null) {
return result;
}
else {
Map<Object, Object> returnValue = new LinkedHashMap<>();
int childcount = getChildCount();
for (int c = 0; c < childcount; c++) {
SpelNode keyChild = getChild(c++);
Object key = null;
if (keyChild instanceof PropertyOrFieldReference reference) {
key = reference.getName();
}
else {
key = keyChild.getValue(expressionState);
}
Object value = getChild(c).getValue(expressionState);
returnValue.put(key, value);
result = createMap(expressionState);
if (this.isConstant) {
this.constant = result;
}
return result;
}
private TypedValue createMap(ExpressionState expressionState) throws EvaluationException {
expressionState.trackOperation();
Map<Object, Object> map = new LinkedHashMap<>();
int childCount = getChildCount();
for (int c = 0; c < childCount; c++) {
SpelNode keyChild = getChild(c++);
Object key;
if (keyChild instanceof PropertyOrFieldReference reference) {
key = reference.getName();
}
return new TypedValue(returnValue);
else {
key = keyChild.getValue(expressionState);
}
Object value = getChild(c).getValue(expressionState);
expressionState.trackOperation();
map.put(key, value);
}
return new TypedValue(this.isConstant ? Collections.unmodifiableMap(map) : map);
}
@Override
@@ -156,17 +131,33 @@ public class InlineMap extends SpelNodeImpl {
}
/**
* Return whether this map is a constant value.
* Return whether this map is structurally a constant value.
* <p>Note that the resulting constant value is created lazily on the
* first call to {@link #getValueInternal(ExpressionState)} or
* {@link #getConstantValue()}.
*/
public boolean isConstant() {
return this.constant != null;
return this.isConstant;
}
/**
* Return the cached constant {@link Map} value for this inline map,
* lazily creating it on first access.
* @see #isConstant()
* @deprecated as of Spring Framework 6.2.19; this method was only intended for
* testing purposes and will be removed in a future version of the framework
*/
@SuppressWarnings("unchecked")
@Deprecated(since = "6.2.19")
@Nullable
public Map<Object, Object> getConstantValue() {
Assert.state(this.constant != null, "No constant");
return (Map<Object, Object>) this.constant.getValue();
Assert.state(this.isConstant, "Not a constant");
TypedValue result = this.constant;
if (result == null) {
result = createMap(new ExpressionState(SimpleEvaluationContext.forReadOnlyDataBinding().build()));
this.constant = result;
}
return (Map<Object, Object>) result.getValue();
}
}
@@ -105,12 +105,12 @@ public class MethodReference extends SpelNodeImpl {
Object value = state.getActiveContextObject().getValue();
TypeDescriptor targetType = state.getActiveContextObject().getTypeDescriptor();
Object[] arguments = getArguments(state);
TypedValue result = getValueInternal(evaluationContext, value, targetType, arguments);
TypedValue result = getValueInternal(state, evaluationContext, value, targetType, arguments);
updateExitTypeDescriptor();
return result;
}
private TypedValue getValueInternal(EvaluationContext evaluationContext,
private TypedValue getValueInternal(ExpressionState state, EvaluationContext evaluationContext,
@Nullable Object value, @Nullable TypeDescriptor targetType, Object[] arguments) {
List<TypeDescriptor> argumentTypes = getArgumentTypes(arguments);
@@ -122,6 +122,7 @@ public class MethodReference extends SpelNodeImpl {
MethodExecutor executorToUse = getCachedExecutor(evaluationContext, value, targetType, argumentTypes);
if (executorToUse != null) {
try {
state.trackOperation();
return executorToUse.execute(evaluationContext, value, arguments);
}
catch (AccessException ex) {
@@ -149,6 +150,7 @@ public class MethodReference extends SpelNodeImpl {
this.cachedExecutor = new CachedMethodExecutor(
executorToUse, (value instanceof Class<?> clazz ? clazz : null), targetType, argumentTypes);
try {
state.trackOperation();
return executorToUse.execute(evaluationContext, value, arguments);
}
catch (AccessException ex) {
@@ -376,6 +378,8 @@ public class MethodReference extends SpelNodeImpl {
private class MethodValueRef implements ValueRef {
private final ExpressionState expressionState;
private final EvaluationContext evaluationContext;
@Nullable
@@ -387,6 +391,7 @@ public class MethodReference extends SpelNodeImpl {
private final Object[] arguments;
public MethodValueRef(ExpressionState state, Object[] arguments) {
this.expressionState = state;
this.evaluationContext = state.getEvaluationContext();
this.value = state.getActiveContextObject().getValue();
this.targetType = state.getActiveContextObject().getTypeDescriptor();
@@ -396,7 +401,7 @@ public class MethodReference extends SpelNodeImpl {
@Override
public TypedValue getValue() {
TypedValue result = MethodReference.this.getValueInternal(
this.evaluationContext, this.value, this.targetType, this.arguments);
this.expressionState, this.evaluationContext, this.value, this.targetType, this.arguments);
updateExitTypeDescriptor();
return result;
}
@@ -46,6 +46,7 @@ public class OpAnd extends Operator {
@Override
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
state.trackOperation();
if (!getBooleanValue(state, getLeftOperand())) {
// no need to evaluate right operand
return BooleanTypedValue.FALSE;
@@ -68,6 +68,7 @@ public class OpDec extends Operator {
TypedValue newValue = null;
if (operandValue instanceof Number op1) {
state.trackOperation();
if (op1 instanceof BigDecimal bigDecimal) {
newValue = new TypedValue(bigDecimal.subtract(BigDecimal.ONE), operandTypedValue.getTypeDescriptor());
}
@@ -114,8 +115,9 @@ public class OpDec extends Operator {
}
}
// set the new value
state.trackOperation();
try {
// set the new value
lvalue.setValue(newValue.getValue());
}
catch (SpelEvaluationException see) {
@@ -51,6 +51,7 @@ public class OpDivide extends Operator {
Object rightOperand = getRightOperand().getValueInternal(state).getValue();
if (leftOperand instanceof Number leftNumber && rightOperand instanceof Number rightNumber) {
state.trackOperation();
if (leftNumber instanceof BigDecimal || rightNumber instanceof BigDecimal) {
BigDecimal leftBigDecimal = NumberUtils.convertNumberToTargetClass(leftNumber, BigDecimal.class);
BigDecimal rightBigDecimal = NumberUtils.convertNumberToTargetClass(rightNumber, BigDecimal.class);
@@ -43,6 +43,7 @@ public class OpEQ extends Operator {
Object right = getRightOperand().getValueInternal(state).getValue();
this.leftActualDescriptor = CodeFlow.toDescriptorFromObject(left);
this.rightActualDescriptor = CodeFlow.toDescriptorFromObject(right);
state.trackOperation();
return BooleanTypedValue.forValue(equalityCheck(state.getEvaluationContext(), left, right));
}
@@ -46,6 +46,7 @@ public class OpGE extends Operator {
public BooleanTypedValue getValueInternal(ExpressionState state) throws EvaluationException {
Object left = getLeftOperand().getValueInternal(state).getValue();
Object right = getRightOperand().getValueInternal(state).getValue();
state.trackOperation();
this.leftActualDescriptor = CodeFlow.toDescriptorFromObject(left);
this.rightActualDescriptor = CodeFlow.toDescriptorFromObject(right);
@@ -46,6 +46,7 @@ public class OpGT extends Operator {
public BooleanTypedValue getValueInternal(ExpressionState state) throws EvaluationException {
Object left = getLeftOperand().getValueInternal(state).getValue();
Object right = getRightOperand().getValueInternal(state).getValue();
state.trackOperation();
this.leftActualDescriptor = CodeFlow.toDescriptorFromObject(left);
this.rightActualDescriptor = CodeFlow.toDescriptorFromObject(right);
@@ -66,6 +66,7 @@ public class OpInc extends Operator {
TypedValue newValue = null;
if (value instanceof Number op1) {
state.trackOperation();
if (op1 instanceof BigDecimal bigDecimal) {
newValue = new TypedValue(bigDecimal.add(BigDecimal.ONE), typedValue.getTypeDescriptor());
}
@@ -110,8 +111,9 @@ public class OpInc extends Operator {
}
}
// set the new value
state.trackOperation();
try {
// set the new value
valueRef.setValue(newValue.getValue());
}
catch (SpelEvaluationException see) {
@@ -46,6 +46,7 @@ public class OpLE extends Operator {
public BooleanTypedValue getValueInternal(ExpressionState state) throws EvaluationException {
Object left = getLeftOperand().getValueInternal(state).getValue();
Object right = getRightOperand().getValueInternal(state).getValue();
state.trackOperation();
this.leftActualDescriptor = CodeFlow.toDescriptorFromObject(left);
this.rightActualDescriptor = CodeFlow.toDescriptorFromObject(right);
@@ -46,6 +46,7 @@ public class OpLT extends Operator {
public BooleanTypedValue getValueInternal(ExpressionState state) throws EvaluationException {
Object left = getLeftOperand().getValueInternal(state).getValue();
Object right = getRightOperand().getValueInternal(state).getValue();
state.trackOperation();
this.leftActualDescriptor = CodeFlow.toDescriptorFromObject(left);
this.rightActualDescriptor = CodeFlow.toDescriptorFromObject(right);
@@ -51,6 +51,13 @@ public class OpMinus extends Operator {
public OpMinus(int startPos, int endPos, SpelNodeImpl... operands) {
super("-", startPos, endPos, operands);
// If this is a unary negation of a number literal, the exit type descriptor
// can be derived statically from the literal. Doing so up front lets the
// compiler determine that this node is compilable without first evaluating
// the expression.
if (isNegativeNumberLiteral()) {
this.exitTypeDescriptor = ((Literal) operands[0]).exitTypeDescriptor;
}
}
@@ -72,6 +79,7 @@ public class OpMinus extends Operator {
if (this.children.length < 2) { // if only one operand, then this is unary minus
Object operand = leftOp.getValueInternal(state).getValue();
if (operand instanceof Number number) {
state.trackOperation();
if (number instanceof BigDecimal bigDecimal) {
return new TypedValue(bigDecimal.negate());
}
@@ -112,6 +120,7 @@ public class OpMinus extends Operator {
Object right = getRightOperand().getValueInternal(state).getValue();
if (left instanceof Number leftNumber && right instanceof Number rightNumber) {
state.trackOperation();
if (leftNumber instanceof BigDecimal || rightNumber instanceof BigDecimal) {
BigDecimal leftBigDecimal = NumberUtils.convertNumberToTargetClass(leftNumber, BigDecimal.class);
BigDecimal rightBigDecimal = NumberUtils.convertNumberToTargetClass(rightNumber, BigDecimal.class);
@@ -145,6 +154,7 @@ public class OpMinus extends Operator {
}
if (left instanceof String theString && right instanceof Integer theInteger && theString.length() == 1) {
state.trackOperation();
// Implements character - int (ie. b - 1 = a)
return new TypedValue(Character.toString((char) (theString.charAt(0) - theInteger)));
}
@@ -49,6 +49,7 @@ public class OpModulus extends Operator {
Object rightOperand = getRightOperand().getValueInternal(state).getValue();
if (leftOperand instanceof Number leftNumber && rightOperand instanceof Number rightNumber) {
state.trackOperation();
if (leftNumber instanceof BigDecimal || rightNumber instanceof BigDecimal) {
BigDecimal leftBigDecimal = NumberUtils.convertNumberToTargetClass(leftNumber, BigDecimal.class);
BigDecimal rightBigDecimal = NumberUtils.convertNumberToTargetClass(rightNumber, BigDecimal.class);
@@ -82,6 +82,7 @@ public class OpMultiply extends Operator {
Object rightOperand = getRightOperand().getValueInternal(state).getValue();
if (leftOperand instanceof Number leftNumber && rightOperand instanceof Number rightNumber) {
state.trackOperation();
if (leftNumber instanceof BigDecimal || rightNumber instanceof BigDecimal) {
BigDecimal leftBigDecimal = NumberUtils.convertNumberToTargetClass(leftNumber, BigDecimal.class);
BigDecimal rightBigDecimal = NumberUtils.convertNumberToTargetClass(rightNumber, BigDecimal.class);
@@ -116,6 +117,7 @@ public class OpMultiply extends Operator {
if (leftOperand instanceof String text && rightOperand instanceof Integer count) {
checkRepeatedTextSize(text, count);
state.trackOperation();
return new TypedValue(text.repeat(count));
}
@@ -127,9 +129,9 @@ public class OpMultiply extends Operator {
throw new SpelEvaluationException(getStartPosition(),
SpelMessage.NEGATIVE_REPEATED_TEXT_COUNT, count);
}
int result = text.length() * count;
long result = (long) text.length() * (long) count;
if (result < 0 || result > MAX_REPEATED_TEXT_SIZE) {
throw new SpelEvaluationException(getStartPosition(),
throw new SpelEvaluationException(getRightOperand().getStartPosition(),
SpelMessage.MAX_REPEATED_TEXT_SIZE_EXCEEDED, MAX_REPEATED_TEXT_SIZE);
}
}
@@ -44,6 +44,7 @@ public class OpNE extends Operator {
Object rightValue = getRightOperand().getValueInternal(state).getValue();
this.leftActualDescriptor = CodeFlow.toDescriptorFromObject(leftValue);
this.rightActualDescriptor = CodeFlow.toDescriptorFromObject(rightValue);
state.trackOperation();
return BooleanTypedValue.forValue(!equalityCheck(state.getEvaluationContext(), leftValue, rightValue));
}
@@ -45,6 +45,7 @@ public class OpOr extends Operator {
@Override
public BooleanTypedValue getValueInternal(ExpressionState state) throws EvaluationException {
state.trackOperation();
if (getBooleanValue(state, getLeftOperand())) {
// no need to evaluate right operand
return BooleanTypedValue.TRUE;
@@ -73,6 +73,7 @@ public class OpPlus extends Operator {
if (this.children.length < 2) { // if only one operand, then this is unary plus
Object operandOne = leftOp.getValueInternal(state).getValue();
if (operandOne instanceof Number) {
state.trackOperation();
if (operandOne instanceof Double) {
this.exitTypeDescriptor = "D";
}
@@ -96,6 +97,7 @@ public class OpPlus extends Operator {
Object rightOperand = operandTwoValue.getValue();
if (leftOperand instanceof Number leftNumber && rightOperand instanceof Number rightNumber) {
state.trackOperation();
if (leftNumber instanceof BigDecimal || rightNumber instanceof BigDecimal) {
BigDecimal leftBigDecimal = NumberUtils.convertNumberToTargetClass(leftNumber, BigDecimal.class);
BigDecimal rightBigDecimal = NumberUtils.convertNumberToTargetClass(rightNumber, BigDecimal.class);
@@ -132,37 +134,41 @@ public class OpPlus extends Operator {
this.exitTypeDescriptor = "Ljava/lang/String";
checkStringLength(leftString);
checkStringLength(rightString);
return concatenate(leftString, rightString);
return concatenate(state, leftString, rightString);
}
if (leftOperand instanceof String leftString) {
checkStringLength(leftString);
String rightString = (rightOperand == null ? "null" : convertTypedValueToString(operandTwoValue, state));
checkStringLength(rightString);
return concatenate(leftString, rightString);
return concatenate(state, leftString, rightString);
}
if (rightOperand instanceof String rightString) {
checkStringLength(rightString);
String leftString = (leftOperand == null ? "null" : convertTypedValueToString(operandOneValue, state));
checkStringLength(leftString);
return concatenate(leftString, rightString);
return concatenate(state, leftString, rightString);
}
return state.operate(Operation.ADD, leftOperand, rightOperand);
}
private void checkStringLength(String string) {
if (string.length() > MAX_CONCATENATED_STRING_LENGTH) {
checkStringLength(string.length());
}
private void checkStringLength(int stringLength) {
if (stringLength > MAX_CONCATENATED_STRING_LENGTH) {
throw new SpelEvaluationException(getStartPosition(),
SpelMessage.MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, MAX_CONCATENATED_STRING_LENGTH);
}
}
private TypedValue concatenate(String leftString, String rightString) {
String result = leftString + rightString;
checkStringLength(result);
return new TypedValue(result);
private TypedValue concatenate(ExpressionState state, String leftString, String rightString) {
checkStringLength(leftString.length() + rightString.length());
state.trackOperation();
return new TypedValue(leftString + rightString);
}
@Override
@@ -68,6 +68,7 @@ public class OperatorBetween extends Operator {
Object high = list.get(1);
TypeComparator comp = state.getTypeComparator();
try {
state.trackOperation();
return BooleanTypedValue.forValue(comp.compare(left, low) >= 0 && comp.compare(left, high) <= 0);
}
catch (SpelEvaluationException ex) {
@@ -62,6 +62,7 @@ public class OperatorInstanceof extends Operator {
Object leftValue = left.getValue();
Object rightValue = right.getValue();
BooleanTypedValue result;
state.trackOperation();
if (!(rightValue instanceof Class<?> rightClass)) {
throw new SpelEvaluationException(getRightOperand().getStartPosition(),
SpelMessage.INSTANCEOF_OPERATOR_NEEDS_CLASS_OPERAND,
@@ -16,7 +16,6 @@
package org.springframework.expression.spel.ast;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -27,6 +26,7 @@ import org.springframework.expression.spel.ExpressionState;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.SpelMessage;
import org.springframework.expression.spel.support.BooleanTypedValue;
import org.springframework.util.ConcurrentLruCache;
/**
* Implements the matches operator. Matches takes two operands:
@@ -41,6 +41,12 @@ import org.springframework.expression.spel.support.BooleanTypedValue;
*/
public class OperatorMatches extends Operator {
/**
* Maximum number of compiled regular expressions in the pattern cache: {@value}.
* @since 6.2.19
*/
public static final int MAX_PATTERN_CACHE_SIZE = 256;
private static final int PATTERN_ACCESS_THRESHOLD = 1000000;
/**
@@ -49,29 +55,47 @@ public class OperatorMatches extends Operator {
*/
private static final int MAX_REGEX_LENGTH = 1000;
private final ConcurrentMap<String, Pattern> patternCache;
private final ConcurrentLruCache<String, Pattern> patternCache;
/**
* Create a new {@link OperatorMatches} instance.
* @deprecated as of Spring Framework 5.2.23 in favor of invoking
* {@link #OperatorMatches(ConcurrentMap, int, int, SpelNodeImpl...)}
* with a shared pattern cache instead
* @deprecated as of Spring Framework 5.2.23; for removal in Spring Framework 7.1; invoke
* {@link #OperatorMatches(ConcurrentLruCache, int, int, SpelNodeImpl...)} instead
*/
@Deprecated(since = "5.2.23")
@Deprecated(since = "5.2.23", forRemoval = true)
public OperatorMatches(int startPos, int endPos, SpelNodeImpl... operands) {
this(new ConcurrentHashMap<>(), startPos, endPos, operands);
this(new ConcurrentLruCache<>(MAX_PATTERN_CACHE_SIZE, Pattern::compile), startPos, endPos, operands);
}
/**
* Create a new {@link OperatorMatches} instance with a shared pattern cache.
* <p>As of Spring Framework 6.2.19, the supplied {@code patternCacheMap} will
* be ignored.
* @since 5.2.23
* @deprecated as of Spring Framework 6.2.19; for removal in Spring Framework 7.1; invoke
* {@link #OperatorMatches(ConcurrentLruCache, int, int, SpelNodeImpl...)} instead
*/
public OperatorMatches(ConcurrentMap<String, Pattern> patternCache, int startPos, int endPos, SpelNodeImpl... operands) {
@Deprecated(since = "6.2.19", forRemoval = true)
public OperatorMatches(ConcurrentMap<String, Pattern> patternCacheMap,
int startPos, int endPos, SpelNodeImpl... operands) {
this(startPos, endPos, operands);
}
/**
* Create a new {@link OperatorMatches} instance with a shared pattern cache.
* @since 6.2.19
*/
public OperatorMatches(ConcurrentLruCache<String, Pattern> patternCache,
int startPos, int endPos, SpelNodeImpl... operands) {
super("matches", startPos, endPos, operands);
this.patternCache = patternCache;
}
/**
* Check the first operand matches the regex specified as the second operand.
* @param state the expression state
@@ -96,14 +120,14 @@ public class OperatorMatches extends Operator {
throw new SpelEvaluationException(rightOp.getStartPosition(),
SpelMessage.INVALID_SECOND_OPERAND_FOR_MATCHES_OPERATOR, right);
}
if (regex.length() > MAX_REGEX_LENGTH) {
throw new SpelEvaluationException(rightOp.getStartPosition(),
SpelMessage.MAX_REGEX_LENGTH_EXCEEDED, MAX_REGEX_LENGTH);
}
state.trackOperation();
try {
Pattern pattern = this.patternCache.get(regex);
if (pattern == null) {
checkRegexLength(regex);
pattern = Pattern.compile(regex);
this.patternCache.putIfAbsent(regex, pattern);
}
Matcher matcher = pattern.matcher(new MatcherInput(input, new AccessCount()));
return BooleanTypedValue.forValue(matcher.matches());
}
@@ -117,13 +141,6 @@ public class OperatorMatches extends Operator {
}
}
private void checkRegexLength(String regex) {
if (regex.length() > MAX_REGEX_LENGTH) {
throw new SpelEvaluationException(getStartPosition(),
SpelMessage.MAX_REGEX_LENGTH_EXCEEDED, MAX_REGEX_LENGTH);
}
}
private static class AccessCount {
@@ -48,6 +48,7 @@ public class OperatorNot extends SpelNodeImpl { // Not is a unary operator so d
if (value == null) {
throw new SpelEvaluationException(SpelMessage.TYPE_CONVERSION_ERROR, "null", "boolean");
}
state.trackOperation();
return BooleanTypedValue.forValue(!value);
}
catch (SpelEvaluationException ex) {
@@ -48,6 +48,7 @@ public class OperatorPower extends Operator {
Object rightOperand = rightOp.getValueInternal(state).getValue();
if (leftOperand instanceof Number leftNumber && rightOperand instanceof Number rightNumber) {
state.trackOperation();
if (leftNumber instanceof BigDecimal) {
BigDecimal leftBigDecimal = NumberUtils.convertNumberToTargetClass(leftNumber, BigDecimal.class);
return new TypedValue(leftBigDecimal.pow(rightNumber.intValue()));
@@ -79,11 +79,13 @@ public class Projection extends SpelNodeImpl {
// that can be referenced in the operation -- for example,
// {'a':'y', 'b':'n'}.![value == 'y' ? key : null] evaluates to ['a', null].
if (operand instanceof Map<?, ?> mapData) {
state.trackOperation();
List<Object> result = new ArrayList<>();
for (Map.Entry<?, ?> entry : mapData.entrySet()) {
try {
state.pushActiveContextObject(new TypedValue(entry));
state.enterScope();
state.trackOperation();
result.add(this.children[0].getValueInternal(state).getValue());
}
finally {
@@ -99,6 +101,7 @@ public class Projection extends SpelNodeImpl {
Iterable<?> data = (operand instanceof Iterable<?> iterable ?
iterable : Arrays.asList(ObjectUtils.toObjectArray(operand)));
state.trackOperation();
List<Object> result = new ArrayList<>();
Class<?> arrayElementType = null;
for (Object element : data) {
@@ -109,6 +112,7 @@ public class Projection extends SpelNodeImpl {
if (value != null && operandIsArray) {
arrayElementType = determineCommonType(arrayElementType, value.getClass());
}
state.trackOperation();
result.add(value);
}
finally {
@@ -91,14 +91,14 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
@Override
public ValueRef getValueRef(ExpressionState state) throws EvaluationException {
return new AccessorValueRef(this, state.getActiveContextObject(), state.getEvaluationContext(),
return new AccessorValueRef(this, state.getActiveContextObject(), state, state.getEvaluationContext(),
state.getConfiguration().isAutoGrowNullReferences());
}
@Override
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
TypedValue tv = getValueInternal(state.getActiveContextObject(), state.getEvaluationContext(),
state.getConfiguration().isAutoGrowNullReferences());
TypedValue tv = getValueInternal(state, state.getActiveContextObject(),
state.getEvaluationContext(), state.getConfiguration().isAutoGrowNullReferences());
PropertyAccessor accessorToUse = this.cachedReadAccessor;
if (accessorToUse instanceof CompilablePropertyAccessor compilablePropertyAccessor) {
setExitTypeDescriptor(CodeFlow.toDescriptor(compilablePropertyAccessor.getPropertyType()));
@@ -106,10 +106,10 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
return tv;
}
private TypedValue getValueInternal(TypedValue contextObject, EvaluationContext evalContext,
boolean isAutoGrowNullReferences) throws EvaluationException {
private TypedValue getValueInternal(ExpressionState state, TypedValue contextObject,
EvaluationContext evalContext, boolean isAutoGrowNullReferences) throws EvaluationException {
TypedValue result = readProperty(contextObject, evalContext, this.name);
TypedValue result = readProperty(state, contextObject, evalContext, this.name);
// Dynamically create the objects if the user has requested that optional behavior
if (result.getValue() == null && isAutoGrowNullReferences &&
@@ -119,16 +119,18 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
// Create a new collection or map ready for the indexer
if (List.class == resultDescriptor.getType()) {
if (isWritableProperty(this.name, contextObject, evalContext)) {
state.trackOperation();
List<?> newList = new ArrayList<>();
writeProperty(contextObject, evalContext, this.name, newList);
result = readProperty(contextObject, evalContext, this.name);
writeProperty(state, contextObject, evalContext, this.name, newList);
result = readProperty(state, contextObject, evalContext, this.name);
}
}
else if (Map.class == resultDescriptor.getType()) {
if (isWritableProperty(this.name,contextObject, evalContext)) {
state.trackOperation();
Map<?,?> newMap = new HashMap<>();
writeProperty(contextObject, evalContext, this.name, newMap);
result = readProperty(contextObject, evalContext, this.name);
writeProperty(state, contextObject, evalContext, this.name, newMap);
result = readProperty(state, contextObject, evalContext, this.name);
}
}
else {
@@ -136,9 +138,10 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
try {
if (isWritableProperty(this.name,contextObject, evalContext)) {
Class<?> clazz = resultDescriptor.getType();
state.trackOperation();
Object newObject = ReflectionUtils.accessibleConstructor(clazz).newInstance();
writeProperty(contextObject, evalContext, this.name, newObject);
result = readProperty(contextObject, evalContext, this.name);
writeProperty(state, contextObject, evalContext, this.name, newObject);
result = readProperty(state, contextObject, evalContext, this.name);
}
}
catch (InvocationTargetException ex) {
@@ -159,7 +162,7 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
throws EvaluationException {
TypedValue typedValue = valueSupplier.get();
writeProperty(state.getActiveContextObject(), state.getEvaluationContext(), this.name, typedValue.getValue());
writeProperty(state, state.getActiveContextObject(), state.getEvaluationContext(), this.name, typedValue.getValue());
return typedValue;
}
@@ -178,8 +181,8 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
* @return the value of the property
* @throws EvaluationException if any problem accessing the property, or if it cannot be found
*/
private TypedValue readProperty(TypedValue contextObject, EvaluationContext evalContext, String name)
throws EvaluationException {
private TypedValue readProperty(ExpressionState state, TypedValue contextObject,
EvaluationContext evalContext, String name) throws EvaluationException {
Object targetObject = contextObject.getValue();
if (targetObject == null && isNullSafe()) {
@@ -190,6 +193,7 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
if (accessorToUse != null) {
if (evalContext.getPropertyAccessors().contains(accessorToUse)) {
try {
state.trackOperation();
return accessorToUse.read(evalContext, targetObject, name);
}
catch (Exception ex) {
@@ -213,11 +217,15 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
evalContext, targetObject, name);
}
this.cachedReadAccessor = accessor;
state.trackOperation();
return accessor.read(evalContext, targetObject, name);
}
}
}
catch (Exception ex) {
if (ex instanceof SpelEvaluationException see) {
throw see;
}
throw new SpelEvaluationException(ex, SpelMessage.EXCEPTION_DURING_PROPERTY_READ, name, ex.getMessage());
}
@@ -230,9 +238,8 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
}
}
private void writeProperty(
TypedValue contextObject, EvaluationContext evalContext, String name, @Nullable Object newValue)
throws EvaluationException {
private void writeProperty(ExpressionState state, TypedValue contextObject, EvaluationContext evalContext,
String name, @Nullable Object newValue) throws EvaluationException {
Object targetObject = contextObject.getValue();
if (targetObject == null) {
@@ -247,6 +254,7 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
if (accessorToUse != null) {
if (evalContext.getPropertyAccessors().contains(accessorToUse)) {
try {
state.trackOperation();
accessorToUse.write(evalContext, targetObject, name, newValue);
return;
}
@@ -264,6 +272,7 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
for (PropertyAccessor accessor : accessorsToTry) {
if (accessor.canWrite(evalContext, targetObject, name)) {
this.cachedWriteAccessor = accessor;
state.trackOperation();
accessor.write(evalContext, targetObject, name, newValue);
return;
}
@@ -357,15 +366,18 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
private final TypedValue contextObject;
private final ExpressionState expressionState;
private final EvaluationContext evalContext;
private final boolean autoGrowNullReferences;
public AccessorValueRef(PropertyOrFieldReference propertyOrFieldReference, TypedValue activeContextObject,
EvaluationContext evalContext, boolean autoGrowNullReferences) {
ExpressionState expressionState, EvaluationContext evalContext, boolean autoGrowNullReferences) {
this.ref = propertyOrFieldReference;
this.contextObject = activeContextObject;
this.expressionState = expressionState;
this.evalContext = evalContext;
this.autoGrowNullReferences = autoGrowNullReferences;
}
@@ -373,7 +385,7 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
@Override
public TypedValue getValue() {
TypedValue value =
this.ref.getValueInternal(this.contextObject, this.evalContext, this.autoGrowNullReferences);
this.ref.getValueInternal(this.expressionState, this.contextObject, this.evalContext, this.autoGrowNullReferences);
if (this.ref.cachedReadAccessor instanceof CompilablePropertyAccessor compilablePropertyAccessor) {
this.ref.setExitTypeDescriptor(CodeFlow.toDescriptor(compilablePropertyAccessor.getPropertyType()));
}
@@ -382,7 +394,7 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
@Override
public void setValue(@Nullable Object newValue) {
this.ref.writeProperty(this.contextObject, this.evalContext, this.ref.name, newValue);
this.ref.writeProperty(this.expressionState, this.contextObject, this.evalContext, this.ref.name, newValue);
}
@Override
@@ -99,6 +99,7 @@ public class Selection extends SpelNodeImpl {
SpelNodeImpl selectionCriteria = this.children[0];
if (operand instanceof Map<?, ?> mapdata) {
state.trackOperation();
Map<Object, Object> result = new HashMap<>();
Object lastKey = null;
@@ -110,6 +111,7 @@ public class Selection extends SpelNodeImpl {
Object val = selectionCriteria.getValueInternal(state).getValue();
if (val instanceof Boolean b) {
if (b) {
state.trackOperation();
result.put(entry.getKey(), entry.getValue());
if (this.variant == FIRST) {
return new ValueRef.TypedValueHolderValueRef(new TypedValue(result), this);
@@ -146,6 +148,7 @@ public class Selection extends SpelNodeImpl {
Iterable<?> data = (operand instanceof Iterable<?> iterable ? iterable :
Arrays.asList(ObjectUtils.toObjectArray(operand)));
state.trackOperation();
List<Object> result = new ArrayList<>();
for (Object element : data) {
try {
@@ -157,6 +160,7 @@ public class Selection extends SpelNodeImpl {
if (this.variant == FIRST) {
return new ValueRef.TypedValueHolderValueRef(new TypedValue(element), this);
}
state.trackOperation();
result.add(element);
}
}

Some files were not shown because too many files have changed in this diff Show More