mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
112 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d3343db2ff | |||
| 9d6d67188a | |||
| 24fd0940bd | |||
| 5b928f47a8 | |||
| 5e52baee86 | |||
| c10596a1fd | |||
| a68b7289f0 | |||
| a5b9e667e3 | |||
| 78fe55f4d1 | |||
| 65913c3655 | |||
| b7a996a64b | |||
| 5b1a7c7f21 | |||
| ebd80bdd6c | |||
| a4fc68b8e8 | |||
| c333946b0b | |||
| b0a8a3ec5f | |||
| bb5be2119c | |||
| cf662368a5 | |||
| e1b06ccfaa | |||
| 9eefdb8041 | |||
| c50cb10964 | |||
| 245341231f | |||
| 875cc828cf | |||
| 898d3ec86a | |||
| 8ccaabe778 | |||
| 9c934b5019 | |||
| 4b45338ae6 | |||
| 95003e3512 | |||
| 68368621f3 | |||
| 890a8f4311 | |||
| 0976d1a252 | |||
| 2c0ce790d8 | |||
| d512aafc36 | |||
| faa000f330 | |||
| daf1b3d7a7 | |||
| a48897a241 | |||
| 7ffa12f90f | |||
| 0e74ffa144 | |||
| 357195289d | |||
| 4fbca99f20 | |||
| c48ca5151f | |||
| aaf2e8fbe6 | |||
| 3635ff0c17 | |||
| f20e76e227 | |||
| dd32f95192 | |||
| 75a920bc9f | |||
| 92874adae9 | |||
| 55167b7f50 | |||
| 3129afba19 | |||
| 1366d2926b | |||
| 5145cf476f | |||
| 6cb41dc5e3 | |||
| 1e95332f62 | |||
| fec6ba4dfe | |||
| bfde33a514 | |||
| 14b9865de7 | |||
| 62eb21f938 | |||
| 01c85b1afb | |||
| df376d9343 | |||
| 4d792d0e45 | |||
| 0a5bd89129 | |||
| 53b9a2cb78 | |||
| e78179b96e | |||
| 49a63e2c37 | |||
| 5666e363d1 | |||
| f06853a339 | |||
| 8afd01ba2c | |||
| 9724f9b9c8 | |||
| 0d9033592b | |||
| fc7b8ae966 | |||
| c457131f1c | |||
| 39cfe136da | |||
| 05a880e3b5 | |||
| c93af1f76d | |||
| 541866fd70 | |||
| 0219ee656f | |||
| f6e1a5de09 | |||
| 9b3cb15389 | |||
| 4ef2b429e0 | |||
| 0beb56a58c | |||
| 6bd4687706 | |||
| 07b12666b4 | |||
| 438d6de3c1 | |||
| d43126705f | |||
| 57bbf0ca0f | |||
| 27912b1ed1 | |||
| 8ffbafd384 | |||
| 022fdcd67e | |||
| 11ebceee38 | |||
| fa21dffcf8 | |||
| 323de1208a | |||
| 94d46eba3c | |||
| e340e45f5a | |||
| a06bbccf9e | |||
| bbe362c0e6 | |||
| 4a81f2c904 | |||
| fb0a108254 | |||
| c98f314665 | |||
| d22924c728 | |||
| e235e661d8 | |||
| 09fe0adb40 | |||
| 84e762b470 | |||
| e90a2da05d | |||
| d48f388c6a | |||
| bbbb7c396e | |||
| d72c8b32b7 | |||
| 67d78eb61c | |||
| 73fd9133e9 | |||
| 5e28a25a30 | |||
| 4c44b91cf9 | |||
| 912c067e23 | |||
| a55701588e |
@@ -15,7 +15,7 @@ permissions:
|
||||
jobs:
|
||||
build:
|
||||
name: Dispatch docs deployment
|
||||
if: github.repository_owner == 'spring-projects'
|
||||
if: ${{ github.repository == 'spring-projects/spring-framework' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Enable auto-env through the sdkman_auto_env config
|
||||
# Add key=value pairs of SDKs to use below
|
||||
java=17.0.12-librca
|
||||
java=17.0.13-librca
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -50,7 +50,7 @@ public class CheckstyleConventions {
|
||||
project.getPlugins().apply(CheckstylePlugin.class);
|
||||
project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g"));
|
||||
CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class);
|
||||
checkstyle.setToolVersion("10.18.1");
|
||||
checkstyle.setToolVersion("10.21.2");
|
||||
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
|
||||
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
|
||||
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
|
||||
|
||||
@@ -36,4 +36,4 @@ runtime:
|
||||
failure_level: warn
|
||||
ui:
|
||||
bundle:
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.17/ui-bundle.zip
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.18/ui-bundle.zip
|
||||
|
||||
@@ -23,6 +23,12 @@ The following table lists all currently supported Spring properties.
|
||||
|===
|
||||
| Name | Description
|
||||
|
||||
| `spring.aop.ajc.ignore`
|
||||
| Instructs Spring to ignore ajc-compiled aspects for Spring AOP proxying, restoring traditional
|
||||
Spring behavior for scenarios where both weaving and AspectJ auto-proxying are enabled. See
|
||||
{spring-framework-api}++/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.html#IGNORE_AJC_PROPERTY_NAME++[`AbstractAspectJAdvisorFactory`]
|
||||
for details.
|
||||
|
||||
| `spring.aot.enabled`
|
||||
| Indicates the application should run with AOT generated artifacts. See
|
||||
xref:core/aot.adoc[Ahead of Time Optimizations] and
|
||||
@@ -32,7 +38,7 @@ for details.
|
||||
| `spring.beaninfo.ignore`
|
||||
| Instructs Spring to use the `Introspector.IGNORE_ALL_BEANINFO` mode when calling the
|
||||
JavaBeans `Introspector`. See
|
||||
{spring-framework-api}++/beans/StandardBeanInfoFactory.html#IGNORE_BEANINFO_PROPERTY_NAME++[`CachedIntrospectionResults`]
|
||||
{spring-framework-api}++/beans/StandardBeanInfoFactory.html#IGNORE_BEANINFO_PROPERTY_NAME++[`StandardBeanInfoFactory`]
|
||||
for details.
|
||||
|
||||
| `spring.cache.reactivestreams.ignore`
|
||||
@@ -49,15 +55,13 @@ for details.
|
||||
|
||||
| `spring.context.checkpoint`
|
||||
| Property that specifies a common context checkpoint. See
|
||||
xref:integration/checkpoint-restore.adoc#_automatic_checkpointrestore_at_startup[Automatic
|
||||
checkpoint/restore at startup] and
|
||||
xref:integration/checkpoint-restore.adoc#_automatic_checkpointrestore_at_startup[Automatic checkpoint/restore at startup] and
|
||||
{spring-framework-api}++/context/support/DefaultLifecycleProcessor.html#CHECKPOINT_PROPERTY_NAME++[`DefaultLifecycleProcessor`]
|
||||
for details.
|
||||
|
||||
| `spring.context.exit`
|
||||
| Property for terminating the JVM when the context reaches a specific phase. See
|
||||
xref:integration/checkpoint-restore.adoc#_automatic_checkpointrestore_at_startup[Automatic
|
||||
checkpoint/restore at startup] and
|
||||
xref:integration/checkpoint-restore.adoc#_automatic_checkpointrestore_at_startup[Automatic checkpoint/restore at startup] and
|
||||
{spring-framework-api}++/context/support/DefaultLifecycleProcessor.html#EXIT_PROPERTY_NAME++[`DefaultLifecycleProcessor`]
|
||||
for details.
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ NOTE: At the moment, AOT is focused on allowing Spring applications to be deploy
|
||||
We intend to support more JVM-based use cases in future generations.
|
||||
|
||||
[[aot.basics]]
|
||||
== AOT engine overview
|
||||
== AOT Engine Overview
|
||||
|
||||
The entry point of the AOT engine for processing an `ApplicationContext` is `ApplicationContextAotGenerator`. It takes care of the following steps, based on a `GenericApplicationContext` that represents the application to optimize and a {spring-framework-api}/aot/generate/GenerationContext.html[`GenerationContext`]:
|
||||
|
||||
@@ -225,7 +225,7 @@ When a `datasource` instance is required, a `BeanInstanceSupplier` is called.
|
||||
This supplier invokes the `dataSource()` method on the `dataSourceConfiguration` bean.
|
||||
|
||||
[[aot.running]]
|
||||
== Running with AOT optimizations
|
||||
== Running with AOT Optimizations
|
||||
|
||||
AOT is a mandatory step to transform a Spring application to a native executable, so it
|
||||
is automatically enabled when running in this mode. It is possible to use those optimizations
|
||||
@@ -244,7 +244,7 @@ However, keep in mind that some optimizations are made at build time based on a
|
||||
This section lists the best practices that make sure your application is ready for AOT.
|
||||
|
||||
[[aot.bestpractices.bean-registration]]
|
||||
== Programmatic bean registration
|
||||
=== Programmatic Bean Registration
|
||||
|
||||
The AOT engine takes care of the `@Configuration` model and any callback that might be
|
||||
invoked as part of processing your configuration. If you need to register additional
|
||||
@@ -266,7 +266,7 @@ notion of a classpath. For cases like this, it is crucial that the scanning happ
|
||||
build time.
|
||||
|
||||
[[aot.bestpractices.bean-type]]
|
||||
=== Expose The Most Precise Bean Type
|
||||
=== Expose the Most Precise Bean Type
|
||||
|
||||
While your application may interact with an interface that a bean implements, it is still very important to declare the most precise type.
|
||||
The AOT engine performs additional checks on the bean type, such as detecting the presence of `@Autowired` members or lifecycle callback methods.
|
||||
@@ -326,21 +326,21 @@ However, this is not a best practice and flagging the preferred constructor with
|
||||
|
||||
In case you are working on a code base that you cannot modify, you can set the {spring-framework-api}/beans/factory/support/AbstractBeanDefinition.html#PREFERRED_CONSTRUCTORS_ATTRIBUTE[`preferredConstructors` attribute] on the related bean definition to indicate which constructor should be used.
|
||||
|
||||
[[aot.bestpractices.comlext-data-structure]]
|
||||
=== Avoid Complex Data Structure for Constructor Parameters and Properties
|
||||
[[aot.bestpractices.complex-data-structures]]
|
||||
=== Avoid Complex Data Structures for Constructor Parameters and Properties
|
||||
|
||||
When crafting a `RootBeanDefinition` programmatically, you are not constrained in terms of types that you can use.
|
||||
For instance, you may have a custom `record` with several properties that your bean takes as a constructor argument.
|
||||
|
||||
While this works fine with the regular runtime, AOT does not know how to generate the code of your custom data structure.
|
||||
A good rule of thumb is to keep in mind that bean definitions are an abstraction on top of several models.
|
||||
Rather than using such structure, decomposing to simple types or referring to a bean that is built as such is recommended.
|
||||
Rather than using such structures, decomposing to simple types or referring to a bean that is built as such is recommended.
|
||||
|
||||
As a last resort, you can implement your own `org.springframework.aot.generate.ValueCodeGenerator$Delegate`.
|
||||
To use it, register its fully qualified name in `META-INF/spring/aot.factories` using the `Delegate` as the key.
|
||||
|
||||
[[aot.bestpractices.custom-arguments]]
|
||||
=== Avoid Creating Bean with Custom Arguments
|
||||
=== Avoid Creating Beans with Custom Arguments
|
||||
|
||||
Spring AOT detects what needs to be done to create a bean and translates that in generated code using an instance supplier.
|
||||
The container also supports creating a bean with {spring-framework-api}++/beans/factory/BeanFactory.html#getBean(java.lang.String,java.lang.Object...)++[custom arguments] that leads to several issues with AOT:
|
||||
@@ -352,6 +352,20 @@ For instance, autowiring on fields and methods will be skipped as they are handl
|
||||
|
||||
Rather than having prototype-scoped beans created with custom arguments, we recommend a manual factory pattern where a bean is responsible for the creation of the instance.
|
||||
|
||||
[[aot.bestpractices.circular-dependencies]]
|
||||
=== Avoid Circular Dependencies
|
||||
|
||||
Certain use cases can result in circular dependencies between one or more beans. With the
|
||||
regular runtime, it may be possible to wire those circular dependencies via `@Autowired`
|
||||
on setter methods or fields. However, an AOT-optimized context will fail to start with
|
||||
explicit circular dependencies.
|
||||
|
||||
In an AOT-optimized application, you should therefore strive to avoid circular
|
||||
dependencies. If that is not possible, you can use `@Lazy` injection points or
|
||||
`ObjectProvider` to lazily access or retrieve the necessary collaborating beans. See
|
||||
xref:core/beans/classpath-scanning.adoc#beans-factorybeans-annotations-lazy-injection-points[this tip]
|
||||
for further information.
|
||||
|
||||
[[aot.bestpractices.factory-bean]]
|
||||
=== FactoryBean
|
||||
|
||||
|
||||
@@ -480,11 +480,15 @@ factory method and other bean definition properties, such as a qualifier value t
|
||||
the `@Qualifier` annotation. Other method-level annotations that can be specified are
|
||||
`@Scope`, `@Lazy`, and custom qualifier annotations.
|
||||
|
||||
TIP: In addition to its role for component initialization, you can also place the `@Lazy`
|
||||
[[beans-factorybeans-annotations-lazy-injection-points]]
|
||||
[TIP]
|
||||
====
|
||||
In addition to its role for component initialization, you can also place the `@Lazy`
|
||||
annotation on injection points marked with `@Autowired` or `@Inject`. In this context,
|
||||
it leads to the injection of a lazy-resolution proxy. However, such a proxy approach
|
||||
is rather limited. For sophisticated lazy interactions, in particular in combination
|
||||
with optional dependencies, we recommend `ObjectProvider<MyTargetBean>` instead.
|
||||
====
|
||||
|
||||
Autowired fields and methods are supported, as previously discussed, with additional
|
||||
support for autowiring of `@Bean` methods. The following example shows how to do so:
|
||||
|
||||
@@ -883,11 +883,12 @@ e.g. for processing all events asynchronously and/or for handling listener excep
|
||||
== Convenient Access to Low-level Resources
|
||||
|
||||
For optimal usage and understanding of application contexts, you should familiarize
|
||||
yourself with Spring's `Resource` abstraction, as described in xref:web/webflux-webclient/client-builder.adoc#webflux-client-builder-reactor-resources[Resources].
|
||||
yourself with Spring's `Resource` abstraction, as described in
|
||||
xref:core/resources.adoc[Resources].
|
||||
|
||||
An application context is a `ResourceLoader`, which can be used to load `Resource` objects.
|
||||
A `Resource` is essentially a more feature rich version of the JDK `java.net.URL` class.
|
||||
In fact, the implementations of the `Resource` wrap an instance of `java.net.URL`, where
|
||||
In fact, implementations of `Resource` wrap an instance of `java.net.URL`, where
|
||||
appropriate. A `Resource` can obtain low-level resources from almost any location in a
|
||||
transparent fashion, including from the classpath, a filesystem location, anywhere
|
||||
describable with a standard URL, and some other variations. If the resource location
|
||||
|
||||
-7
@@ -120,8 +120,6 @@ dynamically generate a subclass that overrides the method.
|
||||
subclasses cannot be `final`, and the method to be overridden cannot be `final`, either.
|
||||
* Unit-testing a class that has an `abstract` method requires you to subclass the class
|
||||
yourself and to supply a stub implementation of the `abstract` method.
|
||||
* Concrete methods are also necessary for component scanning, which requires concrete
|
||||
classes to pick up.
|
||||
* A further key limitation is that lookup methods do not work with factory methods and
|
||||
in particular not with `@Bean` methods in configuration classes, since, in that case,
|
||||
the container is not in charge of creating the instance and therefore cannot create
|
||||
@@ -293,11 +291,6 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
Note that you should typically declare such annotated lookup methods with a concrete
|
||||
stub implementation, in order for them to be compatible with Spring's component
|
||||
scanning rules where abstract classes get ignored by default. This limitation does not
|
||||
apply to explicitly registered or explicitly imported bean classes.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
Another way of accessing differently scoped target beans is an `ObjectFactory`/
|
||||
|
||||
+2
-2
@@ -102,8 +102,8 @@ following snippet:
|
||||
----
|
||||
<bean id="theTargetBean" class="..." />
|
||||
|
||||
<bean id="client" class="...">
|
||||
<property name="targetName" value="theTargetBean"/>
|
||||
<bean id="theClientBean" class="...">
|
||||
<property name="targetName" ref="theTargetBean"/>
|
||||
</bean>
|
||||
----
|
||||
|
||||
|
||||
@@ -12,27 +12,27 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
Inventor einstein = p.parseExpression(
|
||||
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
|
||||
Inventor einstein = parser.parseExpression(
|
||||
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
|
||||
.getValue(Inventor.class);
|
||||
|
||||
// create new Inventor instance within the add() method of List
|
||||
p.parseExpression(
|
||||
"Members.add(new org.spring.samples.spel.inventor.Inventor(
|
||||
'Albert Einstein', 'German'))").getValue(societyContext);
|
||||
parser.parseExpression(
|
||||
"Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))")
|
||||
.getValue(societyContext);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
val einstein = p.parseExpression(
|
||||
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
|
||||
val einstein = parser.parseExpression(
|
||||
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
|
||||
.getValue(Inventor::class.java)
|
||||
|
||||
// create new Inventor instance within the add() method of List
|
||||
p.parseExpression(
|
||||
"Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))")
|
||||
parser.parseExpression(
|
||||
"Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))")
|
||||
.getValue(societyContext)
|
||||
----
|
||||
======
|
||||
|
||||
@@ -110,8 +110,8 @@ potentially more efficient use cases if the `MethodHandle` target and parameters
|
||||
been fully bound prior to registration; however, partially bound handles are also
|
||||
supported.
|
||||
|
||||
Consider the `String#formatted(String, Object...)` instance method, which produces a
|
||||
message according to a template and a variable number of arguments.
|
||||
Consider the `String#formatted(Object...)` instance method, which produces a message
|
||||
according to a template and a variable number of arguments.
|
||||
|
||||
You can register and use the `formatted` method as a `MethodHandle`, as the following
|
||||
example shows:
|
||||
@@ -151,10 +151,10 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
As hinted above, binding a `MethodHandle` and registering the bound `MethodHandle` is also
|
||||
supported. This is likely to be more performant if both the target and all the arguments
|
||||
are bound. In that case no arguments are necessary in the SpEL expression, as the
|
||||
following example shows:
|
||||
As mentioned above, binding a `MethodHandle` and registering the bound `MethodHandle` is
|
||||
also supported. This is likely to be more performant if both the target and all the
|
||||
arguments are bound. In that case no arguments are necessary in the SpEL expression, as
|
||||
the following example shows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -168,9 +168,10 @@ Java::
|
||||
String template = "This is a %s message with %s words: <%s>";
|
||||
Object varargs = new Object[] { "prerecorded", 3, "Oh Hello World!", "ignored" };
|
||||
MethodHandle mh = MethodHandles.lookup().findVirtual(String.class, "formatted",
|
||||
MethodType.methodType(String.class, Object[].class))
|
||||
MethodType.methodType(String.class, Object[].class))
|
||||
.bindTo(template)
|
||||
.bindTo(varargs); //here we have to provide arguments in a single array binding
|
||||
// Here we have to provide the arguments in a single array binding:
|
||||
.bindTo(varargs);
|
||||
context.setVariable("message", mh);
|
||||
|
||||
// evaluates to "This is a prerecorded message with 3 words: <Oh Hello World!>"
|
||||
@@ -189,9 +190,10 @@ Kotlin::
|
||||
val varargs = arrayOf("prerecorded", 3, "Oh Hello World!", "ignored")
|
||||
|
||||
val mh = MethodHandles.lookup().findVirtual(String::class.java, "formatted",
|
||||
MethodType.methodType(String::class.java, Array<Any>::class.java))
|
||||
MethodType.methodType(String::class.java, Array<Any>::class.java))
|
||||
.bindTo(template)
|
||||
.bindTo(varargs) //here we have to provide arguments in a single array binding
|
||||
// Here we have to provide the arguments in a single array binding:
|
||||
.bindTo(varargs)
|
||||
context.setVariable("message", mh)
|
||||
|
||||
// evaluates to "This is a prerecorded message with 3 words: <Oh Hello World!>"
|
||||
|
||||
@@ -196,7 +196,7 @@ Kotlin::
|
||||
|
||||
When used as `org.springframework.validation.Validator`, `LocalValidatorFactoryBean`
|
||||
invokes the underlying `jakarta.validation.Validator`, and then adapts
|
||||
``ContraintViolation``s to ``FieldError``s, and registers them with the `Errors` object
|
||||
``ConstraintViolation``s to ``FieldError``s, and registers them with the `Errors` object
|
||||
passed into the `validate` method.
|
||||
|
||||
|
||||
|
||||
@@ -280,10 +280,11 @@ Kotlin::
|
||||
|
||||
A portable format annotation API exists in the `org.springframework.format.annotation`
|
||||
package. You can use `@NumberFormat` to format `Number` fields such as `Double` and
|
||||
`Long`, and `@DateTimeFormat` to format `java.util.Date`, `java.util.Calendar`, `Long`
|
||||
(for millisecond timestamps) as well as JSR-310 `java.time`.
|
||||
`Long`, and `@DateTimeFormat` to format fields such as `java.util.Date`,
|
||||
`java.util.Calendar`, and `Long` (for millisecond timestamps) as well as JSR-310
|
||||
`java.time` types.
|
||||
|
||||
The following example uses `@DateTimeFormat` to format a `java.util.Date` as an ISO Date
|
||||
The following example uses `@DateTimeFormat` to format a `java.util.Date` as an ISO date
|
||||
(yyyy-MM-dd):
|
||||
|
||||
[tabs]
|
||||
@@ -309,6 +310,28 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
For further details, see the javadoc for
|
||||
{spring-framework-api}/format/annotation/DateTimeFormat.html[`@DateTimeFormat`] and
|
||||
{spring-framework-api}/format/annotation/NumberFormat.html[`@NumberFormat`].
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
Style-based formatting and parsing rely on locale-sensitive patterns which may change
|
||||
depending on the Java runtime. Specifically, applications that rely on date, time, or
|
||||
number parsing and formatting may encounter incompatible changes in behavior when running
|
||||
on JDK 20 or higher.
|
||||
|
||||
Using an ISO standardized format or a concrete pattern that you control allows for
|
||||
reliable system-independent and locale-independent parsing and formatting of date, time,
|
||||
and number values.
|
||||
|
||||
For `@DateTimeFormat`, the use of fallback patterns can also help to address
|
||||
compatibility issues.
|
||||
|
||||
For further details, see the
|
||||
https://github.com/spring-projects/spring-framework/wiki/Date-and-Time-Formatting-with-JDK-20-and-higher[Date and Time Formatting with JDK 20 and higher]
|
||||
page in the Spring Framework wiki.
|
||||
====
|
||||
|
||||
[[format-FormatterRegistry-SPI]]
|
||||
== The `FormatterRegistry` SPI
|
||||
|
||||
@@ -198,7 +198,7 @@ methods it offers can be found in the {spring-framework-api}/validation/Errors.h
|
||||
Validators may also get locally invoked for the immediate validation of a given object,
|
||||
not involving a binding process. As of 6.1, this has been simplified through a new
|
||||
`Validator.validateObject(Object)` method which is available by default now, returning
|
||||
a simple ´Errors` representation which can be inspected: typically calling `hasErrors()`
|
||||
a simple `Errors` representation which can be inspected: typically calling `hasErrors()`
|
||||
or the new `failOnError` method for turning the error summary message into an exception
|
||||
(e.g. `validator.validateObject(myObject).failOnError(IllegalArgumentException::new)`).
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ Kotlin::
|
||||
[[webtestclient-fn-config]]
|
||||
=== Bind to Router Function
|
||||
|
||||
This setup allows you to test <<web-reactive.adoc#webflux-fn, functional endpoints>> via
|
||||
This setup allows you to test xref:web/webflux-functional.adoc[functional endpoints] via
|
||||
mock request and response objects, without a running server.
|
||||
|
||||
For WebFlux, use the following which delegates to `RouterFunctions.toWebHandler` to
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[[webflux-cors]]
|
||||
= CORS
|
||||
|
||||
[.small]#xref:web/webmvc-cors.adoc[See equivalent in the Servlet stack]#
|
||||
|
||||
Spring WebFlux lets you handle CORS (Cross-Origin Resource Sharing). This section
|
||||
@@ -364,7 +365,7 @@ Kotlin::
|
||||
|
||||
You can apply CORS support through the built-in
|
||||
{spring-framework-api}/web/cors/reactive/CorsWebFilter.html[`CorsWebFilter`], which is a
|
||||
good fit with <<webflux-fn, functional endpoints>>.
|
||||
good fit with xref:web/webflux-functional.adoc[functional endpoints].
|
||||
|
||||
NOTE: If you try to use the `CorsFilter` with Spring Security, keep in mind that Spring
|
||||
Security has {docs-spring-security}/servlet/integrations/cors.html[built-in support] for
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[[webflux-fn]]
|
||||
= Functional Endpoints
|
||||
|
||||
[.small]#xref:web/webmvc-functional.adoc[See equivalent in the Servlet stack]#
|
||||
|
||||
Spring WebFlux includes WebFlux.fn, a lightweight functional programming model in which functions
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[[webflux-websocket]]
|
||||
= WebSockets
|
||||
|
||||
[.small]#xref:web/websocket.adoc[See equivalent in the Servlet stack]#
|
||||
|
||||
This part of the reference documentation covers support for reactive-stack WebSocket
|
||||
|
||||
@@ -91,7 +91,7 @@ class WebConfig : WebFluxConfigurer {
|
||||
[.small]#xref:web/webmvc/mvc-config/conversion.adoc[See equivalent in the Servlet stack]#
|
||||
|
||||
By default, formatters for various number and date types are installed, along with support
|
||||
for customization via `@NumberFormat` and `@DateTimeFormat` on fields.
|
||||
for customization via `@NumberFormat` and `@DateTimeFormat` on fields and parameters.
|
||||
|
||||
To register custom formatters and converters in Java config, use the following:
|
||||
|
||||
@@ -356,7 +356,6 @@ which customizes Jackson's default properties with the following ones:
|
||||
|
||||
It also automatically registers the following well-known modules if they are detected on the classpath:
|
||||
|
||||
* {jackson-github-org}/jackson-datatype-joda[`jackson-datatype-joda`]: Support for Joda-Time types.
|
||||
* {jackson-github-org}/jackson-datatype-jsr310[`jackson-datatype-jsr310`]: Support for Java 8 Date and Time API types.
|
||||
* {jackson-github-org}/jackson-datatype-jdk8[`jackson-datatype-jdk8`]: Support for other Java 8 types, such as `Optional`.
|
||||
* {jackson-github-org}/jackson-module-kotlin[`jackson-module-kotlin`]: Support for Kotlin classes and data classes.
|
||||
|
||||
@@ -101,7 +101,7 @@ On that foundation, Spring WebFlux provides a choice of two programming models:
|
||||
from the `spring-web` module. Both Spring MVC and WebFlux controllers support reactive
|
||||
(Reactor and RxJava) return types, and, as a result, it is not easy to tell them apart. One notable
|
||||
difference is that WebFlux also supports reactive `@RequestBody` arguments.
|
||||
* <<webflux-fn>>: Lambda-based, lightweight, and functional programming model. You can think of
|
||||
* xref:web/webflux-functional.adoc[Functional Endpoints]: Lambda-based, lightweight, and functional programming model. You can think of
|
||||
this as a small library or a set of utilities that an application can use to route and
|
||||
handle requests. The big difference with annotated controllers is that the application
|
||||
is in charge of request handling from start to finish versus declaring intent through
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[[mvc-cors]]
|
||||
= CORS
|
||||
|
||||
[.small]#xref:web/webflux-cors.adoc[See equivalent in the Reactive stack]#
|
||||
|
||||
Spring MVC lets you handle CORS (Cross-Origin Resource Sharing). This section
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[[webmvc-fn]]
|
||||
= Functional Endpoints
|
||||
[.small]#<<web-reactive.adoc#webflux-fn, See equivalent in the Reactive stack>>#
|
||||
|
||||
[.small]#xref:web/webflux-functional.adoc[See equivalent in the Reactive stack]#
|
||||
|
||||
Spring Web MVC includes WebMvc.fn, a lightweight functional programming model in which functions
|
||||
are used to route and handle requests and contracts are designed for immutability.
|
||||
|
||||
@@ -26,7 +26,7 @@ available through the `ServletRequest.getParameter{asterisk}()` family of method
|
||||
|
||||
|
||||
|
||||
[[forwarded-headers]]
|
||||
[[filters-forwarded-headers]]
|
||||
== Forwarded Headers
|
||||
[.small]#xref:web/webflux/reactive-spring.adoc#webflux-forwarded-headers[See equivalent in the Reactive stack]#
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
[.small]#xref:web/webflux/config.adoc#webflux-config-conversion[See equivalent in the Reactive stack]#
|
||||
|
||||
By default, formatters for various number and date types are installed, along with support
|
||||
for customization via `@NumberFormat` and `@DateTimeFormat` on fields.
|
||||
for customization via `@NumberFormat` and `@DateTimeFormat` on fields and parameters.
|
||||
|
||||
To register custom formatters and converters in Java config, use the following:
|
||||
|
||||
|
||||
@@ -73,7 +73,6 @@ This builder customizes Jackson's default properties as follows:
|
||||
|
||||
It also automatically registers the following well-known modules if they are detected on the classpath:
|
||||
|
||||
* {jackson-github-org}/jackson-datatype-joda[jackson-datatype-joda]: Support for Joda-Time types.
|
||||
* {jackson-github-org}/jackson-datatype-jsr310[jackson-datatype-jsr310]: Support for Java 8 Date and Time API types.
|
||||
* {jackson-github-org}/jackson-datatype-jdk8[jackson-datatype-jdk8]: Support for other Java 8 types, such as `Optional`.
|
||||
* {jackson-github-org}/jackson-module-kotlin[`jackson-module-kotlin`]: Support for Kotlin classes and data classes.
|
||||
|
||||
@@ -119,7 +119,7 @@ Some example patterns:
|
||||
* `+"/resources/*.png"+` - match zero or more characters in a path segment
|
||||
* `+"/resources/**"+` - match multiple path segments
|
||||
* `+"/projects/{project}/versions"+` - match a path segment and capture it as a variable
|
||||
* `+"/projects/{project:[a-z]+}/versions"+` - match and capture a variable with a regex
|
||||
* `++"/projects/{project:[a-z]+}/versions"++` - match and capture a variable with a regex
|
||||
|
||||
Captured URI variables can be accessed with `@PathVariable`. For example:
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[[websocket]]
|
||||
= WebSockets
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
[.small]#xref:web/webflux-websocket.adoc[See equivalent in the Reactive stack]#
|
||||
|
||||
This part of the reference documentation covers support for Servlet stack, WebSocket
|
||||
|
||||
@@ -45,7 +45,7 @@ input is ignored. This is in contrast to property binding which by default binds
|
||||
request parameter for which there is a matching property.
|
||||
|
||||
If neither a dedicated model object nor constructor binding is sufficient, and you must
|
||||
use property binding, we strongy recommend registering `allowedFields` patterns (case
|
||||
use property binding, we strongly recommend registering `allowedFields` patterns (case
|
||||
sensitive) on `WebDataBinder` in order to prevent unexpected properties from being set.
|
||||
For example:
|
||||
|
||||
|
||||
@@ -8,16 +8,16 @@ javaPlatform {
|
||||
|
||||
dependencies {
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.15.4"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.12.11"))
|
||||
api(platform("io.netty:netty-bom:4.1.113.Final"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.12.12"))
|
||||
api(platform("io.netty:netty-bom:4.1.118.Final"))
|
||||
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
|
||||
api(platform("io.projectreactor:reactor-bom:2023.0.11"))
|
||||
api(platform("io.rsocket:rsocket-bom:1.1.3"))
|
||||
api(platform("org.apache.groovy:groovy-bom:4.0.23"))
|
||||
api(platform("io.projectreactor:reactor-bom:2023.0.15"))
|
||||
api(platform("io.rsocket:rsocket-bom:1.1.5"))
|
||||
api(platform("org.apache.groovy:groovy-bom:4.0.24"))
|
||||
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
|
||||
api(platform("org.assertj:assertj-bom:3.26.3"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.0.13"))
|
||||
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.13"))
|
||||
api(platform("org.assertj:assertj-bom:3.27.3"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.0.16"))
|
||||
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.16"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3"))
|
||||
api(platform("org.junit:junit-bom:5.10.5"))
|
||||
@@ -44,7 +44,7 @@ dependencies {
|
||||
api("com.sun.xml.bind:jaxb-impl:3.0.2")
|
||||
api("com.sun.xml.bind:jaxb-xjc:3.0.2")
|
||||
api("com.thoughtworks.qdox:qdox:2.1.0")
|
||||
api("com.thoughtworks.xstream:xstream:1.4.20")
|
||||
api("com.thoughtworks.xstream:xstream:1.4.21")
|
||||
api("commons-io:commons-io:2.15.0")
|
||||
api("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.2")
|
||||
api("io.micrometer:context-propagation:1.1.1")
|
||||
@@ -54,11 +54,11 @@ dependencies {
|
||||
api("io.r2dbc:r2dbc-h2:1.0.0.RELEASE")
|
||||
api("io.r2dbc:r2dbc-spi-test:1.0.0.RELEASE")
|
||||
api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE")
|
||||
api("io.reactivex.rxjava3:rxjava:3.1.8")
|
||||
api("io.reactivex.rxjava3:rxjava:3.1.10")
|
||||
api("io.smallrye.reactive:mutiny:1.10.0")
|
||||
api("io.undertow:undertow-core:2.3.17.Final")
|
||||
api("io.undertow:undertow-servlet:2.3.17.Final")
|
||||
api("io.undertow:undertow-websockets-jsr:2.3.17.Final")
|
||||
api("io.undertow:undertow-core:2.3.18.Final")
|
||||
api("io.undertow:undertow-servlet:2.3.18.Final")
|
||||
api("io.undertow:undertow-websockets-jsr:2.3.18.Final")
|
||||
api("io.vavr:vavr:0.10.4")
|
||||
api("jakarta.activation:jakarta.activation-api:2.0.1")
|
||||
api("jakarta.annotation:jakarta.annotation-api:2.0.0")
|
||||
@@ -101,8 +101,8 @@ dependencies {
|
||||
api("org.apache.derby:derby:10.16.1.1")
|
||||
api("org.apache.derby:derbyclient:10.16.1.1")
|
||||
api("org.apache.derby:derbytools:10.16.1.1")
|
||||
api("org.apache.httpcomponents.client5:httpclient5:5.4")
|
||||
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3")
|
||||
api("org.apache.httpcomponents.client5:httpclient5:5.4.1")
|
||||
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.1")
|
||||
api("org.apache.poi:poi-ooxml:5.2.5")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-core:10.1.28")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.28")
|
||||
@@ -116,7 +116,7 @@ dependencies {
|
||||
api("org.codehaus.jettison:jettison:1.5.4")
|
||||
api("org.crac:crac:1.4.0")
|
||||
api("org.dom4j:dom4j:2.1.4")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.7")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.8")
|
||||
api("org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.4")
|
||||
api("org.eclipse:yasson:2.0.4")
|
||||
api("org.ehcache:ehcache:3.10.8")
|
||||
@@ -131,7 +131,7 @@ dependencies {
|
||||
api("org.hibernate:hibernate-validator:7.0.5.Final")
|
||||
api("org.hsqldb:hsqldb:2.7.2")
|
||||
api("org.javamoney:moneta:1.4.4")
|
||||
api("org.jruby:jruby:9.4.8.0")
|
||||
api("org.jruby:jruby:9.4.9.0")
|
||||
api("org.junit.support:testng-engine:1.0.5")
|
||||
api("org.mozilla:rhino:1.7.15")
|
||||
api("org.ogce:xpp3:1.1.6")
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
version=6.1.14-SNAPSHOT
|
||||
version=6.1.17
|
||||
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
+59
-59
@@ -56,33 +56,33 @@ import kotlin.annotation.AnnotationTarget.TYPE
|
||||
@SpringJUnitConfig(InterceptorConfig::class)
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
|
||||
class AspectJAutoProxyInterceptorKotlinIntegrationTests(
|
||||
@Autowired val echo: Echo,
|
||||
@Autowired val firstAdvisor: TestPointcutAdvisor,
|
||||
@Autowired val secondAdvisor: TestPointcutAdvisor,
|
||||
@Autowired val countingAspect: CountingAspect,
|
||||
@Autowired val reactiveTransactionManager: ReactiveCallCountingTransactionManager) {
|
||||
@Autowired val echo: Echo,
|
||||
@Autowired val firstAdvisor: TestPointcutAdvisor,
|
||||
@Autowired val secondAdvisor: TestPointcutAdvisor,
|
||||
@Autowired val countingAspect: CountingAspect,
|
||||
@Autowired val reactiveTransactionManager: ReactiveCallCountingTransactionManager) {
|
||||
|
||||
@Test
|
||||
fun `Multiple interceptors with regular function`() {
|
||||
assertThat(firstAdvisor.interceptor.invocations).isEmpty()
|
||||
assertThat(secondAdvisor.interceptor.invocations).isEmpty()
|
||||
val value = "Hello!"
|
||||
assertThat(echo.echo(value)).isEqualTo(value)
|
||||
@Test
|
||||
fun `Multiple interceptors with regular function`() {
|
||||
assertThat(firstAdvisor.interceptor.invocations).isEmpty()
|
||||
assertThat(secondAdvisor.interceptor.invocations).isEmpty()
|
||||
val value = "Hello!"
|
||||
assertThat(echo.echo(value)).isEqualTo(value)
|
||||
assertThat(firstAdvisor.interceptor.invocations).singleElement().matches { String::class.java.isAssignableFrom(it) }
|
||||
assertThat(secondAdvisor.interceptor.invocations).singleElement().matches { String::class.java.isAssignableFrom(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Multiple interceptors with suspending function`() {
|
||||
assertThat(firstAdvisor.interceptor.invocations).isEmpty()
|
||||
assertThat(secondAdvisor.interceptor.invocations).isEmpty()
|
||||
val value = "Hello!"
|
||||
runBlocking {
|
||||
assertThat(echo.suspendingEcho(value)).isEqualTo(value)
|
||||
}
|
||||
@Test
|
||||
fun `Multiple interceptors with suspending function`() {
|
||||
assertThat(firstAdvisor.interceptor.invocations).isEmpty()
|
||||
assertThat(secondAdvisor.interceptor.invocations).isEmpty()
|
||||
val value = "Hello!"
|
||||
runBlocking {
|
||||
assertThat(echo.suspendingEcho(value)).isEqualTo(value)
|
||||
}
|
||||
assertThat(firstAdvisor.interceptor.invocations).singleElement().matches { Mono::class.java.isAssignableFrom(it) }
|
||||
assertThat(secondAdvisor.interceptor.invocations).singleElement().matches { Mono::class.java.isAssignableFrom(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test // gh-33095
|
||||
fun `Aspect and reactive transactional with suspending function`() {
|
||||
@@ -113,17 +113,17 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
|
||||
assertThat(countingAspect.counter).`as`("aspect applied once per key").isEqualTo(2)
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableAspectJAutoProxy
|
||||
@EnableTransactionManagement
|
||||
@Configuration
|
||||
@EnableAspectJAutoProxy
|
||||
@EnableTransactionManagement
|
||||
@EnableCaching
|
||||
open class InterceptorConfig {
|
||||
open class InterceptorConfig {
|
||||
|
||||
@Bean
|
||||
open fun firstAdvisor() = TestPointcutAdvisor().apply { order = 0 }
|
||||
@Bean
|
||||
open fun firstAdvisor() = TestPointcutAdvisor().apply { order = 0 }
|
||||
|
||||
@Bean
|
||||
open fun secondAdvisor() = TestPointcutAdvisor().apply { order = 1 }
|
||||
@Bean
|
||||
open fun secondAdvisor() = TestPointcutAdvisor().apply { order = 1 }
|
||||
|
||||
@Bean
|
||||
open fun countingAspect() = CountingAspect()
|
||||
@@ -138,34 +138,34 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
|
||||
return ConcurrentMapCacheManager()
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun echo(): Echo {
|
||||
return Echo()
|
||||
}
|
||||
}
|
||||
@Bean
|
||||
open fun echo(): Echo {
|
||||
return Echo()
|
||||
}
|
||||
}
|
||||
|
||||
class TestMethodInterceptor: MethodInterceptor {
|
||||
class TestMethodInterceptor: MethodInterceptor {
|
||||
|
||||
var invocations: MutableList<Class<*>> = mutableListOf()
|
||||
var invocations: MutableList<Class<*>> = mutableListOf()
|
||||
|
||||
@Suppress("RedundantNullableReturnType")
|
||||
override fun invoke(invocation: MethodInvocation): Any? {
|
||||
val result = invocation.proceed()
|
||||
invocations.add(result!!.javaClass)
|
||||
return result
|
||||
}
|
||||
@Suppress("RedundantNullableReturnType")
|
||||
override fun invoke(invocation: MethodInvocation): Any? {
|
||||
val result = invocation.proceed()
|
||||
invocations.add(result!!.javaClass)
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class TestPointcutAdvisor : StaticMethodMatcherPointcutAdvisor(TestMethodInterceptor()) {
|
||||
class TestPointcutAdvisor : StaticMethodMatcherPointcutAdvisor(TestMethodInterceptor()) {
|
||||
|
||||
val interceptor: TestMethodInterceptor
|
||||
get() = advice as TestMethodInterceptor
|
||||
val interceptor: TestMethodInterceptor
|
||||
get() = advice as TestMethodInterceptor
|
||||
|
||||
override fun matches(method: Method, targetClass: Class<*>): Boolean {
|
||||
return targetClass == Echo::class.java && method.name.lowercase().endsWith("echo")
|
||||
}
|
||||
}
|
||||
override fun matches(method: Method, targetClass: Class<*>): Boolean {
|
||||
return targetClass == Echo::class.java && method.name.lowercase().endsWith("echo")
|
||||
}
|
||||
}
|
||||
|
||||
@Target(CLASS, FUNCTION, ANNOTATION_CLASS, TYPE)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@@ -185,16 +185,16 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
|
||||
}
|
||||
}
|
||||
|
||||
open class Echo {
|
||||
open class Echo {
|
||||
|
||||
open fun echo(value: String): String {
|
||||
return value
|
||||
}
|
||||
open fun echo(value: String): String {
|
||||
return value
|
||||
}
|
||||
|
||||
open suspend fun suspendingEcho(value: String): String {
|
||||
delay(1)
|
||||
return value
|
||||
}
|
||||
open suspend fun suspendingEcho(value: String): String {
|
||||
delay(1)
|
||||
return value
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Counting
|
||||
@@ -212,6 +212,6 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
|
||||
return "$value ${cacheCounter++}"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+13
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -276,14 +276,18 @@ public abstract class AbstractAspectJAdvice implements Advice, AspectJPrecedence
|
||||
}
|
||||
if (this.aspectJAdviceMethod.getParameterCount() == this.argumentNames.length + 1) {
|
||||
// May need to add implicit join point arg name...
|
||||
Class<?> firstArgType = this.aspectJAdviceMethod.getParameterTypes()[0];
|
||||
if (firstArgType == JoinPoint.class ||
|
||||
firstArgType == ProceedingJoinPoint.class ||
|
||||
firstArgType == JoinPoint.StaticPart.class) {
|
||||
String[] oldNames = this.argumentNames;
|
||||
this.argumentNames = new String[oldNames.length + 1];
|
||||
this.argumentNames[0] = "THIS_JOIN_POINT";
|
||||
System.arraycopy(oldNames, 0, this.argumentNames, 1, oldNames.length);
|
||||
for (int i = 0; i < this.aspectJAdviceMethod.getParameterCount(); i++) {
|
||||
Class<?> argType = this.aspectJAdviceMethod.getParameterTypes()[i];
|
||||
if (argType == JoinPoint.class ||
|
||||
argType == ProceedingJoinPoint.class ||
|
||||
argType == JoinPoint.StaticPart.class) {
|
||||
String[] oldNames = this.argumentNames;
|
||||
this.argumentNames = new String[oldNames.length + 1];
|
||||
System.arraycopy(oldNames, 0, this.argumentNames, 0, i);
|
||||
this.argumentNames[i] = "THIS_JOIN_POINT";
|
||||
System.arraycopy(oldNames, i, this.argumentNames, i + 1, oldNames.length - i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
-1
@@ -18,6 +18,7 @@ package org.springframework.aop.aspectj.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
import java.util.StringTokenizer;
|
||||
@@ -37,6 +38,7 @@ import org.aspectj.lang.reflect.PerClauseKind;
|
||||
|
||||
import org.springframework.aop.framework.AopConfigException;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.core.SpringProperties;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
@@ -58,6 +60,23 @@ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFac
|
||||
private static final Class<?>[] ASPECTJ_ANNOTATION_CLASSES = new Class<?>[] {
|
||||
Pointcut.class, Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class};
|
||||
|
||||
private static final String AJC_MAGIC = "ajc$";
|
||||
|
||||
/**
|
||||
* System property that instructs Spring to ignore ajc-compiled aspects
|
||||
* for Spring AOP proxying, restoring traditional Spring behavior for
|
||||
* scenarios where both weaving and AspectJ auto-proxying are enabled.
|
||||
* <p>The default is "false". Consider switching this to "true" if you
|
||||
* encounter double execution of your aspects in a given build setup.
|
||||
* Note that we recommend restructuring your AspectJ configuration to
|
||||
* avoid such double exposure of an AspectJ aspect to begin with.
|
||||
* @since 6.1.15
|
||||
*/
|
||||
public static final String IGNORE_AJC_PROPERTY_NAME = "spring.aop.ajc.ignore";
|
||||
|
||||
private static final boolean shouldIgnoreAjcCompiledAspects =
|
||||
SpringProperties.getFlag(IGNORE_AJC_PROPERTY_NAME);
|
||||
|
||||
|
||||
/** Logger available to subclasses. */
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
@@ -67,7 +86,8 @@ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFac
|
||||
|
||||
@Override
|
||||
public boolean isAspect(Class<?> clazz) {
|
||||
return (AnnotationUtils.findAnnotation(clazz, Aspect.class) != null);
|
||||
return (AnnotationUtils.findAnnotation(clazz, Aspect.class) != null &&
|
||||
(!shouldIgnoreAjcCompiledAspects || !compiledByAjc(clazz)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -114,6 +134,15 @@ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFac
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean compiledByAjc(Class<?> clazz) {
|
||||
for (Field field : clazz.getDeclaredFields()) {
|
||||
if (field.getName().startsWith(AJC_MAGIC)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Enum for AspectJ annotation types.
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.aop.aspectj;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link AbstractAspectJAdvice}.
|
||||
*
|
||||
* @author Joshua Chen
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class AbstractAspectJAdviceTests {
|
||||
|
||||
@Test
|
||||
void setArgumentNamesFromStringArray_withoutJoinPointParameter() {
|
||||
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithNoJoinPoint");
|
||||
assertThat(advice).satisfies(hasArgumentNames("arg1", "arg2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void setArgumentNamesFromStringArray_withJoinPointAsFirstParameter() {
|
||||
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithJoinPointAsFirstParameter");
|
||||
assertThat(advice).satisfies(hasArgumentNames("THIS_JOIN_POINT", "arg1", "arg2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void setArgumentNamesFromStringArray_withJoinPointAsLastParameter() {
|
||||
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithJoinPointAsLastParameter");
|
||||
assertThat(advice).satisfies(hasArgumentNames("arg1", "arg2", "THIS_JOIN_POINT"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void setArgumentNamesFromStringArray_withJoinPointAsMiddleParameter() {
|
||||
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithJoinPointAsMiddleParameter");
|
||||
assertThat(advice).satisfies(hasArgumentNames("arg1", "THIS_JOIN_POINT", "arg2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void setArgumentNamesFromStringArray_withProceedingJoinPoint() {
|
||||
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithProceedingJoinPoint");
|
||||
assertThat(advice).satisfies(hasArgumentNames("THIS_JOIN_POINT", "arg1", "arg2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void setArgumentNamesFromStringArray_withStaticPart() {
|
||||
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithStaticPart");
|
||||
assertThat(advice).satisfies(hasArgumentNames("THIS_JOIN_POINT", "arg1", "arg2"));
|
||||
}
|
||||
|
||||
private Consumer<AbstractAspectJAdvice> hasArgumentNames(String... argumentNames) {
|
||||
return advice -> assertThat(advice).extracting("argumentNames")
|
||||
.asInstanceOf(InstanceOfAssertFactories.array(String[].class))
|
||||
.containsExactly(argumentNames);
|
||||
}
|
||||
|
||||
private AbstractAspectJAdvice getAspectJAdvice(final String methodName) {
|
||||
AbstractAspectJAdvice advice = new TestAspectJAdvice(getMethod(methodName),
|
||||
mock(AspectJExpressionPointcut.class), mock(AspectInstanceFactory.class));
|
||||
advice.setArgumentNamesFromStringArray("arg1", "arg2");
|
||||
return advice;
|
||||
}
|
||||
|
||||
private Method getMethod(final String methodName) {
|
||||
return Arrays.stream(Sample.class.getDeclaredMethods())
|
||||
.filter(method -> method.getName().equals(methodName)).findFirst()
|
||||
.orElseThrow();
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public static class TestAspectJAdvice extends AbstractAspectJAdvice {
|
||||
|
||||
public TestAspectJAdvice(Method aspectJAdviceMethod, AspectJExpressionPointcut pointcut,
|
||||
AspectInstanceFactory aspectInstanceFactory) {
|
||||
super(aspectJAdviceMethod, pointcut, aspectInstanceFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBeforeAdvice() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAfterAdvice() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
static class Sample {
|
||||
|
||||
void methodWithNoJoinPoint(String arg1, String arg2) {
|
||||
}
|
||||
|
||||
void methodWithJoinPointAsFirstParameter(JoinPoint joinPoint, String arg1, String arg2) {
|
||||
}
|
||||
|
||||
void methodWithJoinPointAsLastParameter(String arg1, String arg2, JoinPoint joinPoint) {
|
||||
}
|
||||
|
||||
void methodWithJoinPointAsMiddleParameter(String arg1, JoinPoint joinPoint, String arg2) {
|
||||
}
|
||||
|
||||
void methodWithProceedingJoinPoint(ProceedingJoinPoint joinPoint, String arg1, String arg2) {
|
||||
}
|
||||
|
||||
void methodWithStaticPart(JoinPoint.StaticPart staticPart, String arg1, String arg2) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,17 +31,17 @@ import kotlin.coroutines.Continuation
|
||||
*/
|
||||
class AopUtilsKotlinTests {
|
||||
|
||||
@Test
|
||||
fun `Invoking suspending function should return Mono`() {
|
||||
val value = "foo"
|
||||
val method = ReflectionUtils.findMethod(WithoutInterface::class.java, "handle",
|
||||
@Test
|
||||
fun `Invoking suspending function should return Mono`() {
|
||||
val value = "foo"
|
||||
val method = ReflectionUtils.findMethod(WithoutInterface::class.java, "handle",
|
||||
String::class. java, Continuation::class.java)!!
|
||||
val continuation = Continuation<Any>(CoroutineName("test")) { }
|
||||
val continuation = Continuation<Any>(CoroutineName("test")) { }
|
||||
val result = AopUtils.invokeJoinpointUsingReflection(WithoutInterface(), method, arrayOf(value, continuation))
|
||||
assertThat(result).isInstanceOfSatisfying(Mono::class.java) {
|
||||
assertThat(it.block()).isEqualTo(value)
|
||||
}
|
||||
}
|
||||
assertThat(result).isInstanceOfSatisfying(Mono::class.java) {
|
||||
assertThat(it.block()).isEqualTo(value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Invoking suspending function on bridged method should return Mono`() {
|
||||
@@ -54,11 +54,11 @@ class AopUtilsKotlinTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
suspend fun suspendingFunction(value: String): String {
|
||||
delay(1)
|
||||
return value
|
||||
}
|
||||
@Suppress("unused")
|
||||
suspend fun suspendingFunction(value: String): String {
|
||||
delay(1)
|
||||
return value
|
||||
}
|
||||
|
||||
class WithoutInterface {
|
||||
suspend fun handle(value: String): String {
|
||||
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -28,10 +28,10 @@ import org.springframework.scheduling.annotation.Async;
|
||||
* <p>This aspect routes methods marked with the {@link Async} annotation as well as methods
|
||||
* in classes marked with the same. Any method expected to be routed asynchronously must
|
||||
* return either {@code void}, {@link Future}, or a subtype of {@link Future} (in particular,
|
||||
* Spring's {@link org.springframework.util.concurrent.ListenableFuture}). This aspect,
|
||||
* therefore, will produce a compile-time error for methods that violate this constraint
|
||||
* on the return type. If, however, a class marked with {@code @Async} contains a method
|
||||
* that violates this constraint, it produces only a warning.
|
||||
* {@link java.util.concurrent.CompletableFuture}). This aspect, therefore, will produce a
|
||||
* compile-time error for methods that violate this constraint on the return type. If,
|
||||
* however, a class marked with {@code @Async} contains a method that violates this
|
||||
* constraint, it produces only a warning.
|
||||
*
|
||||
* <p>This aspect needs to be injected with an implementation of a task-oriented
|
||||
* {@link java.util.concurrent.Executor} to activate it for a specific thread pool,
|
||||
|
||||
+9
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -658,6 +658,14 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
growCollectionIfNecessary(list, index, indexedPropertyName.toString(), ph, i + 1);
|
||||
value = list.get(index);
|
||||
}
|
||||
else if (value instanceof Map map) {
|
||||
Class<?> mapKeyType = ph.getResolvableType().getNested(i + 1).asMap().resolveGeneric(0);
|
||||
// IMPORTANT: Do not pass full property name in here - property editors
|
||||
// must not kick in for map keys but rather only for map values.
|
||||
TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType);
|
||||
Object convertedMapKey = convertIfNecessary(null, null, key, mapKeyType, typeDescriptor);
|
||||
value = map.get(convertedMapKey);
|
||||
}
|
||||
else if (value instanceof Iterable iterable) {
|
||||
// Apply index to Iterator in case of a Set/Collection/Iterable.
|
||||
int index = Integer.parseInt(key);
|
||||
@@ -685,14 +693,6 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
currIndex + ", accessed using property path '" + propertyName + "'");
|
||||
}
|
||||
}
|
||||
else if (value instanceof Map map) {
|
||||
Class<?> mapKeyType = ph.getResolvableType().getNested(i + 1).asMap().resolveGeneric(0);
|
||||
// IMPORTANT: Do not pass full property name in here - property editors
|
||||
// must not kick in for map keys but rather only for map values.
|
||||
TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType);
|
||||
Object convertedMapKey = convertIfNecessary(null, null, key, mapKeyType, typeDescriptor);
|
||||
value = map.get(convertedMapKey);
|
||||
}
|
||||
else {
|
||||
throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
|
||||
"Property referenced in indexed property path '" + propertyName +
|
||||
|
||||
+33
-19
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -92,9 +92,13 @@ public interface ListableBeanFactory extends BeanFactory {
|
||||
* Return a provider for the specified bean, allowing for lazy on-demand retrieval
|
||||
* of instances, including availability and uniqueness options.
|
||||
* @param requiredType type the bean must match; can be an interface or superclass
|
||||
* @param allowEagerInit whether stream-based access may initialize <i>lazy-init
|
||||
* singletons</i> and <i>objects created by FactoryBeans</i> (or by factory methods
|
||||
* with a "factory-bean" reference) for the type check
|
||||
* @param allowEagerInit whether stream access may introspect <i>lazy-init singletons</i>
|
||||
* and <i>objects created by FactoryBeans</i> - or by factory methods with a
|
||||
* "factory-bean" reference - for the type check. Note that FactoryBeans need to be
|
||||
* eagerly initialized to determine their type: So be aware that passing in "true"
|
||||
* for this flag will initialize FactoryBeans and "factory-bean" references. Only
|
||||
* actually necessary initialization for type checking purposes will be performed;
|
||||
* constructor and method invocations will still be avoided as far as possible.
|
||||
* @return a corresponding provider handle
|
||||
* @since 5.3
|
||||
* @see #getBeanProvider(ResolvableType, boolean)
|
||||
@@ -112,9 +116,13 @@ public interface ListableBeanFactory extends BeanFactory {
|
||||
* injection points. For programmatically retrieving a list of beans matching a
|
||||
* specific type, specify the actual bean type as an argument here and subsequently
|
||||
* use {@link ObjectProvider#orderedStream()} or its lazy streaming/iteration options.
|
||||
* @param allowEagerInit whether stream-based access may initialize <i>lazy-init
|
||||
* singletons</i> and <i>objects created by FactoryBeans</i> (or by factory methods
|
||||
* with a "factory-bean" reference) for the type check
|
||||
* @param allowEagerInit whether stream access may introspect <i>lazy-init singletons</i>
|
||||
* and <i>objects created by FactoryBeans</i> - or by factory methods with a
|
||||
* "factory-bean" reference - for the type check. Note that FactoryBeans need to be
|
||||
* eagerly initialized to determine their type: So be aware that passing in "true"
|
||||
* for this flag will initialize FactoryBeans and "factory-bean" references. Only
|
||||
* actually necessary initialization for type checking purposes will be performed;
|
||||
* constructor and method invocations will still be avoided as far as possible.
|
||||
* @return a corresponding provider handle
|
||||
* @since 5.3
|
||||
* @see #getBeanProvider(ResolvableType)
|
||||
@@ -175,11 +183,13 @@ public interface ListableBeanFactory extends BeanFactory {
|
||||
* @param type the generically typed class or interface to match
|
||||
* @param includeNonSingletons whether to include prototype or scoped beans too
|
||||
* or just singletons (also applies to FactoryBeans)
|
||||
* @param allowEagerInit whether to initialize <i>lazy-init singletons</i> and
|
||||
* <i>objects created by FactoryBeans</i> (or by factory methods with a
|
||||
* "factory-bean" reference) for the type check. Note that FactoryBeans need to be
|
||||
* @param allowEagerInit whether to introspect <i>lazy-init singletons</i>
|
||||
* and <i>objects created by FactoryBeans</i> - or by factory methods with a
|
||||
* "factory-bean" reference - for the type check. Note that FactoryBeans need to be
|
||||
* eagerly initialized to determine their type: So be aware that passing in "true"
|
||||
* for this flag will initialize FactoryBeans and "factory-bean" references.
|
||||
* for this flag will initialize FactoryBeans and "factory-bean" references. Only
|
||||
* actually necessary initialization for type checking purposes will be performed;
|
||||
* constructor and method invocations will still be avoided as far as possible.
|
||||
* @return the names of beans (or objects created by FactoryBeans) matching
|
||||
* the given object type (including subclasses), or an empty array if none
|
||||
* @since 5.2
|
||||
@@ -236,11 +246,13 @@ public interface ListableBeanFactory extends BeanFactory {
|
||||
* @param type the class or interface to match, or {@code null} for all bean names
|
||||
* @param includeNonSingletons whether to include prototype or scoped beans too
|
||||
* or just singletons (also applies to FactoryBeans)
|
||||
* @param allowEagerInit whether to initialize <i>lazy-init singletons</i> and
|
||||
* <i>objects created by FactoryBeans</i> (or by factory methods with a
|
||||
* "factory-bean" reference) for the type check. Note that FactoryBeans need to be
|
||||
* @param allowEagerInit whether to introspect <i>lazy-init singletons</i>
|
||||
* and <i>objects created by FactoryBeans</i> - or by factory methods with a
|
||||
* "factory-bean" reference - for the type check. Note that FactoryBeans need to be
|
||||
* eagerly initialized to determine their type: So be aware that passing in "true"
|
||||
* for this flag will initialize FactoryBeans and "factory-bean" references.
|
||||
* for this flag will initialize FactoryBeans and "factory-bean" references. Only
|
||||
* actually necessary initialization for type checking purposes will be performed;
|
||||
* constructor and method invocations will still be avoided as far as possible.
|
||||
* @return the names of beans (or objects created by FactoryBeans) matching
|
||||
* the given object type (including subclasses), or an empty array if none
|
||||
* @see FactoryBean#getObjectType
|
||||
@@ -300,11 +312,13 @@ public interface ListableBeanFactory extends BeanFactory {
|
||||
* @param type the class or interface to match, or {@code null} for all concrete beans
|
||||
* @param includeNonSingletons whether to include prototype or scoped beans too
|
||||
* or just singletons (also applies to FactoryBeans)
|
||||
* @param allowEagerInit whether to initialize <i>lazy-init singletons</i> and
|
||||
* <i>objects created by FactoryBeans</i> (or by factory methods with a
|
||||
* "factory-bean" reference) for the type check. Note that FactoryBeans need to be
|
||||
* @param allowEagerInit whether to introspect <i>lazy-init singletons</i>
|
||||
* and <i>objects created by FactoryBeans</i> - or by factory methods with a
|
||||
* "factory-bean" reference - for the type check. Note that FactoryBeans need to be
|
||||
* eagerly initialized to determine their type: So be aware that passing in "true"
|
||||
* for this flag will initialize FactoryBeans and "factory-bean" references.
|
||||
* for this flag will initialize FactoryBeans and "factory-bean" references. Only
|
||||
* actually necessary initialization for type checking purposes will be performed;
|
||||
* constructor and method invocations will still be avoided as far as possible.
|
||||
* @return a Map with the matching beans, containing the bean names as
|
||||
* keys and the corresponding bean instances as values
|
||||
* @throws BeansException if a bean could not be created
|
||||
|
||||
+17
-3
@@ -1055,6 +1055,11 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("Removing alias '" + beanName + "' for bean '" + aliasedName +
|
||||
"' due to registration of bean definition for bean '" + beanName + "': [" +
|
||||
beanDefinition + "]");
|
||||
}
|
||||
removeAlias(beanName);
|
||||
}
|
||||
}
|
||||
@@ -1776,12 +1781,15 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
* @param requiredType the target dependency type to match against
|
||||
* @return the name of the candidate with the highest priority,
|
||||
* or {@code null} if none found
|
||||
* @throws NoUniqueBeanDefinitionException if multiple beans are detected with
|
||||
* the same highest priority value
|
||||
* @see #getPriority(Object)
|
||||
*/
|
||||
@Nullable
|
||||
protected String determineHighestPriorityCandidate(Map<String, Object> candidates, Class<?> requiredType) {
|
||||
String highestPriorityBeanName = null;
|
||||
Integer highestPriority = null;
|
||||
boolean highestPriorityConflictDetected = false;
|
||||
for (Map.Entry<String, Object> entry : candidates.entrySet()) {
|
||||
String candidateBeanName = entry.getKey();
|
||||
Object beanInstance = entry.getValue();
|
||||
@@ -1790,13 +1798,12 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
if (candidatePriority != null) {
|
||||
if (highestPriority != null) {
|
||||
if (candidatePriority.equals(highestPriority)) {
|
||||
throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(),
|
||||
"Multiple beans found with the same priority ('" + highestPriority +
|
||||
"') among candidates: " + candidates.keySet());
|
||||
highestPriorityConflictDetected = true;
|
||||
}
|
||||
else if (candidatePriority < highestPriority) {
|
||||
highestPriorityBeanName = candidateBeanName;
|
||||
highestPriority = candidatePriority;
|
||||
highestPriorityConflictDetected = false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -1806,6 +1813,13 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (highestPriorityConflictDetected) {
|
||||
throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(),
|
||||
"Multiple beans found with the same highest priority (" + highestPriority +
|
||||
") among candidates: " + candidates.keySet());
|
||||
|
||||
}
|
||||
return highestPriorityBeanName;
|
||||
}
|
||||
|
||||
|
||||
+28
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -139,6 +139,7 @@ abstract class AbstractPropertyAccessorTests {
|
||||
assertThat(accessor.isReadableProperty("list")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("set")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("map")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("iterableMap")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("myTestBeans")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("xxx")).isFalse();
|
||||
|
||||
@@ -146,6 +147,7 @@ abstract class AbstractPropertyAccessorTests {
|
||||
assertThat(accessor.isWritableProperty("list")).isTrue();
|
||||
assertThat(accessor.isWritableProperty("set")).isTrue();
|
||||
assertThat(accessor.isWritableProperty("map")).isTrue();
|
||||
assertThat(accessor.isWritableProperty("iterableMap")).isTrue();
|
||||
assertThat(accessor.isWritableProperty("myTestBeans")).isTrue();
|
||||
assertThat(accessor.isWritableProperty("xxx")).isFalse();
|
||||
|
||||
@@ -161,6 +163,14 @@ abstract class AbstractPropertyAccessorTests {
|
||||
assertThat(accessor.isReadableProperty("map[key4][0].name")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("map[key4][1]")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("map[key4][1].name")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("map[key999]")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("iterableMap[key1]")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("iterableMap[key1].name")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("iterableMap[key2][0]")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("iterableMap[key2][0].name")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("iterableMap[key2][1]")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("iterableMap[key2][1].name")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("iterableMap[key999]")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("myTestBeans[0]")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("myTestBeans[1]")).isFalse();
|
||||
assertThat(accessor.isReadableProperty("array[key1]")).isFalse();
|
||||
@@ -177,6 +187,14 @@ abstract class AbstractPropertyAccessorTests {
|
||||
assertThat(accessor.isWritableProperty("map[key4][0].name")).isTrue();
|
||||
assertThat(accessor.isWritableProperty("map[key4][1]")).isTrue();
|
||||
assertThat(accessor.isWritableProperty("map[key4][1].name")).isTrue();
|
||||
assertThat(accessor.isWritableProperty("map[key999]")).isTrue();
|
||||
assertThat(accessor.isWritableProperty("iterableMap[key1]")).isTrue();
|
||||
assertThat(accessor.isWritableProperty("iterableMap[key1].name")).isTrue();
|
||||
assertThat(accessor.isWritableProperty("iterableMap[key2][0]")).isTrue();
|
||||
assertThat(accessor.isWritableProperty("iterableMap[key2][0].name")).isTrue();
|
||||
assertThat(accessor.isWritableProperty("iterableMap[key2][1]")).isTrue();
|
||||
assertThat(accessor.isWritableProperty("iterableMap[key2][1].name")).isTrue();
|
||||
assertThat(accessor.isWritableProperty("iterableMap[key999]")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("myTestBeans[0]")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("myTestBeans[1]")).isFalse();
|
||||
assertThat(accessor.isWritableProperty("array[key1]")).isFalse();
|
||||
@@ -1394,6 +1412,9 @@ 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("iterableMap[key1].name")).isEqualTo("nameC");
|
||||
assertThat(accessor.getPropertyValue("iterableMap[key2][0].name")).isEqualTo("nameA");
|
||||
assertThat(accessor.getPropertyValue("iterableMap[key2][1].name")).isEqualTo("nameB");
|
||||
assertThat(accessor.getPropertyValue("myTestBeans[0].name")).isEqualTo("nameZ");
|
||||
|
||||
MutablePropertyValues pvs = new MutablePropertyValues();
|
||||
@@ -1408,6 +1429,9 @@ abstract class AbstractPropertyAccessorTests {
|
||||
pvs.add("map[key4][0].name", "nameA");
|
||||
pvs.add("map[key4][1].name", "nameB");
|
||||
pvs.add("map[key5[foo]].name", "name10");
|
||||
pvs.add("iterableMap[key1].name", "newName1");
|
||||
pvs.add("iterableMap[key2][0].name", "newName2A");
|
||||
pvs.add("iterableMap[key2][1].name", "newName2B");
|
||||
pvs.add("myTestBeans[0].name", "nameZZ");
|
||||
accessor.setPropertyValues(pvs);
|
||||
assertThat(tb0.getName()).isEqualTo("name5");
|
||||
@@ -1427,6 +1451,9 @@ abstract class AbstractPropertyAccessorTests {
|
||||
assertThat(accessor.getPropertyValue("map[key4][0].name")).isEqualTo("nameA");
|
||||
assertThat(accessor.getPropertyValue("map[key4][1].name")).isEqualTo("nameB");
|
||||
assertThat(accessor.getPropertyValue("map[key5[foo]].name")).isEqualTo("name10");
|
||||
assertThat(accessor.getPropertyValue("iterableMap[key1].name")).isEqualTo("newName1");
|
||||
assertThat(accessor.getPropertyValue("iterableMap[key2][0].name")).isEqualTo("newName2A");
|
||||
assertThat(accessor.getPropertyValue("iterableMap[key2][1].name")).isEqualTo("newName2B");
|
||||
assertThat(accessor.getPropertyValue("myTestBeans[0].name")).isEqualTo("nameZZ");
|
||||
}
|
||||
|
||||
|
||||
+84
-4
@@ -885,10 +885,15 @@ class DefaultListableBeanFactoryTests {
|
||||
@Test
|
||||
void beanDefinitionOverriding() {
|
||||
lbf.registerBeanDefinition("test", new RootBeanDefinition(TestBean.class));
|
||||
// Override "test" bean definition.
|
||||
lbf.registerBeanDefinition("test", new RootBeanDefinition(NestedTestBean.class));
|
||||
// Temporary "test2" alias for nonexistent bean.
|
||||
lbf.registerAlias("otherTest", "test2");
|
||||
// Reassign "test2" alias to "test".
|
||||
lbf.registerAlias("test", "test2");
|
||||
// Assign "testX" alias to "test" as well.
|
||||
lbf.registerAlias("test", "testX");
|
||||
// Register new "testX" bean definition which also removes the "testX" alias for "test".
|
||||
lbf.registerBeanDefinition("testX", new RootBeanDefinition(TestBean.class));
|
||||
|
||||
assertThat(lbf.getBean("test")).isInstanceOf(NestedTestBean.class);
|
||||
@@ -1729,18 +1734,73 @@ class DefaultListableBeanFactoryTests {
|
||||
assertThat(bean.getBeanName()).isEqualTo("bd1");
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code determineHighestPriorityCandidate()} should reject duplicate
|
||||
* priorities for the highest priority detected.
|
||||
*
|
||||
* @see #getBeanByTypeWithMultipleNonHighestPriorityCandidates()
|
||||
*/
|
||||
@Test
|
||||
void getBeanByTypeWithMultiplePriority() {
|
||||
void getBeanByTypeWithMultipleHighestPriorityCandidates() {
|
||||
lbf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE);
|
||||
RootBeanDefinition bd1 = new RootBeanDefinition(HighPriorityTestBean.class);
|
||||
RootBeanDefinition bd2 = new RootBeanDefinition(HighPriorityTestBean.class);
|
||||
RootBeanDefinition bd2 = new RootBeanDefinition(LowPriorityTestBean.class);
|
||||
RootBeanDefinition bd3 = new RootBeanDefinition(HighPriorityTestBean.class);
|
||||
lbf.registerBeanDefinition("bd1", bd1);
|
||||
lbf.registerBeanDefinition("bd2", bd2);
|
||||
lbf.registerBeanDefinition("bd3", bd3);
|
||||
|
||||
assertThatExceptionOfType(NoUniqueBeanDefinitionException.class)
|
||||
.isThrownBy(() -> lbf.getBean(TestBean.class))
|
||||
.withMessageContaining("Multiple beans found with the same priority")
|
||||
.withMessageContaining("5"); // conflicting priority
|
||||
.withMessageContaining("Multiple beans found with the same highest priority (5) among candidates: ");
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code determineHighestPriorityCandidate()} should ignore duplicate
|
||||
* priorities for any priority other than the highest, and the order in
|
||||
* which beans is declared should not affect the outcome.
|
||||
*
|
||||
* @see #getBeanByTypeWithMultipleHighestPriorityCandidates()
|
||||
*/
|
||||
@Test // gh-33733
|
||||
void getBeanByTypeWithMultipleNonHighestPriorityCandidates() {
|
||||
getBeanByTypeWithMultipleNonHighestPriorityCandidates(
|
||||
PriorityService1.class,
|
||||
PriorityService2A.class,
|
||||
PriorityService2B.class,
|
||||
PriorityService3.class
|
||||
);
|
||||
|
||||
getBeanByTypeWithMultipleNonHighestPriorityCandidates(
|
||||
PriorityService3.class,
|
||||
PriorityService2B.class,
|
||||
PriorityService2A.class,
|
||||
PriorityService1.class
|
||||
);
|
||||
|
||||
getBeanByTypeWithMultipleNonHighestPriorityCandidates(
|
||||
PriorityService2A.class,
|
||||
PriorityService1.class,
|
||||
PriorityService2B.class,
|
||||
PriorityService3.class
|
||||
);
|
||||
|
||||
getBeanByTypeWithMultipleNonHighestPriorityCandidates(
|
||||
PriorityService2A.class,
|
||||
PriorityService3.class,
|
||||
PriorityService1.class,
|
||||
PriorityService2B.class
|
||||
);
|
||||
}
|
||||
|
||||
private void getBeanByTypeWithMultipleNonHighestPriorityCandidates(Class<?>... classes) {
|
||||
lbf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE);
|
||||
for (Class<?> clazz : classes) {
|
||||
lbf.registerBeanDefinition(clazz.getSimpleName(), new RootBeanDefinition(clazz));
|
||||
}
|
||||
|
||||
PriorityService bean = lbf.getBean(PriorityService.class);
|
||||
assertThat(bean).isExactlyInstanceOf(PriorityService1.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -3500,6 +3560,26 @@ class DefaultListableBeanFactoryTests {
|
||||
}
|
||||
|
||||
|
||||
interface PriorityService {
|
||||
}
|
||||
|
||||
@Priority(1)
|
||||
static class PriorityService1 implements PriorityService {
|
||||
}
|
||||
|
||||
@Priority(2)
|
||||
static class PriorityService2A implements PriorityService {
|
||||
}
|
||||
|
||||
@Priority(2)
|
||||
static class PriorityService2B implements PriorityService {
|
||||
}
|
||||
|
||||
@Priority(3)
|
||||
static class PriorityService3 implements PriorityService {
|
||||
}
|
||||
|
||||
|
||||
@Priority(5)
|
||||
private static class HighPriorityTestBean extends TestBean {
|
||||
}
|
||||
|
||||
+85
-85
@@ -45,99 +45,99 @@ import javax.lang.model.element.Modifier
|
||||
*/
|
||||
class InstanceSupplierCodeGeneratorKotlinTests {
|
||||
|
||||
private val generationContext = TestGenerationContext()
|
||||
private val generationContext = TestGenerationContext()
|
||||
|
||||
@Test
|
||||
fun generateWhenHasDefaultConstructor() {
|
||||
val beanDefinition: BeanDefinition = RootBeanDefinition(KotlinTestBean::class.java)
|
||||
val beanFactory = DefaultListableBeanFactory()
|
||||
compile(beanFactory, beanDefinition) { instanceSupplier, compiled ->
|
||||
val bean = getBean<KotlinTestBean>(beanFactory, beanDefinition, instanceSupplier)
|
||||
Assertions.assertThat(bean).isInstanceOf(KotlinTestBean::class.java)
|
||||
Assertions.assertThat(compiled.sourceFile).contains("InstanceSupplier.using(KotlinTestBean::new)")
|
||||
}
|
||||
Assertions.assertThat(getReflectionHints().getTypeHint(KotlinTestBean::class.java))
|
||||
.satisfies(hasConstructorWithMode(ExecutableMode.INTROSPECT))
|
||||
}
|
||||
@Test
|
||||
fun generateWhenHasDefaultConstructor() {
|
||||
val beanDefinition: BeanDefinition = RootBeanDefinition(KotlinTestBean::class.java)
|
||||
val beanFactory = DefaultListableBeanFactory()
|
||||
compile(beanFactory, beanDefinition) { instanceSupplier, compiled ->
|
||||
val bean = getBean<KotlinTestBean>(beanFactory, beanDefinition, instanceSupplier)
|
||||
Assertions.assertThat(bean).isInstanceOf(KotlinTestBean::class.java)
|
||||
Assertions.assertThat(compiled.sourceFile).contains("InstanceSupplier.using(KotlinTestBean::new)")
|
||||
}
|
||||
Assertions.assertThat(getReflectionHints().getTypeHint(KotlinTestBean::class.java))
|
||||
.satisfies(hasConstructorWithMode(ExecutableMode.INTROSPECT))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateWhenConstructorHasOptionalParameter() {
|
||||
val beanDefinition: BeanDefinition = RootBeanDefinition(KotlinTestBeanWithOptionalParameter::class.java)
|
||||
val beanFactory = DefaultListableBeanFactory()
|
||||
compile(beanFactory, beanDefinition) { instanceSupplier, compiled ->
|
||||
val bean: KotlinTestBeanWithOptionalParameter = getBean(beanFactory, beanDefinition, instanceSupplier)
|
||||
Assertions.assertThat(bean).isInstanceOf(KotlinTestBeanWithOptionalParameter::class.java)
|
||||
Assertions.assertThat(compiled.sourceFile)
|
||||
.contains("return BeanInstanceSupplier.<KotlinTestBeanWithOptionalParameter>forConstructor();")
|
||||
}
|
||||
Assertions.assertThat<TypeHint>(getReflectionHints().getTypeHint(KotlinTestBeanWithOptionalParameter::class.java))
|
||||
.satisfies(hasMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS))
|
||||
}
|
||||
@Test
|
||||
fun generateWhenConstructorHasOptionalParameter() {
|
||||
val beanDefinition: BeanDefinition = RootBeanDefinition(KotlinTestBeanWithOptionalParameter::class.java)
|
||||
val beanFactory = DefaultListableBeanFactory()
|
||||
compile(beanFactory, beanDefinition) { instanceSupplier, compiled ->
|
||||
val bean: KotlinTestBeanWithOptionalParameter = getBean(beanFactory, beanDefinition, instanceSupplier)
|
||||
Assertions.assertThat(bean).isInstanceOf(KotlinTestBeanWithOptionalParameter::class.java)
|
||||
Assertions.assertThat(compiled.sourceFile)
|
||||
.contains("return BeanInstanceSupplier.<KotlinTestBeanWithOptionalParameter>forConstructor();")
|
||||
}
|
||||
Assertions.assertThat<TypeHint>(getReflectionHints().getTypeHint(KotlinTestBeanWithOptionalParameter::class.java))
|
||||
.satisfies(hasMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS))
|
||||
}
|
||||
|
||||
private fun getReflectionHints(): ReflectionHints {
|
||||
return generationContext.runtimeHints.reflection()
|
||||
}
|
||||
private fun getReflectionHints(): ReflectionHints {
|
||||
return generationContext.runtimeHints.reflection()
|
||||
}
|
||||
|
||||
private fun hasConstructorWithMode(mode: ExecutableMode): ThrowingConsumer<TypeHint> {
|
||||
return ThrowingConsumer {
|
||||
Assertions.assertThat(it.constructors()).anySatisfy(hasMode(mode))
|
||||
}
|
||||
}
|
||||
private fun hasConstructorWithMode(mode: ExecutableMode): ThrowingConsumer<TypeHint> {
|
||||
return ThrowingConsumer {
|
||||
Assertions.assertThat(it.constructors()).anySatisfy(hasMode(mode))
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasMemberCategory(category: MemberCategory): ThrowingConsumer<TypeHint> {
|
||||
return ThrowingConsumer {
|
||||
Assertions.assertThat(it.memberCategories).contains(category)
|
||||
}
|
||||
}
|
||||
private fun hasMemberCategory(category: MemberCategory): ThrowingConsumer<TypeHint> {
|
||||
return ThrowingConsumer {
|
||||
Assertions.assertThat(it.memberCategories).contains(category)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasMode(mode: ExecutableMode): ThrowingConsumer<ExecutableHint> {
|
||||
return ThrowingConsumer {
|
||||
Assertions.assertThat(it.mode).isEqualTo(mode)
|
||||
}
|
||||
}
|
||||
private fun hasMode(mode: ExecutableMode): ThrowingConsumer<ExecutableHint> {
|
||||
return ThrowingConsumer {
|
||||
Assertions.assertThat(it.mode).isEqualTo(mode)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T> getBean(beanFactory: DefaultListableBeanFactory, beanDefinition: BeanDefinition,
|
||||
instanceSupplier: InstanceSupplier<*>): T {
|
||||
(beanDefinition as RootBeanDefinition).instanceSupplier = instanceSupplier
|
||||
beanFactory.registerBeanDefinition("testBean", beanDefinition)
|
||||
return beanFactory.getBean("testBean") as T
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T> getBean(beanFactory: DefaultListableBeanFactory, beanDefinition: BeanDefinition,
|
||||
instanceSupplier: InstanceSupplier<*>): T {
|
||||
(beanDefinition as RootBeanDefinition).instanceSupplier = instanceSupplier
|
||||
beanFactory.registerBeanDefinition("testBean", beanDefinition)
|
||||
return beanFactory.getBean("testBean") as T
|
||||
}
|
||||
|
||||
private fun compile(beanFactory: DefaultListableBeanFactory, beanDefinition: BeanDefinition,
|
||||
result: BiConsumer<InstanceSupplier<*>, Compiled>) {
|
||||
private fun compile(beanFactory: DefaultListableBeanFactory, beanDefinition: BeanDefinition,
|
||||
result: BiConsumer<InstanceSupplier<*>, Compiled>) {
|
||||
|
||||
val freshBeanFactory = DefaultListableBeanFactory(beanFactory)
|
||||
freshBeanFactory.registerBeanDefinition("testBean", beanDefinition)
|
||||
val registeredBean = RegisteredBean.of(freshBeanFactory, "testBean")
|
||||
val typeBuilder = DeferredTypeBuilder()
|
||||
val generateClass = generationContext.generatedClasses.addForFeature("TestCode", typeBuilder)
|
||||
val generator = InstanceSupplierCodeGenerator(
|
||||
generationContext, generateClass.name,
|
||||
generateClass.methods, false
|
||||
)
|
||||
val instantiationDescriptor = registeredBean.resolveInstantiationDescriptor()
|
||||
Assertions.assertThat(instantiationDescriptor).isNotNull()
|
||||
val generatedCode = generator.generateCode(registeredBean, instantiationDescriptor)
|
||||
typeBuilder.set { type: TypeSpec.Builder ->
|
||||
type.addModifiers(Modifier.PUBLIC)
|
||||
type.addSuperinterface(
|
||||
ParameterizedTypeName.get(
|
||||
Supplier::class.java,
|
||||
InstanceSupplier::class.java
|
||||
)
|
||||
)
|
||||
type.addMethod(
|
||||
MethodSpec.methodBuilder("get")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.returns(InstanceSupplier::class.java)
|
||||
.addStatement("return \$L", generatedCode).build()
|
||||
)
|
||||
}
|
||||
generationContext.writeGeneratedContent()
|
||||
TestCompiler.forSystem().with(generationContext).compile {
|
||||
result.accept(it.getInstance(Supplier::class.java).get() as InstanceSupplier<*>, it)
|
||||
}
|
||||
}
|
||||
val freshBeanFactory = DefaultListableBeanFactory(beanFactory)
|
||||
freshBeanFactory.registerBeanDefinition("testBean", beanDefinition)
|
||||
val registeredBean = RegisteredBean.of(freshBeanFactory, "testBean")
|
||||
val typeBuilder = DeferredTypeBuilder()
|
||||
val generateClass = generationContext.generatedClasses.addForFeature("TestCode", typeBuilder)
|
||||
val generator = InstanceSupplierCodeGenerator(
|
||||
generationContext, generateClass.name,
|
||||
generateClass.methods, false
|
||||
)
|
||||
val instantiationDescriptor = registeredBean.resolveInstantiationDescriptor()
|
||||
Assertions.assertThat(instantiationDescriptor).isNotNull()
|
||||
val generatedCode = generator.generateCode(registeredBean, instantiationDescriptor)
|
||||
typeBuilder.set { type: TypeSpec.Builder ->
|
||||
type.addModifiers(Modifier.PUBLIC)
|
||||
type.addSuperinterface(
|
||||
ParameterizedTypeName.get(
|
||||
Supplier::class.java,
|
||||
InstanceSupplier::class.java
|
||||
)
|
||||
)
|
||||
type.addMethod(
|
||||
MethodSpec.methodBuilder("get")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.returns(InstanceSupplier::class.java)
|
||||
.addStatement("return \$L", generatedCode).build()
|
||||
)
|
||||
}
|
||||
generationContext.writeGeneratedContent()
|
||||
TestCompiler.forSystem().with(generationContext).compile {
|
||||
result.accept(it.getInstance(Supplier::class.java).get() as InstanceSupplier<*>, it)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+30
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -21,6 +21,7 @@ import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -49,6 +50,8 @@ public class IndexedTestBean {
|
||||
|
||||
private SortedMap sortedMap;
|
||||
|
||||
private IterableMap iterableMap;
|
||||
|
||||
private MyTestBeans myTestBeans;
|
||||
|
||||
|
||||
@@ -73,6 +76,9 @@ public class IndexedTestBean {
|
||||
TestBean tb6 = new TestBean("name6", 0);
|
||||
TestBean tb7 = new TestBean("name7", 0);
|
||||
TestBean tb8 = new TestBean("name8", 0);
|
||||
TestBean tbA = new TestBean("nameA", 0);
|
||||
TestBean tbB = new TestBean("nameB", 0);
|
||||
TestBean tbC = new TestBean("nameC", 0);
|
||||
TestBean tbX = new TestBean("nameX", 0);
|
||||
TestBean tbY = new TestBean("nameY", 0);
|
||||
TestBean tbZ = new TestBean("nameZ", 0);
|
||||
@@ -88,6 +94,12 @@ public class IndexedTestBean {
|
||||
this.map.put("key2", tb5);
|
||||
this.map.put("key.3", tb5);
|
||||
List list = new ArrayList();
|
||||
list.add(tbA);
|
||||
list.add(tbB);
|
||||
this.iterableMap = new IterableMap<>();
|
||||
this.iterableMap.put("key1", tbC);
|
||||
this.iterableMap.put("key2", list);
|
||||
list = new ArrayList();
|
||||
list.add(tbX);
|
||||
list.add(tbY);
|
||||
this.map.put("key4", list);
|
||||
@@ -152,6 +164,14 @@ public class IndexedTestBean {
|
||||
this.sortedMap = sortedMap;
|
||||
}
|
||||
|
||||
public IterableMap getIterableMap() {
|
||||
return this.iterableMap;
|
||||
}
|
||||
|
||||
public void setIterableMap(IterableMap iterableMap) {
|
||||
this.iterableMap = iterableMap;
|
||||
}
|
||||
|
||||
public MyTestBeans getMyTestBeans() {
|
||||
return myTestBeans;
|
||||
}
|
||||
@@ -161,6 +181,15 @@ public class IndexedTestBean {
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public static class IterableMap<K,V> extends LinkedHashMap<K,V> implements Iterable<V> {
|
||||
|
||||
@Override
|
||||
public Iterator<V> iterator() {
|
||||
return values().iterator();
|
||||
}
|
||||
}
|
||||
|
||||
public static class MyTestBeans implements Iterable<TestBean> {
|
||||
|
||||
private final Collection<TestBean> testBeans;
|
||||
|
||||
Vendored
+12
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -303,6 +303,17 @@ public class CaffeineCacheManager implements CacheManager {
|
||||
this.cacheMap.put(name, adaptCaffeineCache(name, cache));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified cache from this cache manager, applying to
|
||||
* custom caches as well as dynamically registered caches at runtime.
|
||||
* @param name the name of the cache
|
||||
* @since 6.1.15
|
||||
*/
|
||||
public void removeCache(String name) {
|
||||
this.customCacheNames.remove(name);
|
||||
this.cacheMap.remove(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapt the given new native Caffeine Cache instance to Spring's {@link Cache}
|
||||
* abstraction for the specified cache name.
|
||||
|
||||
+4
@@ -32,6 +32,10 @@ import org.springframework.lang.Nullable;
|
||||
* {@link #cacheManager()}, {@link #cacheResolver()}, {@link #keyGenerator()}, and
|
||||
* {@link #errorHandler()} for detailed instructions.
|
||||
*
|
||||
* <p><b>NOTE: A {@code CachingConfigurer} will get initialized early.</b>
|
||||
* Do not inject common dependencies into autowired fields directly; instead, consider
|
||||
* declaring a lazy {@link org.springframework.beans.factory.ObjectProvider} for those.
|
||||
*
|
||||
* @author Chris Beams
|
||||
* @author Stephane Nicoll
|
||||
* @since 3.1
|
||||
|
||||
Vendored
+10
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -175,6 +175,15 @@ public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderA
|
||||
return cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified cache from this cache manager.
|
||||
* @param name the name of the cache
|
||||
* @since 6.1.15
|
||||
*/
|
||||
public void removeCache(String name) {
|
||||
this.cacheMap.remove(name);
|
||||
}
|
||||
|
||||
private void recreateCaches() {
|
||||
for (Map.Entry<String, Cache> entry : this.cacheMap.entrySet()) {
|
||||
entry.setValue(createConcurrentMapCache(entry.getKey()));
|
||||
|
||||
+32
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.context.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
@@ -33,6 +34,7 @@ import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.BeanNameGenerator;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.core.annotation.AnnotationAttributes;
|
||||
import org.springframework.core.annotation.MergedAnnotation;
|
||||
import org.springframework.core.annotation.MergedAnnotation.Adapt;
|
||||
@@ -41,6 +43,7 @@ import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
@@ -147,16 +150,26 @@ public class AnnotationBeanNameGenerator implements BeanNameGenerator {
|
||||
Set<String> metaAnnotationTypes = this.metaAnnotationTypesCache.computeIfAbsent(annotationType,
|
||||
key -> getMetaAnnotationTypes(mergedAnnotation));
|
||||
if (isStereotypeWithNameValue(annotationType, metaAnnotationTypes, attributes)) {
|
||||
Object value = attributes.get("value");
|
||||
Object value = attributes.get(MergedAnnotation.VALUE);
|
||||
if (value instanceof String currentName && !currentName.isBlank()) {
|
||||
if (conventionBasedStereotypeCheckCache.add(annotationType) &&
|
||||
metaAnnotationTypes.contains(COMPONENT_ANNOTATION_CLASSNAME) && logger.isWarnEnabled()) {
|
||||
logger.warn("""
|
||||
Support for convention-based stereotype names is deprecated and will \
|
||||
be removed in a future version of the framework. Please annotate the \
|
||||
'value' attribute in @%s with @AliasFor(annotation=Component.class) \
|
||||
to declare an explicit alias for @Component's 'value' attribute."""
|
||||
.formatted(annotationType));
|
||||
if (hasExplicitlyAliasedValueAttribute(mergedAnnotation.getType())) {
|
||||
logger.warn("""
|
||||
Although the 'value' attribute in @%s declares @AliasFor for an attribute \
|
||||
other than @Component's 'value' attribute, the value is still used as the \
|
||||
@Component name based on convention. As of Spring Framework 7.0, such a \
|
||||
'value' attribute will no longer be used as the @Component name."""
|
||||
.formatted(annotationType));
|
||||
}
|
||||
else {
|
||||
logger.warn("""
|
||||
Support for convention-based @Component names is deprecated and will \
|
||||
be removed in a future version of the framework. Please annotate the \
|
||||
'value' attribute in @%s with @AliasFor(annotation=Component.class) \
|
||||
to declare an explicit alias for @Component's 'value' attribute."""
|
||||
.formatted(annotationType));
|
||||
}
|
||||
}
|
||||
if (beanName != null && !currentName.equals(beanName)) {
|
||||
throw new IllegalStateException("Stereotype annotations suggest inconsistent " +
|
||||
@@ -224,7 +237,7 @@ public class AnnotationBeanNameGenerator implements BeanNameGenerator {
|
||||
annotationType.equals("jakarta.inject.Named") ||
|
||||
annotationType.equals("javax.inject.Named");
|
||||
|
||||
return (isStereotype && attributes.containsKey("value"));
|
||||
return (isStereotype && attributes.containsKey(MergedAnnotation.VALUE));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -255,4 +268,14 @@ public class AnnotationBeanNameGenerator implements BeanNameGenerator {
|
||||
return StringUtils.uncapitalizeAsProperty(shortClassName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the supplied annotation type declares a {@code value()} attribute
|
||||
* with an explicit alias configured via {@link AliasFor @AliasFor}.
|
||||
* @since 6.2.3
|
||||
*/
|
||||
private static boolean hasExplicitlyAliasedValueAttribute(Class<? extends Annotation> annotationType) {
|
||||
Method valueAttribute = ReflectionUtils.findMethod(annotationType, MergedAnnotation.VALUE);
|
||||
return (valueAttribute != null && valueAttribute.isAnnotationPresent(AliasFor.class));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -565,9 +565,10 @@ public class ClassPathScanningCandidateComponentProvider implements EnvironmentC
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the given bean definition qualifies as candidate.
|
||||
* <p>The default implementation checks whether the class is not an interface
|
||||
* and not dependent on an enclosing class.
|
||||
* Determine whether the given bean definition qualifies as a candidate component.
|
||||
* <p>The default implementation checks whether the class is not dependent on an
|
||||
* enclosing class as well as whether the class is either concrete (and therefore
|
||||
* not an interface) or has {@link Lookup @Lookup} methods.
|
||||
* <p>Can be overridden in subclasses.
|
||||
* @param beanDefinition the bean definition to check
|
||||
* @return whether the bean definition qualifies as a candidate component
|
||||
|
||||
+2
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -39,7 +39,6 @@ import org.springframework.beans.factory.support.AbstractBeanDefinitionReader;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionReader;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.BeanNameGenerator;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
|
||||
import org.springframework.context.annotation.ConfigurationCondition.ConfigurationPhase;
|
||||
@@ -318,7 +317,7 @@ class ConfigurationClassBeanDefinitionReader {
|
||||
|
||||
// At this point, it's a top-level override (probably XML), just having been parsed
|
||||
// before configuration class processing kicks in...
|
||||
if (this.registry instanceof DefaultListableBeanFactory dlbf && !dlbf.isBeanDefinitionOverridable(beanName)) {
|
||||
if (!this.registry.isBeanDefinitionOverridable(beanName)) {
|
||||
throw new BeanDefinitionStoreException(beanMethod.getConfigurationClass().getResource().getDescription(),
|
||||
beanName, "@Bean definition illegally overridden by existing bean definition: " + existingBeanDef);
|
||||
}
|
||||
|
||||
+28
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -109,8 +109,16 @@ class ConfigurationClassEnhancer {
|
||||
}
|
||||
return configClass;
|
||||
}
|
||||
|
||||
try {
|
||||
Class<?> enhancedClass = createClass(newEnhancer(configClass, classLoader));
|
||||
// Use original ClassLoader if config class not locally loaded in overriding class loader
|
||||
if (classLoader instanceof SmartClassLoader smartClassLoader &&
|
||||
classLoader != configClass.getClassLoader()) {
|
||||
classLoader = smartClassLoader.getOriginalClassLoader();
|
||||
}
|
||||
Enhancer enhancer = newEnhancer(configClass, classLoader);
|
||||
boolean classLoaderMismatch = (classLoader != null && classLoader != configClass.getClassLoader());
|
||||
Class<?> enhancedClass = createClass(enhancer, classLoaderMismatch);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace(String.format("Successfully enhanced %s; enhanced class name is: %s",
|
||||
configClass.getName(), enhancedClass.getName()));
|
||||
@@ -129,6 +137,9 @@ class ConfigurationClassEnhancer {
|
||||
*/
|
||||
private Enhancer newEnhancer(Class<?> configSuperClass, @Nullable ClassLoader classLoader) {
|
||||
Enhancer enhancer = new Enhancer();
|
||||
if (classLoader != null) {
|
||||
enhancer.setClassLoader(classLoader);
|
||||
}
|
||||
enhancer.setSuperclass(configSuperClass);
|
||||
enhancer.setInterfaces(new Class<?>[] {EnhancedConfiguration.class});
|
||||
enhancer.setUseFactory(false);
|
||||
@@ -152,8 +163,21 @@ class ConfigurationClassEnhancer {
|
||||
* Uses enhancer to generate a subclass of superclass,
|
||||
* ensuring that callbacks are registered for the new subclass.
|
||||
*/
|
||||
private Class<?> createClass(Enhancer enhancer) {
|
||||
Class<?> subclass = enhancer.createClass();
|
||||
private Class<?> createClass(Enhancer enhancer, boolean fallback) {
|
||||
Class<?> subclass;
|
||||
try {
|
||||
subclass = enhancer.createClass();
|
||||
}
|
||||
catch (CodeGenerationException ex) {
|
||||
if (!fallback) {
|
||||
throw ex;
|
||||
}
|
||||
// Possibly a package-visible @Bean method declaration not accessible
|
||||
// in the given ClassLoader -> retry with original ClassLoader
|
||||
enhancer.setClassLoader(null);
|
||||
subclass = enhancer.createClass();
|
||||
}
|
||||
|
||||
// Registering callbacks statically (as opposed to thread-local)
|
||||
// is critical for usage in an OSGi environment (SPR-5932)...
|
||||
Enhancer.registerStaticCallbacks(subclass, CALLBACKS);
|
||||
|
||||
+26
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -25,15 +25,29 @@ import java.lang.annotation.Target;
|
||||
/**
|
||||
* Declares that a field or method parameter should be formatted as a date or time.
|
||||
*
|
||||
* <p>Supports formatting by style pattern, ISO date time pattern, or custom format pattern string.
|
||||
* <p>Formatting applies to parsing a date/time object from a string as well as printing a
|
||||
* date/time object to a string.
|
||||
*
|
||||
* <p>Supports formatting by style pattern, ISO date/time pattern, or custom format pattern string.
|
||||
* Can be applied to {@link java.util.Date}, {@link java.util.Calendar}, {@link Long} (for
|
||||
* millisecond timestamps) as well as JSR-310 {@code java.time} value types.
|
||||
*
|
||||
* <p>For style-based formatting, set the {@link #style} attribute to the desired style pattern code.
|
||||
* The first character of the code is the date style, and the second character is the time style.
|
||||
* Specify a character of 'S' for short style, 'M' for medium, 'L' for long, and 'F' for full.
|
||||
* The date or time may be omitted by specifying the style character '-' — for example,
|
||||
* 'M-' specifies a medium format for the date with no time.
|
||||
* The date or time may be omitted by specifying the style character '-'. For example,
|
||||
* 'M-' specifies a medium format for the date with no time. The supported style pattern codes
|
||||
* correlate to the enum constants defined in {@link java.time.format.FormatStyle}.
|
||||
*
|
||||
* <p><strong>WARNING</strong>: Style-based formatting and parsing rely on locale-sensitive
|
||||
* patterns which may change depending on the Java runtime. Specifically, applications that
|
||||
* rely on date/time parsing and formatting may encounter incompatible changes in behavior
|
||||
* when running on JDK 20 or higher. Using an ISO standardized format or a concrete pattern
|
||||
* that you control allows for reliable system-independent and locale-independent parsing and
|
||||
* formatting of date/time values. The use of {@linkplain #fallbackPatterns() fallback patterns}
|
||||
* can also help to address compatibility issues. For further details, see the
|
||||
* <a href="https://github.com/spring-projects/spring-framework/wiki/Date-and-Time-Formatting-with-JDK-20-and-higher">
|
||||
* Date and Time Formatting with JDK 20 and higher</a> page in the Spring Framework wiki.
|
||||
*
|
||||
* <p>For ISO-based formatting, set the {@link #iso} attribute to the desired {@link ISO} format,
|
||||
* such as {@link ISO#DATE}.
|
||||
@@ -65,6 +79,8 @@ import java.lang.annotation.Target;
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 3.0
|
||||
* @see java.text.DateFormat
|
||||
* @see java.text.SimpleDateFormat
|
||||
* @see java.time.format.DateTimeFormatter
|
||||
*/
|
||||
@Documented
|
||||
@@ -77,6 +93,8 @@ public @interface DateTimeFormat {
|
||||
* <p>Defaults to 'SS' for short date, short time. Set this attribute when you
|
||||
* wish to format your field or method parameter in accordance with a common
|
||||
* style other than the default style.
|
||||
* <p>See the {@linkplain DateTimeFormat class-level documentation} for further
|
||||
* details.
|
||||
* @see #fallbackPatterns
|
||||
*/
|
||||
String style() default "SS";
|
||||
@@ -93,13 +111,13 @@ public @interface DateTimeFormat {
|
||||
|
||||
/**
|
||||
* The custom pattern to use to format the field or method parameter.
|
||||
* <p>Defaults to empty String, indicating no custom pattern String has been
|
||||
* <p>Defaults to an empty String, indicating no custom pattern String has been
|
||||
* specified. Set this attribute when you wish to format your field or method
|
||||
* parameter in accordance with a custom date time pattern not represented by
|
||||
* a style or ISO format.
|
||||
* <p>Note: This pattern follows the original {@link java.text.SimpleDateFormat} style,
|
||||
* as also supported by Joda-Time, with strict parsing semantics towards overflows
|
||||
* (e.g. rejecting a Feb 29 value for a non-leap-year). As a consequence, 'yy'
|
||||
* <p>Note: This pattern follows the original {@link java.text.SimpleDateFormat}
|
||||
* style, with strict parsing semantics towards overflows (for example, rejecting
|
||||
* a {@code Feb 29} value for a non-leap-year). As a consequence, 'yy'
|
||||
* characters indicate a year in the traditional style, not a "year-of-era" as in the
|
||||
* {@link java.time.format.DateTimeFormatter} specification (i.e. 'yy' turns into 'uu'
|
||||
* when going through a {@code DateTimeFormatter} with strict resolution mode).
|
||||
@@ -121,7 +139,6 @@ public @interface DateTimeFormat {
|
||||
* or {@link #style} attribute is always used for printing. For details on
|
||||
* which time zone is used for fallback patterns, see the
|
||||
* {@linkplain DateTimeFormat class-level documentation}.
|
||||
* <p>Fallback patterns are not supported for Joda-Time value types.
|
||||
* @since 5.3.5
|
||||
*/
|
||||
String[] fallbackPatterns() default {};
|
||||
|
||||
@@ -181,7 +181,6 @@ public class DateFormatter implements Formatter<Date> {
|
||||
* <li>'F' = Full</li>
|
||||
* <li>'-' = Omitted</li>
|
||||
* </ul>
|
||||
* This method mimics the styles supported by Joda-Time.
|
||||
* @param stylePattern two characters from the set {"S", "M", "L", "F", "-"}
|
||||
* @since 3.2
|
||||
*/
|
||||
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -116,7 +116,7 @@ public class DateTimeFormatterFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the two characters to use to format date values, in Joda-Time style.
|
||||
* Set the two characters to use to format date values.
|
||||
* <p>The first character is used for the date style; the second is for
|
||||
* the time style. Supported characters are:
|
||||
* <ul>
|
||||
@@ -126,9 +126,9 @@ public class DateTimeFormatterFactory {
|
||||
* <li>'F' = Full</li>
|
||||
* <li>'-' = Omitted</li>
|
||||
* </ul>
|
||||
* <p>This method mimics the styles supported by Joda-Time. Note that
|
||||
* JSR-310 natively favors {@link java.time.format.FormatStyle} as used for
|
||||
* {@link #setDateStyle}, {@link #setTimeStyle} and {@link #setDateTimeStyle}.
|
||||
* <p>Note that JSR-310 natively favors {@link java.time.format.FormatStyle}
|
||||
* as used for {@link #setDateStyle}, {@link #setTimeStyle}, and
|
||||
* {@link #setDateTimeStyle}.
|
||||
* @param style two characters from the set {"S", "M", "L", "F", "-"}
|
||||
*/
|
||||
public void setStylePattern(String style) {
|
||||
|
||||
+2
-2
@@ -38,8 +38,8 @@ abstract class DateTimeFormatterUtils {
|
||||
* @see ResolverStyle#STRICT
|
||||
*/
|
||||
static DateTimeFormatter createStrictDateTimeFormatter(String pattern) {
|
||||
// Using strict resolution to align with Joda-Time and standard DateFormat behavior:
|
||||
// otherwise, an overflow like e.g. Feb 29 for a non-leap-year wouldn't get rejected.
|
||||
// Using strict resolution to align with standard DateFormat behavior:
|
||||
// otherwise, an overflow like, for example, Feb 29 for a non-leap-year wouldn't get rejected.
|
||||
// However, with strict resolution, a year digit needs to be specified as 'u'...
|
||||
String patternToUse = StringUtils.replace(pattern, "yy", "uu");
|
||||
return DateTimeFormatter.ofPattern(patternToUse).withResolverStyle(ResolverStyle.STRICT);
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -97,7 +97,7 @@ public class DefaultFormattingConversionService extends FormattingConversionServ
|
||||
|
||||
/**
|
||||
* Add formatters appropriate for most environments: including number formatters,
|
||||
* JSR-354 Money & Currency formatters, JSR-310 Date-Time and/or Joda-Time formatters,
|
||||
* JSR-354 Money & Currency formatters, and JSR-310 Date-Time formatters,
|
||||
* depending on the presence of the corresponding API on the classpath.
|
||||
* @param formatterRegistry the service to register default formatters with
|
||||
*/
|
||||
|
||||
+7
-12
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -32,24 +32,19 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.StringValueResolver;
|
||||
|
||||
/**
|
||||
* A factory providing convenient access to a {@code FormattingConversionService}
|
||||
* configured with converters and formatters for common types such as numbers and
|
||||
* datetimes.
|
||||
* A factory providing convenient access to a {@link FormattingConversionService}
|
||||
* configured with converters and formatters for common types such as numbers, dates,
|
||||
* and times.
|
||||
*
|
||||
* <p>Additional converters and formatters can be registered declaratively through
|
||||
* {@link #setConverters(Set)} and {@link #setFormatters(Set)}. Another option
|
||||
* is to register converters and formatters in code by implementing the
|
||||
* {@link FormatterRegistrar} interface. You can then configure provide the set
|
||||
* of registrars to use through {@link #setFormatterRegistrars(Set)}.
|
||||
*
|
||||
* <p>A good example for registering converters and formatters in code is
|
||||
* {@code JodaTimeFormatterRegistrar}, which registers a number of
|
||||
* date-related formatters and converters. For a more detailed list of cases
|
||||
* see {@link #setFormatterRegistrars(Set)}
|
||||
* {@link FormatterRegistrar} interface. You can then provide the set of registrars
|
||||
* to use through {@link #setFormatterRegistrars(Set)}.
|
||||
*
|
||||
* <p>Like all {@code FactoryBean} implementations, this class is suitable for
|
||||
* use when configuring a Spring application context using Spring {@code <beans>}
|
||||
* XML. When configuring the container with
|
||||
* XML configuration files. When configuring the container with
|
||||
* {@link org.springframework.context.annotation.Configuration @Configuration}
|
||||
* classes, simply instantiate, configure and return the appropriate
|
||||
* {@code FormattingConversionService} object from a
|
||||
|
||||
+30
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.instrument.classloading.jboss;
|
||||
|
||||
import java.lang.instrument.ClassFileTransformer;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
@@ -26,13 +27,14 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.function.ThrowingFunction;
|
||||
|
||||
/**
|
||||
* {@link LoadTimeWeaver} implementation for JBoss's instrumentable ClassLoader.
|
||||
* Thanks to Ales Justin and Marius Bogoevici for the initial prototype.
|
||||
*
|
||||
* <p>As of Spring Framework 5.0, this weaver supports WildFly 8+.
|
||||
* As of Spring Framework 5.1.5, it also supports WildFly 13+.
|
||||
* <p>This weaver supports WildFly 13-23 (DelegatingClassFileTransformer) as well as
|
||||
* WildFly 24+ (DelegatingClassTransformer), as of Spring Framework 6.1.15.
|
||||
*
|
||||
* @author Costin Leau
|
||||
* @author Juergen Hoeller
|
||||
@@ -40,9 +42,15 @@ import org.springframework.util.ReflectionUtils;
|
||||
*/
|
||||
public class JBossLoadTimeWeaver implements LoadTimeWeaver {
|
||||
|
||||
private static final String DELEGATING_TRANSFORMER_CLASS_NAME =
|
||||
private static final String LEGACY_DELEGATING_TRANSFORMER_CLASS_NAME =
|
||||
"org.jboss.as.server.deployment.module.DelegatingClassFileTransformer";
|
||||
|
||||
private static final String DELEGATING_TRANSFORMER_CLASS_NAME =
|
||||
"org.jboss.as.server.deployment.module.DelegatingClassTransformer";
|
||||
|
||||
private static final String CLASS_TRANSFORMER_CLASS_NAME =
|
||||
"org.jboss.modules.ClassTransformer";
|
||||
|
||||
private static final String WRAPPER_TRANSFORMER_CLASS_NAME =
|
||||
"org.jboss.modules.JLIClassTransformer";
|
||||
|
||||
@@ -53,6 +61,8 @@ public class JBossLoadTimeWeaver implements LoadTimeWeaver {
|
||||
|
||||
private final Method addTransformer;
|
||||
|
||||
private final ThrowingFunction<Object, Object> adaptTransformer;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new instance of the {@link JBossLoadTimeWeaver} class using
|
||||
@@ -91,18 +101,29 @@ public class JBossLoadTimeWeaver implements LoadTimeWeaver {
|
||||
wrappedTransformer.setAccessible(true);
|
||||
suggestedTransformer = wrappedTransformer.get(suggestedTransformer);
|
||||
}
|
||||
if (!suggestedTransformer.getClass().getName().equals(DELEGATING_TRANSFORMER_CLASS_NAME)) {
|
||||
|
||||
Class<?> transformerType = ClassFileTransformer.class;
|
||||
if (suggestedTransformer.getClass().getName().equals(LEGACY_DELEGATING_TRANSFORMER_CLASS_NAME)) {
|
||||
this.adaptTransformer = (t -> t);
|
||||
}
|
||||
else if (suggestedTransformer.getClass().getName().equals(DELEGATING_TRANSFORMER_CLASS_NAME)) {
|
||||
transformerType = classLoader.loadClass(CLASS_TRANSFORMER_CLASS_NAME);
|
||||
Constructor<?> adaptedTransformer = classLoader.loadClass(WRAPPER_TRANSFORMER_CLASS_NAME)
|
||||
.getConstructor(ClassFileTransformer.class);
|
||||
this.adaptTransformer = adaptedTransformer::newInstance;
|
||||
}
|
||||
else {
|
||||
throw new IllegalStateException(
|
||||
"Transformer not of the expected type DelegatingClassFileTransformer: " +
|
||||
"Transformer not of expected type DelegatingClass(File)Transformer: " +
|
||||
suggestedTransformer.getClass().getName());
|
||||
}
|
||||
this.delegatingTransformer = suggestedTransformer;
|
||||
|
||||
Method addTransformer = ReflectionUtils.findMethod(this.delegatingTransformer.getClass(),
|
||||
"addTransformer", ClassFileTransformer.class);
|
||||
"addTransformer", transformerType);
|
||||
if (addTransformer == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Could not find 'addTransformer' method on JBoss DelegatingClassFileTransformer: " +
|
||||
"Could not find 'addTransformer' method on JBoss DelegatingClass(File)Transformer: " +
|
||||
this.delegatingTransformer.getClass().getName());
|
||||
}
|
||||
addTransformer.setAccessible(true);
|
||||
@@ -117,7 +138,7 @@ public class JBossLoadTimeWeaver implements LoadTimeWeaver {
|
||||
@Override
|
||||
public void addTransformer(ClassFileTransformer transformer) {
|
||||
try {
|
||||
this.addTransformer.invoke(this.delegatingTransformer, transformer);
|
||||
this.addTransformer.invoke(this.delegatingTransformer, this.adaptTransformer.apply(transformer));
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
throw new IllegalStateException("Could not add transformer on JBoss ClassLoader: " + this.classLoader, ex);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -35,17 +35,18 @@ import org.springframework.aot.hint.annotation.Reflective;
|
||||
* <p>In terms of target method signatures, any parameter types are supported.
|
||||
* However, the return type is constrained to either {@code void} or
|
||||
* {@link java.util.concurrent.Future}. In the latter case, you may declare the
|
||||
* more specific {@link org.springframework.util.concurrent.ListenableFuture} or
|
||||
* {@link java.util.concurrent.CompletableFuture} types which allow for richer
|
||||
* interaction with the asynchronous task and for immediate composition with
|
||||
* further processing steps.
|
||||
* more specific {@link java.util.concurrent.CompletableFuture} type which allows
|
||||
* for richer interaction with the asynchronous task and for immediate composition
|
||||
* with further processing steps.
|
||||
*
|
||||
* <p>A {@code Future} handle returned from the proxy will be an actual asynchronous
|
||||
* {@code Future} that can be used to track the result of the asynchronous method
|
||||
* execution. However, since the target method needs to implement the same signature,
|
||||
* it will have to return a temporary {@code Future} handle that just passes a value
|
||||
* through: for example, Spring's {@link AsyncResult}, EJB 3.1's {@link jakarta.ejb.AsyncResult},
|
||||
* or {@link java.util.concurrent.CompletableFuture#completedFuture(Object)}.
|
||||
* {@code (Completable)Future} that can be used to track the result of the
|
||||
* asynchronous method execution. However, since the target method needs to implement
|
||||
* the same signature, it will have to return a temporary {@code Future} handle that
|
||||
* just passes a value after computation in the execution thread: typically through
|
||||
* {@link java.util.concurrent.CompletableFuture#completedFuture(Object)}. The
|
||||
* provided value will be exposed to the caller through the actual asynchronous
|
||||
* {@code Future} handle at runtime.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Chris Beams
|
||||
|
||||
+3
-1
@@ -78,7 +78,9 @@ import org.springframework.util.ErrorHandler;
|
||||
* but rather just the hand-off to an execution thread.</b> As a consequence,
|
||||
* a {@link ScheduledFuture} handle (e.g. from {@link #schedule(Runnable, Instant)})
|
||||
* represents that hand-off rather than the actual completion of the provided task
|
||||
* (or series of repeated tasks).
|
||||
* (or series of repeated tasks). Also, this scheduler participates in lifecycle
|
||||
* management to a limited degree only, stopping trigger firing and fixed-delay
|
||||
* task execution but not stopping the execution of handed-off tasks.
|
||||
*
|
||||
* <p>As an alternative to the built-in thread-per-task capability, this scheduler
|
||||
* can also be configured with a separate target executor for scheduled task
|
||||
|
||||
+11
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -317,8 +317,16 @@ final class QuartzCronField extends CronField {
|
||||
private static TemporalAdjuster dayOfWeekInMonth(int ordinal, DayOfWeek dayOfWeek) {
|
||||
TemporalAdjuster adjuster = TemporalAdjusters.dayOfWeekInMonth(ordinal, dayOfWeek);
|
||||
return temporal -> {
|
||||
Temporal result = adjuster.adjustInto(temporal);
|
||||
return rollbackToMidnight(temporal, result);
|
||||
// TemporalAdjusters can overflow to a different month
|
||||
// in this case, attempt the same adjustment with the next/previous month
|
||||
for (int i = 0; i < 12; i++) {
|
||||
Temporal result = adjuster.adjustInto(temporal);
|
||||
if (result.get(ChronoField.MONTH_OF_YEAR) == temporal.get(ChronoField.MONTH_OF_YEAR)) {
|
||||
return rollbackToMidnight(temporal, result);
|
||||
}
|
||||
temporal = result;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+9
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.validation.beanvalidation;
|
||||
|
||||
import jakarta.validation.ValidationException;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
/**
|
||||
@@ -39,7 +40,13 @@ public class OptionalValidatorFactoryBean extends LocalValidatorFactoryBean {
|
||||
super.afterPropertiesSet();
|
||||
}
|
||||
catch (ValidationException ex) {
|
||||
LogFactory.getLog(getClass()).debug("Failed to set up a Bean Validation provider", ex);
|
||||
Log logger = LogFactory.getLog(getClass());
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Failed to set up a Bean Validation provider", ex);
|
||||
}
|
||||
else if (logger.isInfoEnabled()) {
|
||||
logger.info("Failed to set up a Bean Validation provider: " + ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -86,7 +86,7 @@ class CacheOperationExpressionEvaluatorTests {
|
||||
AnnotatedClass target = new AnnotatedClass();
|
||||
Method method = ReflectionUtils.findMethod(
|
||||
AnnotatedClass.class, "multipleCaching", Object.class, Object.class);
|
||||
Object[] args = new Object[] {new Object(), new Object()};
|
||||
Object[] args = {"arg1", "arg2"};
|
||||
Collection<ConcurrentMapCache> caches = Collections.singleton(new ConcurrentMapCache("test"));
|
||||
|
||||
EvaluationContext evalCtx = this.eval.createEvaluationContext(caches, method, args,
|
||||
@@ -155,7 +155,7 @@ class CacheOperationExpressionEvaluatorTests {
|
||||
AnnotatedClass target = new AnnotatedClass();
|
||||
Method method = ReflectionUtils.findMethod(
|
||||
AnnotatedClass.class, "multipleCaching", Object.class, Object.class);
|
||||
Object[] args = new Object[] {new Object(), new Object()};
|
||||
Object[] args = new Object[] {"arg1", "arg2"};
|
||||
Collection<ConcurrentMapCache> caches = Collections.singleton(new ConcurrentMapCache("test"));
|
||||
return this.eval.createEvaluationContext(
|
||||
caches, method, args, target, target.getClass(), method, result);
|
||||
|
||||
+75
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -168,6 +168,25 @@ class AnnotationBeanNameGeneratorTests {
|
||||
assertGeneratedName(RestControllerAdviceClass.class, "myRestControllerAdvice");
|
||||
}
|
||||
|
||||
@Test // gh-34317
|
||||
void generateBeanNameFromStereotypeAnnotationWithStringValueAsExplicitAliasForMetaAnnotationOtherThanComponent() {
|
||||
// As of Spring Framework 6.2, "enigma" is incorrectly used as the @Component name.
|
||||
// As of Spring Framework 7.0, the generated name will be "annotationBeanNameGeneratorTests.StereotypeWithoutExplicitName".
|
||||
assertGeneratedName(StereotypeWithoutExplicitName.class, "enigma");
|
||||
}
|
||||
|
||||
@Test // gh-34317
|
||||
void generateBeanNameFromStereotypeAnnotationWithStringValueAndExplicitAliasForComponentNameWithBlankName() {
|
||||
// As of Spring Framework 6.2, "enigma" is incorrectly used as the @Component name.
|
||||
// As of Spring Framework 7.0, the generated name will be "annotationBeanNameGeneratorTests.StereotypeWithGeneratedName".
|
||||
assertGeneratedName(StereotypeWithGeneratedName.class, "enigma");
|
||||
}
|
||||
|
||||
@Test // gh-34317
|
||||
void generateBeanNameFromStereotypeAnnotationWithStringValueAndExplicitAliasForComponentName() {
|
||||
assertGeneratedName(StereotypeWithExplicitName.class, "explicitName");
|
||||
}
|
||||
|
||||
|
||||
private void assertGeneratedName(Class<?> clazz, String expectedName) {
|
||||
BeanDefinition bd = annotatedBeanDef(clazz);
|
||||
@@ -210,7 +229,7 @@ class AnnotationBeanNameGeneratorTests {
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Component
|
||||
@interface ConventionBasedComponent1 {
|
||||
// This intentionally convention-based. Please do not add @AliasFor.
|
||||
// This is intentionally convention-based. Please do not add @AliasFor.
|
||||
// See gh-31093.
|
||||
String value() default "";
|
||||
}
|
||||
@@ -218,7 +237,7 @@ class AnnotationBeanNameGeneratorTests {
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Component
|
||||
@interface ConventionBasedComponent2 {
|
||||
// This intentionally convention-based. Please do not add @AliasFor.
|
||||
// This is intentionally convention-based. Please do not add @AliasFor.
|
||||
// See gh-31093.
|
||||
String value() default "";
|
||||
}
|
||||
@@ -260,7 +279,7 @@ class AnnotationBeanNameGeneratorTests {
|
||||
@Target(ElementType.TYPE)
|
||||
@Controller
|
||||
@interface TestRestController {
|
||||
// This intentionally convention-based. Please do not add @AliasFor.
|
||||
// This is intentionally convention-based. Please do not add @AliasFor.
|
||||
// See gh-31093.
|
||||
String value() default "";
|
||||
}
|
||||
@@ -319,7 +338,6 @@ class AnnotationBeanNameGeneratorTests {
|
||||
String[] basePackages() default {};
|
||||
}
|
||||
|
||||
|
||||
@TestControllerAdvice(basePackages = "com.example", name = "myControllerAdvice")
|
||||
static class ControllerAdviceClass {
|
||||
}
|
||||
@@ -328,4 +346,56 @@ class AnnotationBeanNameGeneratorTests {
|
||||
static class RestControllerAdviceClass {
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.ANNOTATION_TYPE)
|
||||
@interface MetaAnnotationWithStringAttribute {
|
||||
|
||||
String attribute() default "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom stereotype annotation which has a {@code String value} attribute that
|
||||
* is explicitly declared as an alias for an attribute in a meta-annotation
|
||||
* other than {@link Component @Component}.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
@Component
|
||||
@MetaAnnotationWithStringAttribute
|
||||
@interface MyStereotype {
|
||||
|
||||
@AliasFor(annotation = MetaAnnotationWithStringAttribute.class, attribute = "attribute")
|
||||
String value() default "";
|
||||
}
|
||||
|
||||
@MyStereotype("enigma")
|
||||
static class StereotypeWithoutExplicitName {
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom stereotype annotation which is identical to {@link MyStereotype @MyStereotype}
|
||||
* except that it has a {@link #name} attribute that is an explicit alias for
|
||||
* {@link Component#value}.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
@Component
|
||||
@MetaAnnotationWithStringAttribute
|
||||
@interface MyNamedStereotype {
|
||||
|
||||
@AliasFor(annotation = MetaAnnotationWithStringAttribute.class, attribute = "attribute")
|
||||
String value() default "";
|
||||
|
||||
@AliasFor(annotation = Component.class, attribute = "value")
|
||||
String name() default "";
|
||||
}
|
||||
|
||||
@MyNamedStereotype(value = "enigma", name ="explicitName")
|
||||
static class StereotypeWithExplicitName {
|
||||
}
|
||||
|
||||
@MyNamedStereotype(value = "enigma")
|
||||
static class StereotypeWithGeneratedName {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+145
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -18,11 +18,16 @@ package org.springframework.context.annotation;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.security.ProtectionDomain;
|
||||
import java.security.SecureClassLoader;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.OverridingClassLoader;
|
||||
import org.springframework.core.SmartClassLoader;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -36,19 +41,108 @@ class ConfigurationClassEnhancerTests {
|
||||
@Test
|
||||
void enhanceReloadedClass() throws Exception {
|
||||
ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer();
|
||||
|
||||
ClassLoader parentClassLoader = getClass().getClassLoader();
|
||||
CustomClassLoader classLoader = new CustomClassLoader(parentClassLoader);
|
||||
ClassLoader classLoader = new CustomSmartClassLoader(parentClassLoader);
|
||||
Class<?> myClass = parentClassLoader.loadClass(MyConfig.class.getName());
|
||||
configurationClassEnhancer.enhance(myClass, parentClassLoader);
|
||||
Class<?> myReloadedClass = classLoader.loadClass(MyConfig.class.getName());
|
||||
Class<?> enhancedReloadedClass = configurationClassEnhancer.enhance(myReloadedClass, classLoader);
|
||||
assertThat(enhancedReloadedClass.getClassLoader()).isEqualTo(classLoader);
|
||||
Class<?> enhancedClass = configurationClassEnhancer.enhance(myClass, parentClassLoader);
|
||||
assertThat(myClass).isAssignableFrom(enhancedClass);
|
||||
|
||||
myClass = classLoader.loadClass(MyConfig.class.getName());
|
||||
enhancedClass = configurationClassEnhancer.enhance(myClass, classLoader);
|
||||
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader);
|
||||
assertThat(myClass).isAssignableFrom(enhancedClass);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withPublicClass() {
|
||||
ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer();
|
||||
|
||||
ClassLoader classLoader = new URLClassLoader(new URL[0], getClass().getClassLoader());
|
||||
Class<?> enhancedClass = configurationClassEnhancer.enhance(MyConfigWithPublicClass.class, classLoader);
|
||||
assertThat(MyConfigWithPublicClass.class).isAssignableFrom(enhancedClass);
|
||||
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader);
|
||||
|
||||
classLoader = new OverridingClassLoader(getClass().getClassLoader());
|
||||
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithPublicClass.class, classLoader);
|
||||
assertThat(MyConfigWithPublicClass.class).isAssignableFrom(enhancedClass);
|
||||
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
|
||||
|
||||
classLoader = new CustomSmartClassLoader(getClass().getClassLoader());
|
||||
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithPublicClass.class, classLoader);
|
||||
assertThat(MyConfigWithPublicClass.class).isAssignableFrom(enhancedClass);
|
||||
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
|
||||
|
||||
classLoader = new BasicSmartClassLoader(getClass().getClassLoader());
|
||||
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithPublicClass.class, classLoader);
|
||||
assertThat(MyConfigWithPublicClass.class).isAssignableFrom(enhancedClass);
|
||||
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void withNonPublicClass() {
|
||||
ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer();
|
||||
|
||||
ClassLoader classLoader = new URLClassLoader(new URL[0], getClass().getClassLoader());
|
||||
Class<?> enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicClass.class, classLoader);
|
||||
assertThat(MyConfigWithNonPublicClass.class).isAssignableFrom(enhancedClass);
|
||||
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
|
||||
|
||||
classLoader = new OverridingClassLoader(getClass().getClassLoader());
|
||||
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicClass.class, classLoader);
|
||||
assertThat(MyConfigWithNonPublicClass.class).isAssignableFrom(enhancedClass);
|
||||
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
|
||||
|
||||
classLoader = new CustomSmartClassLoader(getClass().getClassLoader());
|
||||
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicClass.class, classLoader);
|
||||
assertThat(MyConfigWithNonPublicClass.class).isAssignableFrom(enhancedClass);
|
||||
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
|
||||
|
||||
classLoader = new BasicSmartClassLoader(getClass().getClassLoader());
|
||||
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicClass.class, classLoader);
|
||||
assertThat(MyConfigWithNonPublicClass.class).isAssignableFrom(enhancedClass);
|
||||
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void withNonPublicMethod() {
|
||||
ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer();
|
||||
|
||||
ClassLoader classLoader = new URLClassLoader(new URL[0], getClass().getClassLoader());
|
||||
Class<?> enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader);
|
||||
assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass);
|
||||
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader);
|
||||
|
||||
classLoader = new OverridingClassLoader(getClass().getClassLoader());
|
||||
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader);
|
||||
assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass);
|
||||
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
|
||||
|
||||
classLoader = new CustomSmartClassLoader(getClass().getClassLoader());
|
||||
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader);
|
||||
assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass);
|
||||
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
|
||||
|
||||
classLoader = new BasicSmartClassLoader(getClass().getClassLoader());
|
||||
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader);
|
||||
assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass);
|
||||
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
static class MyConfig {
|
||||
|
||||
@Bean
|
||||
String myBean() {
|
||||
return "bean";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
public static class MyConfigWithPublicClass {
|
||||
|
||||
@Bean
|
||||
public String myBean() {
|
||||
return "bean";
|
||||
@@ -56,9 +150,29 @@ class ConfigurationClassEnhancerTests {
|
||||
}
|
||||
|
||||
|
||||
static class CustomClassLoader extends SecureClassLoader implements SmartClassLoader {
|
||||
@Configuration
|
||||
static class MyConfigWithNonPublicClass {
|
||||
|
||||
CustomClassLoader(ClassLoader parent) {
|
||||
@Bean
|
||||
public String myBean() {
|
||||
return "bean";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
public static class MyConfigWithNonPublicMethod {
|
||||
|
||||
@Bean
|
||||
String myBean() {
|
||||
return "bean";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static class CustomSmartClassLoader extends SecureClassLoader implements SmartClassLoader {
|
||||
|
||||
CustomSmartClassLoader(ClassLoader parent) {
|
||||
super(parent);
|
||||
}
|
||||
|
||||
@@ -82,6 +196,29 @@ class ConfigurationClassEnhancerTests {
|
||||
public boolean isClassReloadable(Class<?> clazz) {
|
||||
return clazz.getName().contains("MyConfig");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClassLoader getOriginalClassLoader() {
|
||||
return getParent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> publicDefineClass(String name, byte[] b, @Nullable ProtectionDomain protectionDomain) {
|
||||
return defineClass(name, b, 0, b.length, protectionDomain);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static class BasicSmartClassLoader extends SecureClassLoader implements SmartClassLoader {
|
||||
|
||||
BasicSmartClassLoader(ClassLoader parent) {
|
||||
super(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> publicDefineClass(String name, byte[] b, @Nullable ProtectionDomain protectionDomain) {
|
||||
return defineClass(name, b, 0, b.length, protectionDomain);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+91
@@ -339,6 +339,70 @@ class DateFormattingTests {
|
||||
assertThat(bindingResult.getFieldValue(propertyName)).isEqualTo("2021-03-02");
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link SimpleDateBean#styleDateTimeWithFallbackPatternsForPreAndPostJdk20}
|
||||
* configures "SS" as the date/time style to use. Thus, we have to be aware
|
||||
* of the following if we do not configure fallback patterns for parsing.
|
||||
*
|
||||
* <ul>
|
||||
* <li>JDK ≤ 19 requires a standard space before the "PM".
|
||||
* <li>JDK ≥ 20 requires a narrow non-breaking space (NNBSP) before the "PM".
|
||||
* </ul>
|
||||
*
|
||||
* <p>To avoid compatibility issues between JDK versions, we have configured
|
||||
* two fallback patterns which emulate the "SS" style: <code>"MM/dd/yy h:mm a"</code>
|
||||
* matches against a standard space before the "PM", and <code>"MM/dd/yy h:mm\u202Fa"</code>
|
||||
* matches against a narrow non-breaking space (NNBSP) before the "PM".
|
||||
*
|
||||
* <p>Thus, the following should theoretically be supported on any JDK (or at least
|
||||
* JDK 17 - 23, where we have tested it).
|
||||
*
|
||||
* @see #patternDateTime(String)
|
||||
*/
|
||||
@ParameterizedTest(name = "input date: {0}") // gh-33151
|
||||
@ValueSource(strings = {"10/31/09, 12:00 PM", "10/31/09, 12:00\u202FPM"})
|
||||
void styleDateTime_PreAndPostJdk20(String propertyValue) {
|
||||
String propertyName = "styleDateTimeWithFallbackPatternsForPreAndPostJdk20";
|
||||
MutablePropertyValues propertyValues = new MutablePropertyValues();
|
||||
propertyValues.add(propertyName, propertyValue);
|
||||
binder.bind(propertyValues);
|
||||
BindingResult bindingResult = binder.getBindingResult();
|
||||
assertThat(bindingResult.getErrorCount()).isEqualTo(0);
|
||||
String value = binder.getBindingResult().getFieldValue(propertyName).toString();
|
||||
// Since the "SS" style is always used for printing and the underlying format
|
||||
// changes depending on the JDK version, we cannot be certain that a normal
|
||||
// space is used before the "PM". Consequently we have to use a regular
|
||||
// expression to match against any Unicode space character (\p{Zs}).
|
||||
assertThat(value).startsWith("10/31/09").matches(".+?12:00\\p{Zs}PM");
|
||||
}
|
||||
|
||||
/**
|
||||
* To avoid the use of Locale-based styles (such as "MM") for
|
||||
* {@link SimpleDateBean#patternDateTimeWithFallbackPatternForPreAndPostJdk20}, we have configured a
|
||||
* primary pattern (<code>"MM/dd/yy h:mm a"</code>) that matches against a standard space
|
||||
* before the "PM" and a fallback pattern (<code>"MM/dd/yy h:mm\u202Fa"</code> that matches
|
||||
* against a narrow non-breaking space (NNBSP) before the "PM".
|
||||
*
|
||||
* <p>Thus, the following should theoretically be supported on any JDK (or at least
|
||||
* JDK 17 - 23, where we have tested it).
|
||||
*
|
||||
* @see #styleDateTime(String)
|
||||
*/
|
||||
@ParameterizedTest(name = "input date: {0}") // gh-33151
|
||||
@ValueSource(strings = {"10/31/09 3:45 PM", "10/31/09 3:45\u202FPM"})
|
||||
void patternDateTime_PreAndPostJdk20(String propertyValue) {
|
||||
String propertyName = "patternDateTimeWithFallbackPatternForPreAndPostJdk20";
|
||||
MutablePropertyValues propertyValues = new MutablePropertyValues();
|
||||
propertyValues.add(propertyName, propertyValue);
|
||||
binder.bind(propertyValues);
|
||||
BindingResult bindingResult = binder.getBindingResult();
|
||||
assertThat(bindingResult.getErrorCount()).isEqualTo(0);
|
||||
String value = binder.getBindingResult().getFieldValue(propertyName).toString();
|
||||
// Since the "MM/dd/yy h:mm a" primary pattern is always used for printing, we
|
||||
// can be certain that a normal space is used before the "PM".
|
||||
assertThat(value).matches("10/31/09 3:45 PM");
|
||||
}
|
||||
|
||||
@Test
|
||||
void patternDateWithUnsupportedPattern() {
|
||||
String propertyValue = "210302";
|
||||
@@ -389,12 +453,23 @@ class DateFormattingTests {
|
||||
@DateTimeFormat(style = "S-", fallbackPatterns = { "yyyy-MM-dd", "yyyyMMdd", "yyyy.MM.dd" })
|
||||
private Date styleDateWithFallbackPatterns;
|
||||
|
||||
// "SS" style matches either a standard space or a narrow non-breaking space (NNBSP) before AM/PM,
|
||||
// depending on the version of the JDK.
|
||||
// Fallback patterns match a standard space OR a narrow non-breaking space (NNBSP) before AM/PM.
|
||||
@DateTimeFormat(style = "SS", fallbackPatterns = { "M/d/yy, h:mm a", "M/d/yy, h:mm\u202Fa" })
|
||||
private Date styleDateTimeWithFallbackPatternsForPreAndPostJdk20;
|
||||
|
||||
@DateTimeFormat(pattern = "M/d/yy h:mm")
|
||||
private Date patternDate;
|
||||
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd", fallbackPatterns = { "M/d/yy", "yyyyMMdd", "yyyy.MM.dd" })
|
||||
private Date patternDateWithFallbackPatterns;
|
||||
|
||||
// Primary pattern matches a standard space before AM/PM.
|
||||
// Fallback pattern matches a narrow non-breaking space (NNBSP) before AM/PM.
|
||||
@DateTimeFormat(pattern = "MM/dd/yy h:mm a", fallbackPatterns = "MM/dd/yy h:mm\u202Fa")
|
||||
private Date patternDateTimeWithFallbackPatternForPreAndPostJdk20;
|
||||
|
||||
@DateTimeFormat(iso = ISO.DATE)
|
||||
private Date isoDate;
|
||||
|
||||
@@ -459,6 +534,14 @@ class DateFormattingTests {
|
||||
this.styleDateWithFallbackPatterns = styleDateWithFallbackPatterns;
|
||||
}
|
||||
|
||||
public Date getStyleDateTimeWithFallbackPatternsForPreAndPostJdk20() {
|
||||
return this.styleDateTimeWithFallbackPatternsForPreAndPostJdk20;
|
||||
}
|
||||
|
||||
public void setStyleDateTimeWithFallbackPatternsForPreAndPostJdk20(Date styleDateTimeWithFallbackPatternsForPreAndPostJdk20) {
|
||||
this.styleDateTimeWithFallbackPatternsForPreAndPostJdk20 = styleDateTimeWithFallbackPatternsForPreAndPostJdk20;
|
||||
}
|
||||
|
||||
public Date getPatternDate() {
|
||||
return this.patternDate;
|
||||
}
|
||||
@@ -475,6 +558,14 @@ class DateFormattingTests {
|
||||
this.patternDateWithFallbackPatterns = patternDateWithFallbackPatterns;
|
||||
}
|
||||
|
||||
public Date getPatternDateTimeWithFallbackPatternForPreAndPostJdk20() {
|
||||
return this.patternDateTimeWithFallbackPatternForPreAndPostJdk20;
|
||||
}
|
||||
|
||||
public void setPatternDateTimeWithFallbackPatternForPreAndPostJdk20(Date patternDateTimeWithFallbackPatternForPreAndPostJdk20) {
|
||||
this.patternDateTimeWithFallbackPatternForPreAndPostJdk20 = patternDateTimeWithFallbackPatternForPreAndPostJdk20;
|
||||
}
|
||||
|
||||
public Date getIsoDate() {
|
||||
return this.isoDate;
|
||||
}
|
||||
|
||||
+50
@@ -605,6 +605,41 @@ class DateTimeFormattingTests {
|
||||
assertThat(bindingResult.getFieldValue(propertyName)).asString().matches("12:00:00\\p{Zs}PM");
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link DateTimeBean#styleLocalTimeWithFallbackPatternsForPreAndPostJdk20}
|
||||
* configures "-M" as the time style to use. Thus, we have to be aware
|
||||
* of the following if we do not configure fallback patterns for parsing.
|
||||
*
|
||||
* <ul>
|
||||
* <li>JDK ≤ 19 requires a standard space before the "PM".
|
||||
* <li>JDK ≥ 20 requires a narrow non-breaking space (NNBSP) before the "PM".
|
||||
* </ul>
|
||||
*
|
||||
* <p>To avoid compatibility issues between JDK versions, we have configured
|
||||
* two fallback patterns which emulate the "-M" style: <code>"HH:mm:ss a"</code>
|
||||
* matches against a standard space before the "PM", and <code>"HH:mm:ss\u202Fa"</code>
|
||||
* matches against a narrow non-breaking space (NNBSP) before the "PM".
|
||||
*
|
||||
* <p>Thus, the following should theoretically be supported on any JDK (or at least
|
||||
* JDK 17 - 23, where we have tested it).
|
||||
*/
|
||||
@ParameterizedTest(name = "input date: {0}") // gh-33151
|
||||
@ValueSource(strings = { "12:00:00 PM", "12:00:00\u202FPM" })
|
||||
void styleLocalTime_PreAndPostJdk20(String propertyValue) {
|
||||
String propertyName = "styleLocalTimeWithFallbackPatternsForPreAndPostJdk20";
|
||||
MutablePropertyValues propertyValues = new MutablePropertyValues();
|
||||
propertyValues.add(propertyName, propertyValue);
|
||||
binder.bind(propertyValues);
|
||||
BindingResult bindingResult = binder.getBindingResult();
|
||||
assertThat(bindingResult.getErrorCount()).isEqualTo(0);
|
||||
String value = binder.getBindingResult().getFieldValue(propertyName).toString();
|
||||
// Since the "-M" style is always used for printing and the underlying format
|
||||
// changes depending on the JDK version, we cannot be certain that a normal
|
||||
// space is used before the "PM". Consequently we have to use a regular
|
||||
// expression to match against any Unicode space character (\p{Zs}).
|
||||
assertThat(value).matches("12:00:00\\p{Zs}PM");
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "input date: {0}")
|
||||
@ValueSource(strings = {"2021-03-02T12:00:00", "2021-03-02 12:00:00", "3/2/21 12:00"})
|
||||
void isoLocalDateTime(String propertyValue) {
|
||||
@@ -682,6 +717,12 @@ class DateTimeFormattingTests {
|
||||
@DateTimeFormat(style = "-M", fallbackPatterns = {"HH:mm:ss", "HH:mm"})
|
||||
private LocalTime styleLocalTimeWithFallbackPatterns;
|
||||
|
||||
// "-M" style matches either a standard space or a narrow non-breaking space (NNBSP) before AM/PM,
|
||||
// depending on the version of the JDK.
|
||||
// Fallback patterns match a standard space OR a narrow non-breaking space (NNBSP) before AM/PM.
|
||||
@DateTimeFormat(style = "-M", fallbackPatterns = {"HH:mm:ss a", "HH:mm:ss\u202Fa"})
|
||||
private LocalTime styleLocalTimeWithFallbackPatternsForPreAndPostJdk20;
|
||||
|
||||
private LocalDateTime localDateTime;
|
||||
|
||||
@DateTimeFormat(style = "MM")
|
||||
@@ -782,6 +823,15 @@ class DateTimeFormattingTests {
|
||||
this.styleLocalTimeWithFallbackPatterns = styleLocalTimeWithFallbackPatterns;
|
||||
}
|
||||
|
||||
public LocalTime getStyleLocalTimeWithFallbackPatternsForPreAndPostJdk20() {
|
||||
return this.styleLocalTimeWithFallbackPatternsForPreAndPostJdk20;
|
||||
}
|
||||
|
||||
public void setStyleLocalTimeWithFallbackPatternsForPreAndPostJdk20(
|
||||
LocalTime styleLocalTimeWithFallbackPatternsForPreAndPostJdk20) {
|
||||
this.styleLocalTimeWithFallbackPatternsForPreAndPostJdk20 = styleLocalTimeWithFallbackPatternsForPreAndPostJdk20;
|
||||
}
|
||||
|
||||
public LocalDateTime getLocalDateTime() {
|
||||
return this.localDateTime;
|
||||
}
|
||||
|
||||
+3
-11
@@ -209,18 +209,10 @@ abstract class AbstractSchedulingTaskExecutorTests {
|
||||
CompletableFuture<?> future1 = executor.submitCompletable(new TestTask(this.testName, -1));
|
||||
CompletableFuture<?> future2 = executor.submitCompletable(new TestTask(this.testName, -1));
|
||||
shutdownExecutor();
|
||||
|
||||
try {
|
||||
assertThatExceptionOfType(TimeoutException.class).isThrownBy(() -> {
|
||||
future1.get(1000, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// ignore
|
||||
}
|
||||
Awaitility.await()
|
||||
.atMost(5, TimeUnit.SECONDS)
|
||||
.pollInterval(10, TimeUnit.MILLISECONDS)
|
||||
.untilAsserted(() -> assertThatExceptionOfType(TimeoutException.class)
|
||||
.isThrownBy(() -> future2.get(1000, TimeUnit.MILLISECONDS)));
|
||||
future2.get(1000, TimeUnit.MILLISECONDS);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+16
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@@ -39,6 +39,7 @@ import static java.time.temporal.TemporalAdjusters.next;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link CronExpression}.
|
||||
* @author Arjen Poutsma
|
||||
*/
|
||||
class CronExpressionTests {
|
||||
@@ -1092,6 +1093,20 @@ class CronExpressionTests {
|
||||
assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void quartz5thMondayOfTheMonthDayName() {
|
||||
CronExpression expression = CronExpression.parse("0 0 0 ? * MON#5");
|
||||
|
||||
LocalDateTime last = LocalDateTime.of(2025, 1, 1, 0, 0, 0);
|
||||
|
||||
// first occurrence of 5 mondays in a month from last
|
||||
LocalDateTime expected = LocalDateTime.of(2025, 3, 31, 0, 0, 0);
|
||||
LocalDateTime actual = expression.next(last);
|
||||
assertThat(actual).isNotNull();
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
assertThat(actual.getDayOfWeek()).isEqualTo(MONDAY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void quartzFifthWednesdayOfTheMonth() {
|
||||
CronExpression expression = CronExpression.parse("0 0 0 ? * 3#5");
|
||||
|
||||
+32
-32
@@ -36,45 +36,45 @@ import org.springframework.validation.beanvalidation.BeanValidationBeanRegistrat
|
||||
*/
|
||||
class BeanValidationBeanRegistrationAotProcessorKotlinTests {
|
||||
|
||||
private val processor = BeanValidationBeanRegistrationAotProcessor()
|
||||
private val processor = BeanValidationBeanRegistrationAotProcessor()
|
||||
|
||||
private val generationContext: GenerationContext = TestGenerationContext()
|
||||
private val generationContext: GenerationContext = TestGenerationContext()
|
||||
|
||||
@Test
|
||||
fun shouldProcessMethodParameterLevelConstraint() {
|
||||
process(MethodParameterLevelConstraint::class.java)
|
||||
Assertions.assertThat(
|
||||
RuntimeHintsPredicates.reflection().onType(ExistsValidator::class.java)
|
||||
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)
|
||||
).accepts(generationContext.runtimeHints)
|
||||
}
|
||||
@Test
|
||||
fun shouldProcessMethodParameterLevelConstraint() {
|
||||
process(MethodParameterLevelConstraint::class.java)
|
||||
Assertions.assertThat(
|
||||
RuntimeHintsPredicates.reflection().onType(ExistsValidator::class.java)
|
||||
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)
|
||||
).accepts(generationContext.runtimeHints)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldSkipMethodParameterLevelConstraintWihExtension() {
|
||||
process(MethodParameterLevelConstraintWithExtension::class.java)
|
||||
Assertions.assertThat(generationContext.runtimeHints.reflection().typeHints()).isEmpty()
|
||||
}
|
||||
@Test
|
||||
fun shouldSkipMethodParameterLevelConstraintWihExtension() {
|
||||
process(MethodParameterLevelConstraintWithExtension::class.java)
|
||||
Assertions.assertThat(generationContext.runtimeHints.reflection().typeHints()).isEmpty()
|
||||
}
|
||||
|
||||
private fun process(beanClass: Class<*>) {
|
||||
val contribution = createContribution(beanClass)
|
||||
contribution?.applyTo(generationContext, Mockito.mock())
|
||||
}
|
||||
private fun process(beanClass: Class<*>) {
|
||||
val contribution = createContribution(beanClass)
|
||||
contribution?.applyTo(generationContext, Mockito.mock())
|
||||
}
|
||||
|
||||
private fun createContribution(beanClass: Class<*>): BeanRegistrationAotContribution? {
|
||||
val beanFactory = DefaultListableBeanFactory()
|
||||
beanFactory.registerBeanDefinition(beanClass.name, RootBeanDefinition(beanClass))
|
||||
return processor.processAheadOfTime(RegisteredBean.of(beanFactory, beanClass.name))
|
||||
}
|
||||
private fun createContribution(beanClass: Class<*>): BeanRegistrationAotContribution? {
|
||||
val beanFactory = DefaultListableBeanFactory()
|
||||
beanFactory.registerBeanDefinition(beanClass.name, RootBeanDefinition(beanClass))
|
||||
return processor.processAheadOfTime(RegisteredBean.of(beanFactory, beanClass.name))
|
||||
}
|
||||
|
||||
internal class MethodParameterLevelConstraintWithExtension {
|
||||
internal class MethodParameterLevelConstraintWithExtension {
|
||||
|
||||
@Suppress("unused")
|
||||
fun hello(name: @Exists String): String {
|
||||
return name.toHello()
|
||||
}
|
||||
@Suppress("unused")
|
||||
fun hello(name: @Exists String): String {
|
||||
return name.toHello()
|
||||
}
|
||||
|
||||
private fun String.toHello() =
|
||||
"Hello $this"
|
||||
}
|
||||
private fun String.toHello() =
|
||||
"Hello $this"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -121,14 +121,14 @@ public class DynamicClassLoader extends ClassLoader {
|
||||
return createResourceUrl(name, () -> classBytes);
|
||||
}
|
||||
}
|
||||
ResourceFile resourceFile = this.resourceFiles.get(name);
|
||||
if (resourceFile != null) {
|
||||
return createResourceUrl(resourceFile.getPath(), resourceFile::getBytes);
|
||||
}
|
||||
DynamicResourceFileObject dynamicResourceFile = this.dynamicResourceFiles.get(name);
|
||||
if (dynamicResourceFile != null && dynamicResourceFile.getBytes() != null) {
|
||||
return createResourceUrl(dynamicResourceFile.getName(), dynamicResourceFile::getBytes);
|
||||
}
|
||||
ResourceFile resourceFile = this.resourceFiles.get(name);
|
||||
if (resourceFile != null) {
|
||||
return createResourceUrl(resourceFile.getPath(), resourceFile::getBytes);
|
||||
}
|
||||
return super.findResource(name);
|
||||
}
|
||||
|
||||
|
||||
+6
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -72,11 +72,11 @@ class DynamicJavaFileManager extends ForwardingJavaFileManager<JavaFileManager>
|
||||
@Override
|
||||
public FileObject getFileForOutput(Location location, String packageName,
|
||||
String relativeName, FileObject sibling) {
|
||||
ResourceFile resourceFile = this.resourceFiles.get(relativeName);
|
||||
if (resourceFile != null) {
|
||||
return new DynamicResourceFileObject(relativeName, resourceFile.getContent());
|
||||
}
|
||||
return this.dynamicResourceFiles.computeIfAbsent(relativeName, DynamicResourceFileObject::new);
|
||||
return this.dynamicResourceFiles.computeIfAbsent(relativeName, name -> {
|
||||
ResourceFile resourceFile = this.resourceFiles.get(name);
|
||||
return (resourceFile != null) ? new DynamicResourceFileObject(name, resourceFile.getContent()) :
|
||||
new DynamicResourceFileObject(name);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+19
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.core.test.tools;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.EnumSet;
|
||||
|
||||
@@ -154,6 +155,23 @@ class DynamicJavaFileManagerTests {
|
||||
"META-INF/second.properties");
|
||||
}
|
||||
|
||||
@Test
|
||||
void existingResourceFileCanBeUpdated() throws IOException {
|
||||
try (InputStream input = getResourceOne().openInputStream()) {
|
||||
assertThat(input).hasContent("a");
|
||||
}
|
||||
try (OutputStream output = getResourceOne().openOutputStream()) {
|
||||
output.write('b');
|
||||
}
|
||||
try (InputStream input = getResourceOne().openInputStream()) {
|
||||
assertThat(input).hasContent("b");
|
||||
}
|
||||
}
|
||||
|
||||
private FileObject getResourceOne() {
|
||||
return this.fileManager.getFileForOutput(this.location, "", "com/example/one/resource.one", null);
|
||||
}
|
||||
|
||||
private void writeDummyBytecode(JavaFileObject fileObject) throws IOException {
|
||||
try (OutputStream outputStream = fileObject.openOutputStream()) {
|
||||
StreamUtils.copy(DUMMY_BYTECODE, outputStream);
|
||||
|
||||
+36
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,7 +16,10 @@
|
||||
|
||||
package org.springframework.core.test.tools;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@@ -27,6 +30,8 @@ import javax.annotation.processing.Processor;
|
||||
import javax.annotation.processing.RoundEnvironment;
|
||||
import javax.annotation.processing.SupportedAnnotationTypes;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.tools.FileObject;
|
||||
import javax.tools.StandardLocation;
|
||||
|
||||
import com.example.PublicInterface;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -367,6 +372,14 @@ class TestCompilerTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void getUpdatedResourceAsStream() {
|
||||
SourceFile sourceFile = SourceFile.of(HELLO_WORLD);
|
||||
TestCompiler.forSystem().withResources(ResourceFile.of("com/example/resource", new byte[] { 'a' }))
|
||||
.withProcessors(new ResourceModifyingProcessor()).compile(sourceFile, compiled -> assertThat(
|
||||
compiled.getClassLoader().getResourceAsStream("com/example/resource")).hasContent("b"));
|
||||
}
|
||||
|
||||
private void assertSuppliesHelloWorld(Compiled compiled) {
|
||||
assertThat(compiled.getInstance(Supplier.class).get()).isEqualTo("Hello World!");
|
||||
}
|
||||
@@ -392,4 +405,26 @@ class TestCompilerTests {
|
||||
}
|
||||
}
|
||||
|
||||
@SupportedAnnotationTypes("java.lang.Deprecated")
|
||||
static class ResourceModifyingProcessor extends AbstractProcessor {
|
||||
|
||||
@Override
|
||||
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
|
||||
if (roundEnv.processingOver()) {
|
||||
try {
|
||||
FileObject resource = this.processingEnv.getFiler()
|
||||
.createResource(StandardLocation.CLASS_OUTPUT, "", "com/example/resource");
|
||||
try (OutputStream output = resource.openOutputStream()) {
|
||||
output.write('b');
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -23,8 +23,8 @@ import java.util.regex.Pattern;
|
||||
import org.graalvm.nativeimage.hosted.Feature;
|
||||
|
||||
/**
|
||||
* GraalVM {@link Feature} that substitutes boolean field values that match a certain pattern
|
||||
* with values pre-computed AOT without causing class build-time initialization.
|
||||
* GraalVM {@link Feature} that substitutes boolean field values that match certain patterns
|
||||
* with values pre-computed ahead-of-time without causing class build-time initialization.
|
||||
*
|
||||
* <p>It is possible to pass <pre style="code">-Dspring.native.precompute.log=verbose</pre> as a
|
||||
* <pre style="code">native-image</pre> compiler build argument to display detailed logs
|
||||
|
||||
@@ -309,12 +309,12 @@ public class ResolvableType implements Serializable {
|
||||
|
||||
// Deal with wildcard bounds
|
||||
WildcardBounds ourBounds = WildcardBounds.get(this);
|
||||
WildcardBounds typeBounds = WildcardBounds.get(other);
|
||||
WildcardBounds otherBounds = WildcardBounds.get(other);
|
||||
|
||||
// In the form X is assignable to <? extends Number>
|
||||
if (typeBounds != null) {
|
||||
return (ourBounds != null && ourBounds.isSameKind(typeBounds) &&
|
||||
ourBounds.isAssignableFrom(typeBounds.getBounds()));
|
||||
if (otherBounds != null) {
|
||||
return (ourBounds != null && ourBounds.isSameKind(otherBounds) &&
|
||||
ourBounds.isAssignableFrom(otherBounds.getBounds()));
|
||||
}
|
||||
|
||||
// In the form <? extends Number> is assignable to X...
|
||||
@@ -365,8 +365,8 @@ public class ResolvableType implements Serializable {
|
||||
if (checkGenerics) {
|
||||
// Recursively check each generic
|
||||
ResolvableType[] ourGenerics = getGenerics();
|
||||
ResolvableType[] typeGenerics = other.as(ourResolved).getGenerics();
|
||||
if (ourGenerics.length != typeGenerics.length) {
|
||||
ResolvableType[] otherGenerics = other.as(ourResolved).getGenerics();
|
||||
if (ourGenerics.length != otherGenerics.length) {
|
||||
return false;
|
||||
}
|
||||
if (ourGenerics.length > 0) {
|
||||
@@ -375,7 +375,7 @@ public class ResolvableType implements Serializable {
|
||||
}
|
||||
matchedBefore.put(this.type, other.type);
|
||||
for (int i = 0; i < ourGenerics.length; i++) {
|
||||
if (!ourGenerics[i].isAssignableFrom(typeGenerics[i], true, matchedBefore)) {
|
||||
if (!ourGenerics[i].isAssignableFrom(otherGenerics[i], true, matchedBefore)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -33,7 +33,7 @@ import org.springframework.lang.Nullable;
|
||||
*
|
||||
* <p>First, creates a new Map of the requested targetType with a size equal to the
|
||||
* size of the source Map. Then copies each element in the source map to the target map.
|
||||
* Will perform a conversion from the source maps's parameterized K,V types to the target
|
||||
* Will perform a conversion from the source map's parameterized K,V types to the target
|
||||
* map's parameterized types K,V if necessary.
|
||||
*
|
||||
* @author Keith Donald
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -51,10 +51,11 @@ public class FlightRecorderApplicationStartup implements ApplicationStartup {
|
||||
|
||||
@Override
|
||||
public StartupStep start(String name) {
|
||||
Long parentId = this.currentSteps.getFirst();
|
||||
long sequenceId = this.currentSequenceId.incrementAndGet();
|
||||
this.currentSteps.offerFirst(sequenceId);
|
||||
return new FlightRecorderStartupStep(sequenceId, name,
|
||||
this.currentSteps.getFirst(), committedStep -> this.currentSteps.removeFirstOccurrence(sequenceId));
|
||||
parentId, committedStep -> this.currentSteps.removeFirstOccurrence(sequenceId));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -46,6 +46,11 @@ import org.springframework.util.concurrent.ListenableFutureTask;
|
||||
* executing a large number of short-lived tasks. Alternatively, on JDK 21,
|
||||
* consider setting {@link #setVirtualThreads} to {@code true}.
|
||||
*
|
||||
* <p><b>NOTE: This executor does not participate in context-level lifecycle
|
||||
* management.</b> Tasks on handed-off execution threads cannot be centrally
|
||||
* stopped and restarted; if such tight lifecycle management is necessary,
|
||||
* consider a common {@code ThreadPoolTaskExecutor} setup instead.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @since 2.0
|
||||
* @see #setVirtualThreads
|
||||
@@ -191,6 +196,11 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
|
||||
* The default of -1 indicates no concurrency limit at all.
|
||||
* <p>This is the equivalent of a maximum pool size in a thread pool,
|
||||
* preventing temporary overload of the thread management system.
|
||||
* However, in contrast to a thread pool with a managed task queue,
|
||||
* this executor will block the submitter until the task can be
|
||||
* accepted when the configured concurrency limit has been reached.
|
||||
* If you prefer queue-based task hand-offs without such blocking,
|
||||
* consider using a {@code ThreadPoolTaskExecutor} instead.
|
||||
* @see #UNBOUNDED_CONCURRENCY
|
||||
* @see org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor#setMaxPoolSize
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -85,17 +85,14 @@ public interface ThrowingBiFunction<T, U, R> extends BiFunction<T, U, R> {
|
||||
*/
|
||||
default ThrowingBiFunction<T, U, R> throwing(BiFunction<String, Exception, RuntimeException> exceptionWrapper) {
|
||||
return new ThrowingBiFunction<>() {
|
||||
|
||||
@Override
|
||||
public R applyWithException(T t, U u) throws Exception {
|
||||
return ThrowingBiFunction.this.applyWithException(t, u);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R apply(T t, U u) {
|
||||
return apply(t, u, exceptionWrapper);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -77,17 +77,14 @@ public interface ThrowingConsumer<T> extends Consumer<T> {
|
||||
*/
|
||||
default ThrowingConsumer<T> throwing(BiFunction<String, Exception, RuntimeException> exceptionWrapper) {
|
||||
return new ThrowingConsumer<>() {
|
||||
|
||||
@Override
|
||||
public void acceptWithException(T t) throws Exception {
|
||||
ThrowingConsumer.this.acceptWithException(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(T t) {
|
||||
accept(t, exceptionWrapper);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -80,17 +80,14 @@ public interface ThrowingFunction<T, R> extends Function<T, R> {
|
||||
*/
|
||||
default ThrowingFunction<T, R> throwing(BiFunction<String, Exception, RuntimeException> exceptionWrapper) {
|
||||
return new ThrowingFunction<>() {
|
||||
|
||||
@Override
|
||||
public R applyWithException(T t) throws Exception {
|
||||
return ThrowingFunction.this.applyWithException(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R apply(T t) {
|
||||
return apply(t, exceptionWrapper);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -25,9 +25,8 @@ import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A data size, such as '12MB'.
|
||||
*
|
||||
* <p>This class models data size in terms of bytes and is immutable and thread-safe.
|
||||
* A data size, such as '12MB'. This class models data size in terms of
|
||||
* bytes and is immutable and thread-safe.
|
||||
*
|
||||
* <p>The terms and units used in this class are based on
|
||||
* <a href="https://en.wikipedia.org/wiki/Binary_prefix">binary prefixes</a>
|
||||
@@ -247,14 +246,14 @@ public final class DataSize implements Comparable<DataSize>, Serializable {
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
if (other == null || getClass() != other.getClass()) {
|
||||
return false;
|
||||
}
|
||||
DataSize that = (DataSize) obj;
|
||||
DataSize that = (DataSize) other;
|
||||
return (this.bytes == that.bytes);
|
||||
}
|
||||
|
||||
@@ -279,7 +278,6 @@ public final class DataSize implements Comparable<DataSize>, Serializable {
|
||||
DataUnit defaultUnitToUse = (defaultUnit != null ? defaultUnit : DataUnit.BYTES);
|
||||
return (StringUtils.hasLength(suffix) ? DataUnit.fromSuffix(suffix) : defaultUnitToUse);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -81,6 +81,7 @@ public enum DataUnit {
|
||||
return this.size;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the {@link DataUnit} matching the specified {@code suffix}.
|
||||
* @param suffix one of the standard suffixes
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -169,7 +169,7 @@ class ResolvableTypeTests {
|
||||
|
||||
@Test
|
||||
void forInstanceProviderNull() {
|
||||
ResolvableType type = ResolvableType.forInstance(new MyGenericInterfaceType<String>(null));
|
||||
ResolvableType type = ResolvableType.forInstance(new MyGenericInterfaceType<>(null));
|
||||
assertThat(type.getType()).isEqualTo(MyGenericInterfaceType.class);
|
||||
assertThat(type.resolve()).isEqualTo(MyGenericInterfaceType.class);
|
||||
}
|
||||
@@ -1177,6 +1177,20 @@ class ResolvableTypeTests {
|
||||
assertThatResolvableType(complex4).isNotAssignableFrom(complex3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAssignableFromForUnresolvedWildcards() {
|
||||
ResolvableType wildcard = ResolvableType.forInstance(new Wildcard<>());
|
||||
ResolvableType wildcardFixed = ResolvableType.forInstance(new WildcardFixed());
|
||||
ResolvableType wildcardConcrete = ResolvableType.forClassWithGenerics(Wildcard.class, Number.class);
|
||||
|
||||
assertThat(wildcard.isAssignableFrom(wildcardFixed)).isTrue();
|
||||
assertThat(wildcard.isAssignableFrom(wildcardConcrete)).isTrue();
|
||||
assertThat(wildcardFixed.isAssignableFrom(wildcard)).isFalse();
|
||||
assertThat(wildcardFixed.isAssignableFrom(wildcardConcrete)).isFalse();
|
||||
assertThat(wildcardConcrete.isAssignableFrom(wildcard)).isTrue();
|
||||
assertThat(wildcardConcrete.isAssignableFrom(wildcardFixed)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void identifyTypeVariable() throws Exception {
|
||||
Method method = ClassArguments.class.getMethod("typedArgumentFirst", Class.class, Class.class, Class.class);
|
||||
@@ -1574,7 +1588,6 @@ class ResolvableTypeTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class MySimpleInterfaceType implements MyInterfaceType<String> {
|
||||
}
|
||||
|
||||
@@ -1584,7 +1597,6 @@ class ResolvableTypeTests {
|
||||
public abstract class ExtendsMySimpleInterfaceTypeWithImplementsRaw extends MySimpleInterfaceTypeWithImplementsRaw {
|
||||
}
|
||||
|
||||
|
||||
public class MyCollectionInterfaceType implements MyInterfaceType<Collection<String>> {
|
||||
}
|
||||
|
||||
@@ -1592,20 +1604,17 @@ class ResolvableTypeTests {
|
||||
public abstract class MySuperclassType<T> {
|
||||
}
|
||||
|
||||
|
||||
public class MySimpleSuperclassType extends MySuperclassType<String> {
|
||||
}
|
||||
|
||||
|
||||
public class MyCollectionSuperclassType extends MySuperclassType<Collection<String>> {
|
||||
}
|
||||
|
||||
|
||||
interface Wildcard<T extends Number> extends List<T> {
|
||||
public class Wildcard<T extends Number> {
|
||||
}
|
||||
|
||||
|
||||
interface RawExtendsWildcard extends Wildcard {
|
||||
public class WildcardFixed extends Wildcard<Integer> {
|
||||
}
|
||||
|
||||
|
||||
|
||||
+18
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -156,6 +156,23 @@ class PropertyPlaceholderHelperTests {
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0} -> {1}")
|
||||
@MethodSource("exactMatchPlaceholders")
|
||||
void placeholdersWithExactMatchAreConsidered(String text, String expected) {
|
||||
Properties properties = new Properties();
|
||||
properties.setProperty("prefix://my-service", "example-service");
|
||||
properties.setProperty("px", "prefix");
|
||||
properties.setProperty("p1", "${prefix://my-service}");
|
||||
assertThat(this.helper.replacePlaceholders(text, properties)).isEqualTo(expected);
|
||||
}
|
||||
|
||||
static Stream<Arguments> exactMatchPlaceholders() {
|
||||
return Stream.of(
|
||||
Arguments.of("${prefix://my-service}", "example-service"),
|
||||
Arguments.of("${p1}", "example-service")
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PlaceholderResolver mockPlaceholderResolver(String... pairs) {
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.util.xml;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.util.List;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link DomUtils}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Kunal Jani
|
||||
*/
|
||||
class DomUtilsTests {
|
||||
|
||||
private static final Element SCHOOL_ELEMENT = getDocumentElement("""
|
||||
<?xml version="1.0"?>
|
||||
<school>TestSchool
|
||||
<class teacher="Happy Teacher">Test Teacher One</class>
|
||||
<class teacher="Sad Teacher">Test Teacher Two</class>
|
||||
<principal>Test Principal</principal>
|
||||
<guard>Fox Test</guard>
|
||||
</school>""");
|
||||
|
||||
|
||||
@Test
|
||||
void getChildElementsByTagNameWithSeveralMatchingTags() {
|
||||
List<Element> childElements = DomUtils.getChildElementsByTagName(SCHOOL_ELEMENT, "class", "principal");
|
||||
assertThat(childElements).map(Element::getNodeName).containsExactly("class", "class", "principal");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getChildElementsByTagNameWhenTagDoesNotExist() {
|
||||
assertThat(DomUtils.getChildElementsByTagName(SCHOOL_ELEMENT, "teacher")).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getChildElementByTagNameWithMatchingTag() {
|
||||
Element principalElement = DomUtils.getChildElementByTagName(SCHOOL_ELEMENT, "principal");
|
||||
assertThat(principalElement).isNotNull();
|
||||
assertThat(principalElement.getTextContent()).isEqualTo("Test Principal");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getChildElementByTagNameWithNonMatchingTag() {
|
||||
assertThat(DomUtils.getChildElementByTagName(SCHOOL_ELEMENT, "teacher")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getChildElementValueByTagName() {
|
||||
assertThat(DomUtils.getChildElementValueByTagName(SCHOOL_ELEMENT, "guard")).isEqualTo("Fox Test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getChildElementValueByTagNameWithNonMatchingTag() {
|
||||
assertThat(DomUtils.getChildElementValueByTagName(SCHOOL_ELEMENT, "math tutor")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getChildElements() {
|
||||
List<Element> childElements = DomUtils.getChildElements(SCHOOL_ELEMENT);
|
||||
assertThat(childElements).map(Element::getNodeName).containsExactly("class", "class", "principal", "guard");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTextValueWithCharacterDataNode() {
|
||||
assertThat(DomUtils.getTextValue(SCHOOL_ELEMENT)).isEqualToIgnoringWhitespace("TestSchool");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTextValueWithCommentInXml() {
|
||||
Element elementWithComment = getDocumentElement("""
|
||||
<?xml version="1.0"?>
|
||||
<state>
|
||||
<!-- This is a comment -->
|
||||
<person>Alice</person>
|
||||
</state>""");
|
||||
assertThat(DomUtils.getTextValue(elementWithComment)).isBlank();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTextValueWithEntityReference() {
|
||||
Element elementWithEntityReference = getDocumentElement("""
|
||||
<?xml version="1.0"?>
|
||||
<state>
|
||||
&
|
||||
<person>Alice</person>
|
||||
</state>""");
|
||||
assertThat(DomUtils.getTextValue(elementWithEntityReference)).contains("&");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTextValueWithEmptyElement() {
|
||||
Element emptyElement = getDocumentElement("""
|
||||
<?xml version="1.0"?>
|
||||
<person></person>""");
|
||||
assertThat(DomUtils.getTextValue(emptyElement)).isBlank();
|
||||
}
|
||||
|
||||
@Test
|
||||
void nodeNameEqualsWhenTrue() {
|
||||
assertThat(DomUtils.nodeNameEquals(SCHOOL_ELEMENT, "school")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void nodeNameEqualsWhenFalse() {
|
||||
assertThat(DomUtils.nodeNameEquals(SCHOOL_ELEMENT, "college")).isFalse();
|
||||
}
|
||||
|
||||
|
||||
private static Element getDocumentElement(String xmlContent) {
|
||||
try {
|
||||
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
|
||||
Document document = documentBuilder.parse(new ByteArrayInputStream(xmlContent.getBytes()));
|
||||
return document.getDocumentElement();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException("Failed to parse xml content:%n%s".formatted(xmlContent), ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -22,4 +22,4 @@ package org.springframework.core
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
class KotlinReflectionParameterNameDiscovererTests :
|
||||
AbstractReflectionParameterNameDiscovererKotlinTests(KotlinReflectionParameterNameDiscoverer())
|
||||
AbstractReflectionParameterNameDiscovererKotlinTests(KotlinReflectionParameterNameDiscoverer())
|
||||
|
||||
+14
-11
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2009 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,7 +18,8 @@ package org.springframework.expression;
|
||||
|
||||
/**
|
||||
* Parses expression strings into compiled expressions that can be evaluated.
|
||||
* Supports parsing templates as well as standard expression strings.
|
||||
*
|
||||
* <p>Supports parsing template expressions as well as standard expression strings.
|
||||
*
|
||||
* @author Keith Donald
|
||||
* @author Andy Clement
|
||||
@@ -27,29 +28,31 @@ package org.springframework.expression;
|
||||
public interface ExpressionParser {
|
||||
|
||||
/**
|
||||
* Parse the expression string and return an Expression object you can use for repeated evaluation.
|
||||
* <p>Some examples:
|
||||
* Parse the expression string and return an {@link Expression} object that
|
||||
* can be used for repeated evaluation.
|
||||
* <p>Examples:
|
||||
* <pre class="code">
|
||||
* 3 + 4
|
||||
* name.firstName
|
||||
* </pre>
|
||||
* @param expressionString the raw expression string to parse
|
||||
* @return an evaluator for the parsed expression
|
||||
* @throws ParseException an exception occurred during parsing
|
||||
* @return an {@code Expression} for the parsed expression
|
||||
* @throws ParseException if an exception occurred during parsing
|
||||
*/
|
||||
Expression parseExpression(String expressionString) throws ParseException;
|
||||
|
||||
/**
|
||||
* Parse the expression string and return an Expression object you can use for repeated evaluation.
|
||||
* <p>Some examples:
|
||||
* Parse the expression string and return an {@link Expression} object that
|
||||
* can be used for repeated evaluation.
|
||||
* <p>Examples:
|
||||
* <pre class="code">
|
||||
* 3 + 4
|
||||
* name.firstName
|
||||
* </pre>
|
||||
* @param expressionString the raw expression string to parse
|
||||
* @param context a context for influencing this expression parsing routine (optional)
|
||||
* @return an evaluator for the parsed expression
|
||||
* @throws ParseException an exception occurred during parsing
|
||||
* @param context a context for influencing the expression parsing routine
|
||||
* @return an {@code Expression} for the parsed expression
|
||||
* @throws ParseException if an exception occurred during parsing
|
||||
*/
|
||||
Expression parseExpression(String expressionString, ParserContext context) throws ParseException;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -47,7 +47,7 @@ public interface ParserContext {
|
||||
String getExpressionPrefix();
|
||||
|
||||
/**
|
||||
* For template expressions, return the prefix that identifies the end of an
|
||||
* For template expressions, returns the prefix that identifies the end of an
|
||||
* expression block within a string. For example: "}"
|
||||
* @return the suffix that identifies the end of an expression
|
||||
*/
|
||||
@@ -55,8 +55,9 @@ public interface ParserContext {
|
||||
|
||||
|
||||
/**
|
||||
* The default ParserContext implementation that enables template expression
|
||||
* parsing mode. The expression prefix is "#{" and the expression suffix is "}".
|
||||
* The default {@link ParserContext} implementation that enables template
|
||||
* expression parsing.
|
||||
* <p>The expression prefix is "#{", and the expression suffix is "}".
|
||||
* @see #isTemplate()
|
||||
*/
|
||||
ParserContext TEMPLATE_EXPRESSION = new ParserContext() {
|
||||
|
||||
+7
-4
@@ -29,8 +29,11 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An expression parser that understands templates. It can be subclassed by expression
|
||||
* parsers that do not offer first class support for templating.
|
||||
* Abstract base class for {@linkplain ExpressionParser expression parsers} that
|
||||
* support templates.
|
||||
*
|
||||
* <p>Can be subclassed by expression parsers that offer first class support for
|
||||
* templating.
|
||||
*
|
||||
* @author Keith Donald
|
||||
* @author Juergen Hoeller
|
||||
@@ -88,7 +91,7 @@ public abstract class TemplateAwareExpressionParser implements ExpressionParser
|
||||
* single quote '.
|
||||
* @param expressionString the expression string
|
||||
* @return the parsed expressions
|
||||
* @throws ParseException when the expressions cannot be parsed
|
||||
* @throws ParseException if the expressions cannot be parsed
|
||||
*/
|
||||
private Expression[] parseExpressions(String expressionString, ParserContext context) throws ParseException {
|
||||
List<Expression> expressions = new ArrayList<>();
|
||||
@@ -229,7 +232,7 @@ public abstract class TemplateAwareExpressionParser implements ExpressionParser
|
||||
* @param expressionString the raw expression string to parse
|
||||
* @param context a context for influencing this expression parsing routine (optional)
|
||||
* @return an evaluator for the parsed expression
|
||||
* @throws ParseException an exception occurred during parsing
|
||||
* @throws ParseException if an exception occurred during parsing
|
||||
*/
|
||||
protected abstract Expression doParseExpression(String expressionString, @Nullable ParserContext context)
|
||||
throws ParseException;
|
||||
|
||||
+5
-1
@@ -291,7 +291,11 @@ public enum SpelMessage {
|
||||
|
||||
/** @since 6.0.13 */
|
||||
NEGATIVE_REPEATED_TEXT_COUNT(Kind.ERROR, 1081,
|
||||
"Repeat count ''{0}'' must not be negative");
|
||||
"Repeat count ''{0}'' must not be negative"),
|
||||
|
||||
/** @since 6.1.15 */
|
||||
UNSUPPORTED_CHARACTER(Kind.ERROR, 1082,
|
||||
"Unsupported character ''{0}'' ({1}) encountered in expression");
|
||||
|
||||
|
||||
private final Kind kind;
|
||||
|
||||
+3
-2
@@ -226,8 +226,9 @@ public class FunctionReference extends SpelNodeImpl {
|
||||
ReflectionHelper.convertAllMethodHandleArguments(converter, functionArgs, methodHandle, varArgPosition);
|
||||
|
||||
if (isSuspectedVarargs) {
|
||||
if (declaredParamCount == 1) {
|
||||
// We only repackage the varargs if it is the ONLY argument -- for example,
|
||||
if (declaredParamCount == 1 && !methodHandle.isVarargsCollector()) {
|
||||
// We only repackage the arguments if the MethodHandle accepts a single
|
||||
// argument AND the MethodHandle is not a "varargs collector" -- for example,
|
||||
// when we are dealing with a bound MethodHandle.
|
||||
functionArgs = ReflectionHelper.setupArgumentsForVarargsInvocation(
|
||||
methodHandle.type().parameterArray(), functionArgs);
|
||||
|
||||
+1
-3
@@ -266,9 +266,7 @@ class Tokenizer {
|
||||
raiseParseException(this.pos, SpelMessage.UNEXPECTED_ESCAPE_CHAR);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException(
|
||||
"Unsupported character '%s' (%d) encountered at position %d in expression."
|
||||
.formatted(ch, (int) ch, (this.pos + 1)));
|
||||
raiseParseException(this.pos + 1, SpelMessage.UNSUPPORTED_CHARACTER, ch, (int) ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ public abstract class AbstractExpressionTests {
|
||||
protected static final boolean SHOULD_NOT_BE_WRITABLE = false;
|
||||
|
||||
|
||||
protected final ExpressionParser parser = new SpelExpressionParser();
|
||||
protected final SpelExpressionParser parser = new SpelExpressionParser();
|
||||
|
||||
protected final StandardEvaluationContext context = TestScenarioCreator.getTestEvaluationContext();
|
||||
|
||||
|
||||
+2
@@ -36,6 +36,8 @@ import static org.assertj.core.api.Assertions.assertThatException;
|
||||
* Tests invocation of constructors.
|
||||
*
|
||||
* @author Andy Clement
|
||||
* @see MethodInvocationTests
|
||||
* @see VariableAndFunctionTests
|
||||
*/
|
||||
class ConstructorInvocationTests extends AbstractExpressionTests {
|
||||
|
||||
|
||||
+2
@@ -46,6 +46,8 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
* @author Andy Clement
|
||||
* @author Phillip Webb
|
||||
* @author Sam Brannen
|
||||
* @see ConstructorInvocationTests
|
||||
* @see VariableAndFunctionTests
|
||||
*/
|
||||
class MethodInvocationTests extends AbstractExpressionTests {
|
||||
|
||||
|
||||
+11
-12
@@ -18,12 +18,12 @@ package org.springframework.expression.spel;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
import org.springframework.expression.spel.standard.SpelExpression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* Parse some expressions and check we get the AST we expect.
|
||||
@@ -34,10 +34,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
* @author Andy Clement
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
class ParsingTests {
|
||||
|
||||
private final SpelExpressionParser parser = new SpelExpressionParser();
|
||||
|
||||
class ParsingTests extends AbstractExpressionTests {
|
||||
|
||||
@Nested
|
||||
class Miscellaneous {
|
||||
@@ -104,12 +101,14 @@ class ParsingTests {
|
||||
parseCheck("have乐趣()");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unsupportedCharactersInIdentifiers() {
|
||||
// Invalid syntax
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> parser.parseRaw("apple~banana"))
|
||||
.withMessage("Unsupported character '~' (126) encountered at position 6 in expression.");
|
||||
@ParameterizedTest(name = "expression = ''{0}''")
|
||||
@CsvSource(textBlock = """
|
||||
apple~banana, ~, 6
|
||||
map[‘c], ‘, 5
|
||||
A § B, §, 3
|
||||
""")
|
||||
void unsupportedCharacter(String expression, char ch, int position) {
|
||||
parseAndCheckError(expression, SpelMessage.UNSUPPORTED_CHARACTER, position, ch, (int) ch);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+2
-1
@@ -597,7 +597,8 @@ class SpelDocumentationTests extends AbstractExpressionTests {
|
||||
MethodHandle methodHandle = MethodHandles.lookup().findVirtual(String.class, "formatted",
|
||||
MethodType.methodType(String.class, Object[].class))
|
||||
.bindTo(template)
|
||||
.bindTo(varargs); // here we have to provide arguments in a single array binding
|
||||
// Here we have to provide the arguments in a single array binding:
|
||||
.bindTo(varargs);
|
||||
context.registerFunction("message", methodHandle);
|
||||
|
||||
String message = parser.parseExpression("#message()").getValue(context, String.class);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user