mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| daf9551578 | |||
| 76a36dfd70 | |||
| 3d47da9746 | |||
| 519d7332eb | |||
| ec89834fa8 | |||
| b294371a46 | |||
| 1829b42b9c | |||
| 86d99790db | |||
| 3aaec98765 | |||
| ee4e79002b | |||
| ce90e4b11f | |||
| 224e647e1b | |||
| c0d399ab88 | |||
| 5c4497a96c | |||
| 0fadafa9bc | |||
| f161a419bd | |||
| 244f6ec390 | |||
| cca70d4ad2 | |||
| 96b44949a8 | |||
| 2555655ccf | |||
| ecfa498ea9 | |||
| e2646fe324 | |||
| a22818564b | |||
| c37ba027e7 | |||
| 2dd550d80f | |||
| 9ca396e424 | |||
| 2eb0ba3843 | |||
| cc48200b65 | |||
| 44150f2523 | |||
| a0a3fe41f6 | |||
| de18f74c53 | |||
| 89ecaea937 | |||
| 444877a475 | |||
| 57b62dd73a | |||
| 5f661f1b64 | |||
| b0137977d8 | |||
| 425ce8625a | |||
| 85c578b929 | |||
| cf727bd7c8 | |||
| e8f08b4158 | |||
| c6b485c31d | |||
| b8ddd2c690 | |||
| a42a6e0c6a | |||
| 8a93a88962 | |||
| fdcf4d50f0 | |||
| 3c023114fb | |||
| 8b5cbb7496 | |||
| a58fdeaf3f | |||
| e4bfdfa229 | |||
| 46867fad81 | |||
| 371dcc5c13 | |||
| dc032c1e33 | |||
| 626907bbfd | |||
| 8d51f47357 | |||
| fb0c78ae27 | |||
| 9f431e2eac | |||
| cba8f225d9 | |||
| 515152b645 |
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -1,4 +1,4 @@
|
||||
version=6.2.18
|
||||
version=6.2.20-SNAPSHOT
|
||||
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
|
||||
+1
-1
@@ -171,7 +171,7 @@ public class MethodInvocationProceedingJoinPoint implements ProceedingJoinPoint,
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "execution(" + getSignature().toString() + ")";
|
||||
return "execution(" + getSignature() + ")";
|
||||
}
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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.
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
+3
-1
@@ -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;
|
||||
|
||||
+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.
|
||||
*/
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+3
-5
@@ -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);
|
||||
}
|
||||
|
||||
+7
@@ -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, () -> {
|
||||
|
||||
+2
@@ -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
|
||||
|
||||
+3
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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());
|
||||
|
||||
Vendored
+2
-2
@@ -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.");
|
||||
|
||||
+1
-1
@@ -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.");
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -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;
|
||||
|
||||
+2
-5
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
+5
-3
@@ -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;
|
||||
|
||||
+1
-1
@@ -195,7 +195,7 @@ public class InstrumentationLoadTimeWeaver implements LoadTimeWeaver {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "FilteringClassFileTransformer for: " + this.targetTransformer.toString();
|
||||
return "FilteringClassFileTransformer for: " + this.targetTransformer;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-2
@@ -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
|
||||
*/
|
||||
|
||||
+42
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+13
@@ -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);
|
||||
|
||||
+2
-2
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+20
@@ -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))) {
|
||||
|
||||
+8
@@ -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
|
||||
|
||||
+8
@@ -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
|
||||
|
||||
+3
-4
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
|
||||
+12
@@ -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++;
|
||||
|
||||
+1
-1
@@ -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");
|
||||
}
|
||||
|
||||
+42
@@ -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") }
|
||||
|
||||
+17
@@ -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
|
||||
|
||||
+4
-1
@@ -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;
|
||||
|
||||
+144
-14
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -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));
|
||||
}
|
||||
|
||||
+1
@@ -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())) {
|
||||
|
||||
+2
@@ -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) {
|
||||
|
||||
+57
-18
@@ -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;
|
||||
|
||||
+60
-49
@@ -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
|
||||
|
||||
+70
-79
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+8
-3
@@ -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);
|
||||
|
||||
+4
-2
@@ -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;
|
||||
|
||||
+14
-8
@@ -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
|
||||
|
||||
+1
@@ -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) {
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
+37
-20
@@ -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 {
|
||||
|
||||
|
||||
+1
@@ -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) {
|
||||
|
||||
+1
@@ -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()));
|
||||
|
||||
+4
@@ -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 {
|
||||
|
||||
+33
-21
@@ -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
Reference in New Issue
Block a user