mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d6f2c84c8 | |||
| 010e8a303b | |||
| 78549d4963 | |||
| e4e6910122 | |||
| 2270df515b | |||
| 70886e32c0 | |||
| e509385eae | |||
| b7aafda872 | |||
| a89a88d17c | |||
| c6b6ccdc89 | |||
| 09b8feadc2 | |||
| d03ea0bf19 | |||
| a5a1ef6b30 | |||
| 1b60b86bb2 | |||
| 89ce63f1f3 | |||
| 7536980be9 | |||
| c1229b03a7 | |||
| a4135bac5c | |||
| bf08e0c1b4 | |||
| 9202c0ad41 | |||
| 22b6d66a28 | |||
| 9376e6322d | |||
| 0eb937a866 | |||
| 6250b64766 | |||
| 5b1278d03c | |||
| 4f02be263f | |||
| 05d9b52b19 | |||
| 59a125d06f | |||
| aebc48ee8d | |||
| f51be0a17e | |||
| 1d2b5a15c3 | |||
| 4baad16437 | |||
| abcc1dfc6c | |||
| 47c5cd208c | |||
| 5a24e94d2e | |||
| f90bdbef42 | |||
| f17527a48b | |||
| 64b0283042 | |||
| 25cedcfb99 | |||
| 610626aec6 | |||
| d8afe7a8ad | |||
| 32c80d5ae6 | |||
| 0241a02e66 | |||
| cbda46984c | |||
| 7263771552 | |||
| fab3633c75 | |||
| 26706f0039 | |||
| 3cf2cd70ab | |||
| 0ac04a7f86 | |||
| 370a4ee8b3 | |||
| 55361fa4f6 | |||
| 546ca9b834 | |||
| 09b476ac18 | |||
| 387e34d881 | |||
| 4a10bc3288 | |||
| 580adfcbed | |||
| d151931f86 | |||
| 16d8d69cf9 | |||
| 95ac0eae4a | |||
| cc73ccefef | |||
| 4d34444a69 | |||
| d4ddbd537b | |||
| 0b9b9b4602 | |||
| 40596d444c | |||
| cb5b9dcaed | |||
| c99e7f8616 | |||
| 8a8c8fe00e | |||
| f45e7b9b9b | |||
| ec1f5ca600 | |||
| 62efdfb89c | |||
| c3da43291b | |||
| 5aa576f5c6 | |||
| 0268180799 | |||
| da4547a27e | |||
| 97eddb769a | |||
| 0e0397a385 | |||
| fec4f9b8cb | |||
| 4e20cdeb43 | |||
| 7f27ba3902 | |||
| b3a45670f9 | |||
| 8ec27438e7 | |||
| bf3278596c | |||
| 3971632415 | |||
| 1e54916119 | |||
| 6e20af41bf | |||
| c68c62b6c6 |
@@ -35,9 +35,7 @@ jobs:
|
||||
env:
|
||||
CI: 'true'
|
||||
GRADLE_ENTERPRISE_URL: 'https://ge.spring.io'
|
||||
GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }}
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }}
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
run: ./gradlew -PdeploymentRepository=$(pwd)/deployment-repository build publishAllPublicationsToDeploymentRepository
|
||||
- name: Deploy
|
||||
uses: spring-io/artifactory-deploy-action@v0.0.1
|
||||
|
||||
@@ -66,9 +66,7 @@ jobs:
|
||||
env:
|
||||
CI: 'true'
|
||||
GRADLE_ENTERPRISE_URL: 'https://ge.spring.io'
|
||||
GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }}
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }}
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
run: ./gradlew check antora
|
||||
- name: Send notification
|
||||
uses: ./.github/actions/send-notification
|
||||
|
||||
@@ -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.10-librca
|
||||
java=17.0.11-librca
|
||||
|
||||
@@ -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.15.0");
|
||||
checkstyle.setToolVersion("10.16.0");
|
||||
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
|
||||
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
|
||||
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
|
||||
|
||||
@@ -9,7 +9,7 @@ case "$1" in
|
||||
echo "https://github.com/bell-sw/Liberica/releases/download/21.0.2%2B14/bellsoft-jdk21.0.2+14-linux-amd64.tar.gz"
|
||||
;;
|
||||
java23)
|
||||
echo "https://download.java.net/java/early_access/jdk23/10/GPL/openjdk-23-ea+10_linux-x64_bin.tar.gz"
|
||||
echo "https://download.java.net/java/early_access/jdk23/17/GPL/openjdk-23-ea+17_linux-x64_bin.tar.gz"
|
||||
;;
|
||||
*)
|
||||
echo $"Unknown java version"
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ curl https://raw.githubusercontent.com/spring-io/concourse-java-scripts/v0.0.4/c
|
||||
|
||||
mkdir -p /opt/openjdk
|
||||
pushd /opt/openjdk > /dev/null
|
||||
for jdk in java17 java21 java22
|
||||
for jdk in java17 java21 java23
|
||||
do
|
||||
JDK_URL=$( /get-jdk-url.sh $jdk )
|
||||
mkdir $jdk
|
||||
|
||||
+1
-3
@@ -5,9 +5,7 @@ anchors:
|
||||
password: ((github-ci-release-token))
|
||||
branch: ((branch))
|
||||
gradle-enterprise-task-params: &gradle-enterprise-task-params
|
||||
GRADLE_ENTERPRISE_ACCESS_KEY: ((gradle_enterprise_secret_access_key))
|
||||
GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle_enterprise_cache_user.username))
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle_enterprise_cache_user.password))
|
||||
DEVELOCITY_ACCESS_KEY: ((gradle_enterprise_secret_access_key))
|
||||
sonatype-task-params: &sonatype-task-params
|
||||
SONATYPE_USERNAME: ((sonatype-username))
|
||||
SONATYPE_PASSWORD: ((sonatype-password))
|
||||
|
||||
@@ -19,6 +19,10 @@ you can do so. However, you should consider the following issues:
|
||||
since the CGLIB proxy instance is created through Objenesis. Only if your JVM does
|
||||
not allow for constructor bypassing, you might see double invocations and
|
||||
corresponding debug log entries from Spring's AOP support.
|
||||
* Your CGLIB proxy usage may face limitations with the JDK 9+ platform module system.
|
||||
As a typical case, you cannot create a CGLIB proxy for a class from the `java.lang`
|
||||
package when deploying on the module path. Such cases require a JVM bootstrap flag
|
||||
`--add-opens=java.base/java.lang=ALL-UNNAMED` which is not available for modules.
|
||||
|
||||
To force the use of CGLIB proxies, set the value of the `proxy-target-class` attribute
|
||||
of the `<aop:config>` element to true, as follows:
|
||||
|
||||
@@ -326,6 +326,19 @@ 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.custom-arguments]]
|
||||
=== Avoid Creating Bean 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:
|
||||
|
||||
. The custom arguments require dynamic introspection of a matching constructor or factory method.
|
||||
Those arguments cannot be detected by AOT, so the necessary reflection hints will have to be provided manually.
|
||||
. By-passing the instance supplier means that all other optimizations after creation are skipped as well.
|
||||
For instance, autowiring on fields and methods will be skipped as they are handled in the instance supplier.
|
||||
|
||||
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.factory-bean]]
|
||||
=== FactoryBean
|
||||
|
||||
|
||||
@@ -1,33 +1,16 @@
|
||||
[[beans-annotation-config]]
|
||||
= Annotation-based Container Configuration
|
||||
|
||||
.Are annotations better than XML for configuring Spring?
|
||||
****
|
||||
The introduction of annotation-based configuration raised the question of whether this
|
||||
approach is "`better`" than XML. The short answer is "`it depends.`" The long answer is
|
||||
that each approach has its pros and cons, and, usually, it is up to the developer to
|
||||
decide which strategy suits them better. Due to the way they are defined, annotations
|
||||
provide a lot of context in their declaration, leading to shorter and more concise
|
||||
configuration. However, XML excels at wiring up components without touching their source
|
||||
code or recompiling them. Some developers prefer having the wiring close to the source
|
||||
while others argue that annotated classes are no longer POJOs and, furthermore, that the
|
||||
configuration becomes decentralized and harder to control.
|
||||
Spring provides comprehensive support for annotation-based configuration, operating on
|
||||
metadata in the component class itself by using annotations on the relevant class,
|
||||
method, or field declaration. As mentioned in
|
||||
xref:core/beans/factory-extension.adoc#beans-factory-extension-bpp-examples-aabpp[Example: The `AutowiredAnnotationBeanPostProcessor`],
|
||||
Spring uses `BeanPostProcessors` in conjunction with annotations to make the core IOC
|
||||
container aware of specific annotations.
|
||||
|
||||
No matter the choice, Spring can accommodate both styles and even mix them together.
|
||||
It is worth pointing out that through its xref:core/beans/java.adoc[JavaConfig] option, Spring lets
|
||||
annotations be used in a non-invasive way, without touching the target components'
|
||||
source code and that, in terms of tooling, all configuration styles are supported by
|
||||
{spring-site-tools}[Spring Tools] for Eclipse, Visual Studio Code, and Theia.
|
||||
****
|
||||
|
||||
An alternative to XML setup is provided by annotation-based configuration, which relies
|
||||
on bytecode metadata for wiring up components instead of XML declarations. Instead of
|
||||
using XML to describe a bean wiring, the developer moves the configuration into the
|
||||
component class itself by using annotations on the relevant class, method, or field
|
||||
declaration. As mentioned in xref:core/beans/factory-extension.adoc#beans-factory-extension-bpp-examples-aabpp[Example: The `AutowiredAnnotationBeanPostProcessor`], using a
|
||||
`BeanPostProcessor` in conjunction with annotations is a common means of extending the
|
||||
Spring IoC container. For example, the xref:core/beans/annotation-config/autowired.adoc[`@Autowired`]
|
||||
annotation provides the same capabilities as described in xref:core/beans/dependencies/factory-autowire.adoc[Autowiring Collaborators] but
|
||||
For example, the xref:core/beans/annotation-config/autowired.adoc[`@Autowired`]
|
||||
annotation provides the same capabilities as described in
|
||||
xref:core/beans/dependencies/factory-autowire.adoc[Autowiring Collaborators] but
|
||||
with more fine-grained control and wider applicability. In addition, Spring provides
|
||||
support for JSR-250 annotations, such as `@PostConstruct` and `@PreDestroy`, as well as
|
||||
support for JSR-330 (Dependency Injection for Java) annotations contained in the
|
||||
@@ -36,13 +19,16 @@ can be found in the xref:core/beans/standard-annotations.adoc[relevant section].
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Annotation injection is performed before XML injection. Thus, the XML configuration
|
||||
overrides the annotations for properties wired through both approaches.
|
||||
Annotation injection is performed before external property injection. Thus, external
|
||||
configuration (e.g. XML-specified bean properties) effectively overrides the annotations
|
||||
for properties when wired through mixed approaches.
|
||||
====
|
||||
|
||||
As always, you can register the post-processors as individual bean definitions, but they
|
||||
can also be implicitly registered by including the following tag in an XML-based Spring
|
||||
configuration (notice the inclusion of the `context` namespace):
|
||||
Technically, you can register the post-processors as individual bean definitions, but they
|
||||
are implicitly registered in an `AnnotationConfigApplicationContext` already.
|
||||
|
||||
In an XML-based Spring setup, you may include the following configuration tag to enable
|
||||
mixing and matching with annotation-based configuration:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
|
||||
@@ -186,6 +186,15 @@ implementation type, consider declaring the most specific return type on your fa
|
||||
method (at least as specific as required by the injection points referring to your bean).
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
As of 4.3, `@Autowired` also considers self references for injection (that is, references
|
||||
back to the bean that is currently injected). Note that self injection is a fallback.
|
||||
In practice, you should use self references as a last resort only (for example, for
|
||||
calling other methods on the same instance through the bean's transactional proxy).
|
||||
Consider factoring out the affected methods to a separate delegate bean in such a scenario.
|
||||
====
|
||||
|
||||
You can also instruct Spring to provide all beans of a particular type from the
|
||||
`ApplicationContext` by adding the `@Autowired` annotation to a field or method that
|
||||
expects an array of that type, as the following example shows:
|
||||
@@ -268,6 +277,12 @@ use the same bean class). `@Order` values may influence priorities at injection
|
||||
but be aware that they do not influence singleton startup order, which is an
|
||||
orthogonal concern determined by dependency relationships and `@DependsOn` declarations.
|
||||
|
||||
Note that `@Order` annotations on configuration classes just influence the evaluation
|
||||
order within the overall set of configuration classes on startup. Such configuration-level
|
||||
order values do not affect the contained `@Bean` methods at all. For bean-level ordering,
|
||||
each `@Bean` method needs to have its own `@Order` annotation which applies within a
|
||||
set of multiple matches for the specific bean type (as returned by the factory method).
|
||||
|
||||
Note that the standard `jakarta.annotation.Priority` annotation is not available at the
|
||||
`@Bean` level, since it cannot be declared on methods. Its semantics can be modeled
|
||||
through `@Order` values in combination with `@Primary` on a single bean for each type.
|
||||
|
||||
@@ -2,36 +2,28 @@
|
||||
= Container Overview
|
||||
|
||||
The `org.springframework.context.ApplicationContext` interface represents the Spring IoC
|
||||
container and is responsible for instantiating, configuring, and assembling the
|
||||
beans. The container gets its instructions on what objects to
|
||||
instantiate, configure, and assemble by reading configuration metadata. The
|
||||
configuration metadata is represented in XML, Java annotations, or Java code. It lets
|
||||
you express the objects that compose your application and the rich interdependencies
|
||||
between those objects.
|
||||
container and is responsible for instantiating, configuring, and assembling the beans.
|
||||
The container gets its instructions on the components to instantiate, configure, and
|
||||
assemble by reading configuration metadata. The configuration metadata can be represented
|
||||
as annotated component classes, configuration classes with factory methods, or external
|
||||
XML files or Groovy scripts. With either format, you may compose your application and the
|
||||
rich interdependencies between those components.
|
||||
|
||||
Several implementations of the `ApplicationContext` interface are supplied
|
||||
with Spring. In stand-alone applications, it is common to create an
|
||||
instance of
|
||||
{spring-framework-api}/context/support/ClassPathXmlApplicationContext.html[`ClassPathXmlApplicationContext`]
|
||||
or {spring-framework-api}/context/support/FileSystemXmlApplicationContext.html[`FileSystemXmlApplicationContext`].
|
||||
While XML has been the traditional format for defining configuration metadata, you can
|
||||
instruct the container to use Java annotations or code as the metadata format by
|
||||
providing a small amount of XML configuration to declaratively enable support for these
|
||||
additional metadata formats.
|
||||
Several implementations of the `ApplicationContext` interface are part of core Spring.
|
||||
In stand-alone applications, it is common to create an instance of
|
||||
{spring-framework-api}/context/annotation/AnnotationConfigApplicationContext.html[`AnnotationConfigApplicationContext`]
|
||||
or {spring-framework-api}/context/support/ClassPathXmlApplicationContext.html[`ClassPathXmlApplicationContext`].
|
||||
|
||||
In most application scenarios, explicit user code is not required to instantiate one or
|
||||
more instances of a Spring IoC container. For example, in a web application scenario, a
|
||||
simple eight (or so) lines of boilerplate web descriptor XML in the `web.xml` file
|
||||
of the application typically suffices (see
|
||||
more instances of a Spring IoC container. For example, in a plain web application scenario,
|
||||
a simple boilerplate web descriptor XML in the `web.xml` file of the application suffices (see
|
||||
xref:core/beans/context-introduction.adoc#context-create[Convenient ApplicationContext Instantiation for Web Applications]).
|
||||
If you use the {spring-site-tools}[Spring Tools for Eclipse] (an Eclipse-powered
|
||||
development environment), you can easily create this boilerplate configuration with a
|
||||
few mouse clicks or keystrokes.
|
||||
In a Spring Boot scenario, the application context is implicitly bootstrapped for you
|
||||
based on common setup conventions.
|
||||
|
||||
The following diagram shows a high-level view of how Spring works. Your application classes
|
||||
are combined with configuration metadata so that, after the `ApplicationContext` is
|
||||
created and initialized, you have a fully configured and executable system or
|
||||
application.
|
||||
created and initialized, you have a fully configured and executable system or application.
|
||||
|
||||
.The Spring IoC container
|
||||
image::container-magic.png[]
|
||||
@@ -43,33 +35,25 @@ image::container-magic.png[]
|
||||
|
||||
As the preceding diagram shows, the Spring IoC container consumes a form of
|
||||
configuration metadata. This configuration metadata represents how you, as an
|
||||
application developer, tell the Spring container to instantiate, configure, and assemble
|
||||
the objects in your application.
|
||||
application developer, tell the Spring container to instantiate, configure,
|
||||
and assemble the components in your application.
|
||||
|
||||
Configuration metadata is traditionally supplied in a simple and intuitive XML format,
|
||||
which is what most of this chapter uses to convey key concepts and features of the
|
||||
Spring IoC container.
|
||||
|
||||
NOTE: XML-based metadata is not the only allowed form of configuration metadata.
|
||||
The Spring IoC container itself is totally decoupled from the format in which this
|
||||
configuration metadata is actually written. These days, many developers choose
|
||||
xref:core/beans/java.adoc[Java-based configuration] for their Spring applications.
|
||||
|
||||
For information about using other forms of metadata with the Spring container, see:
|
||||
xref:core/beans/java.adoc[Java-based configuration] for their Spring applications:
|
||||
|
||||
* xref:core/beans/annotation-config.adoc[Annotation-based configuration]: define beans using
|
||||
annotation-based configuration metadata.
|
||||
annotation-based configuration metadata on your application's component classes.
|
||||
* xref:core/beans/java.adoc[Java-based configuration]: define beans external to your application
|
||||
classes by using Java rather than XML files. To use these features, see the
|
||||
classes by using Java-based configuration classes. To use these features, see the
|
||||
{spring-framework-api}/context/annotation/Configuration.html[`@Configuration`],
|
||||
{spring-framework-api}/context/annotation/Bean.html[`@Bean`],
|
||||
{spring-framework-api}/context/annotation/Import.html[`@Import`],
|
||||
and {spring-framework-api}/context/annotation/DependsOn.html[`@DependsOn`] annotations.
|
||||
|
||||
Spring configuration consists of at least one and typically more than one bean
|
||||
definition that the container must manage. XML-based configuration metadata configures these
|
||||
beans as `<bean/>` elements inside a top-level `<beans/>` element. Java
|
||||
configuration typically uses `@Bean`-annotated methods within a `@Configuration` class.
|
||||
Spring configuration consists of at least one and typically more than one bean definition
|
||||
that the container must manage. Java configuration typically uses `@Bean`-annotated
|
||||
methods within a `@Configuration` class, each corresponding to one bean definition.
|
||||
|
||||
These bean definitions correspond to the actual objects that make up your application.
|
||||
Typically, you define service layer objects, persistence layer objects such as
|
||||
@@ -79,7 +63,14 @@ Typically, one does not configure fine-grained domain objects in the container,
|
||||
it is usually the responsibility of repositories and business logic to create and load
|
||||
domain objects.
|
||||
|
||||
The following example shows the basic structure of XML-based configuration metadata:
|
||||
|
||||
|
||||
[[beans-factory-xml]]
|
||||
=== XML as an External Configuration DSL
|
||||
|
||||
XML-based configuration metadata configures these beans as `<bean/>` elements inside
|
||||
a top-level `<beans/>` element. The following example shows the basic structure of
|
||||
XML-based configuration metadata:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@@ -110,14 +101,9 @@ The value of the `id` attribute can be used to refer to collaborating objects. T
|
||||
for referring to collaborating objects is not shown in this example. See
|
||||
xref:core/beans/dependencies.adoc[Dependencies] for more information.
|
||||
|
||||
|
||||
|
||||
[[beans-factory-instantiation]]
|
||||
== Instantiating a Container
|
||||
|
||||
The location path or paths
|
||||
supplied to an `ApplicationContext` constructor are resource strings that let
|
||||
the container load configuration metadata from a variety of external resources, such
|
||||
For instantiating a container, the location path or paths to the XML resource files
|
||||
need to be supplied to a `ClassPathXmlApplicationContext` constructor that let the
|
||||
container load configuration metadata from a variety of external resources, such
|
||||
as the local file system, the Java `CLASSPATH`, and so on.
|
||||
|
||||
[tabs]
|
||||
@@ -209,9 +195,9 @@ xref:core/beans/dependencies.adoc[Dependencies].
|
||||
It can be useful to have bean definitions span multiple XML files. Often, each individual
|
||||
XML configuration file represents a logical layer or module in your architecture.
|
||||
|
||||
You can use the application context constructor to load bean definitions from all these
|
||||
You can use the `ClassPathXmlApplicationContext` constructor to load bean definitions from
|
||||
XML fragments. This constructor takes multiple `Resource` locations, as was shown in the
|
||||
xref:core/beans/basics.adoc#beans-factory-instantiation[previous section]. Alternatively,
|
||||
xref:core/beans/basics.adoc#beans-factory-xml[previous section]. Alternatively,
|
||||
use one or more occurrences of the `<import/>` element to load bean definitions from
|
||||
another file or files. The following example shows how to do so:
|
||||
|
||||
@@ -259,7 +245,7 @@ configuration features beyond plain bean definitions are available in a selectio
|
||||
of XML namespaces provided by Spring -- for example, the `context` and `util` namespaces.
|
||||
|
||||
|
||||
[[groovy-bean-definition-dsl]]
|
||||
[[beans-factory-groovy]]
|
||||
=== The Groovy Bean Definition DSL
|
||||
|
||||
As a further example for externalized configuration metadata, bean definitions can also
|
||||
@@ -420,4 +406,3 @@ a dependency on a specific bean through metadata (such as an autowiring annotati
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -234,6 +234,10 @@ For details about the mechanism for supplying arguments to the constructor (if r
|
||||
and setting object instance properties after the object is constructed, see
|
||||
xref:core/beans/dependencies/factory-collaborators.adoc[Injecting Dependencies].
|
||||
|
||||
NOTE: In the case of constructor arguments, the container can select a corresponding
|
||||
constructor among several overloaded constructors. That said, to avoid ambiguities,
|
||||
it is recommended to keep your constructor signatures as straightforward as possible.
|
||||
|
||||
|
||||
[[beans-factory-class-static-factory-method]]
|
||||
=== Instantiation with a Static Factory Method
|
||||
@@ -294,6 +298,24 @@ For details about the mechanism for supplying (optional) arguments to the factor
|
||||
and setting object instance properties after the object is returned from the factory,
|
||||
see xref:core/beans/dependencies/factory-properties-detailed.adoc[Dependencies and Configuration in Detail].
|
||||
|
||||
NOTE: In the case of factory method arguments, the container can select a corresponding
|
||||
method among several overloaded methods of the same name. That said, to avoid ambiguities,
|
||||
it is recommended to keep your factory method signatures as straightforward as possible.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
A typical problematic case with factory method overloading is Mockito with its many
|
||||
overloads of the `mock` method. Choose the most specific variant of `mock` possible:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
<bean id="clientService" class="org.mockito.Mockito" factory-method="mock">
|
||||
<constructor-arg type="java.lang.Class" value="examples.ClientService"/>
|
||||
<constructor-arg type="java.lang.String" value="clientService"/>
|
||||
</bean>
|
||||
----
|
||||
====
|
||||
|
||||
|
||||
[[beans-factory-class-instance-factory-method]]
|
||||
=== Instantiation by Using an Instance Factory Method
|
||||
@@ -416,8 +438,8 @@ Kotlin::
|
||||
======
|
||||
|
||||
This approach shows that the factory bean itself can be managed and configured through
|
||||
dependency injection (DI). See xref:core/beans/dependencies/factory-properties-detailed.adoc[Dependencies and Configuration in Detail]
|
||||
.
|
||||
dependency injection (DI).
|
||||
See xref:core/beans/dependencies/factory-properties-detailed.adoc[Dependencies and Configuration in Detail].
|
||||
|
||||
NOTE: In Spring documentation, "factory bean" refers to a bean that is configured in the
|
||||
Spring container and that creates objects through an
|
||||
@@ -444,5 +466,3 @@ cases into account and returns the type of object that a `BeanFactory.getBean` c
|
||||
going to return for the same bean name.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -55,33 +55,34 @@ The preceding `AppConfig` class is equivalent to the following Spring `<beans/>`
|
||||
</beans>
|
||||
----
|
||||
|
||||
.Full @Configuration vs "`lite`" @Bean mode?
|
||||
.@Configuration classes with or without local calls between @Bean methods?
|
||||
****
|
||||
When `@Bean` methods are declared within classes that are not annotated with
|
||||
`@Configuration`, they are referred to as being processed in a "`lite`" mode. Bean methods
|
||||
declared on a bean that is not annotated with `@Configuration` are considered to be "`lite`",
|
||||
with a different primary purpose of the containing class and a `@Bean` method
|
||||
being a sort of bonus there. For example, service components may expose management views
|
||||
to the container through an additional `@Bean` method on each applicable component class.
|
||||
In such scenarios, `@Bean` methods are a general-purpose factory method mechanism.
|
||||
|
||||
Unlike full `@Configuration`, lite `@Bean` methods cannot declare inter-bean dependencies.
|
||||
Instead, they operate on their containing component's internal state and, optionally, on
|
||||
arguments that they may declare. Such a `@Bean` method should therefore not invoke other
|
||||
`@Bean` methods. Each such method is literally only a factory method for a particular
|
||||
bean reference, without any special runtime semantics. The positive side-effect here is
|
||||
that no CGLIB subclassing has to be applied at runtime, so there are no limitations in
|
||||
terms of class design (that is, the containing class may be `final` and so forth).
|
||||
|
||||
In common scenarios, `@Bean` methods are to be declared within `@Configuration` classes,
|
||||
ensuring that "`full`" mode is always used and that cross-method references therefore
|
||||
get redirected to the container's lifecycle management. This prevents the same
|
||||
`@Bean` method from accidentally being invoked through a regular Java call, which helps
|
||||
to reduce subtle bugs that can be hard to track down when operating in "`lite`" mode.
|
||||
ensuring that full configuration class processing applies and that cross-method
|
||||
references therefore get redirected to the container's lifecycle management.
|
||||
This prevents the same `@Bean` method from accidentally being invoked through a regular
|
||||
Java method call, which helps to reduce subtle bugs that can be hard to track down.
|
||||
|
||||
When `@Bean` methods are declared within classes that are not annotated with
|
||||
`@Configuration` - or when `@Configuration(proxyBeanMethods=false)` is declared -,
|
||||
they are referred to as being processed in a "lite" mode. In such scenarios,
|
||||
`@Bean` methods are effectively a general-purpose factory method mechanism without
|
||||
special runtime processing (that is, without generating a CGLIB subclass for it).
|
||||
A custom Java call to such a method will not get intercepted by the container and
|
||||
therefore behaves just like a regular method call, creating a new instance every time
|
||||
rather than reusing an existing singleton (or scoped) instance for the given bean.
|
||||
|
||||
As a consequence, `@Bean` methods on classes without runtime proxying are not meant to
|
||||
declare inter-bean dependencies at all. Instead, they are expected to operate on their
|
||||
containing component's fields and, optionally, on arguments that a factory method may
|
||||
declare in order to receive autowired collaborators. Such a `@Bean` method therefore
|
||||
never needs to invoke other `@Bean` methods; every such call can be expressed through
|
||||
a factory method argument instead. The positive side-effect here is that no CGLIB
|
||||
subclassing has to be applied at runtime, reducing the overhead and the footprint.
|
||||
****
|
||||
|
||||
The `@Bean` and `@Configuration` annotations are discussed in depth in the following sections.
|
||||
First, however, we cover the various ways of creating a spring container by using
|
||||
First, however, we cover the various ways of creating a Spring container by using
|
||||
Java-based configuration.
|
||||
|
||||
|
||||
|
||||
+9
-7
@@ -74,10 +74,11 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
Used at the class level as above, the annotation indicates a default for all methods of
|
||||
the declaring class (as well as its subclasses). Alternatively, each method can be
|
||||
annotated individually. See xref:data-access/transaction/declarative/annotations.adoc#transaction-declarative-annotations-method-visibility[method visibility] for
|
||||
further details on which methods Spring considers transactional. Note that a class-level
|
||||
Used at the class level as above, the annotation indicates a default for all methods
|
||||
of the declaring class (as well as its subclasses). Alternatively, each method can be
|
||||
annotated individually. See
|
||||
xref:data-access/transaction/declarative/annotations.adoc#transaction-declarative-annotations-method-visibility[method visibility]
|
||||
for further details on which methods Spring considers transactional. Note that a class-level
|
||||
annotation does not apply to ancestor classes up the class hierarchy; in such a scenario,
|
||||
inherited methods need to be locally redeclared in order to participate in a
|
||||
subclass-level annotation.
|
||||
@@ -436,9 +437,10 @@ properties of the `@Transactional` annotation:
|
||||
| Optional array of exception name patterns that must not cause rollback.
|
||||
|===
|
||||
|
||||
TIP: See xref:data-access/transaction/declarative/rolling-back.adoc#transaction-declarative-rollback-rules[Rollback rules] for further details
|
||||
on rollback rule semantics, patterns, and warnings regarding possible unintentional
|
||||
matches for pattern-based rollback rules.
|
||||
TIP: See
|
||||
xref:data-access/transaction/declarative/rolling-back.adoc#transaction-declarative-rollback-rules[Rollback rules]
|
||||
for further details on rollback rule semantics, patterns, and warnings
|
||||
regarding possible unintentional matches for pattern-based rollback rules.
|
||||
|
||||
Currently, you cannot have explicit control over the name of a transaction, where 'name'
|
||||
means the transaction name that appears in a transaction monitor and in logging output.
|
||||
|
||||
+27
-1
@@ -24,8 +24,8 @@ Vavr's `Try` method to trigger transaction rollbacks when it returns a 'Failure'
|
||||
This allows you to handle functional-style errors using Try and have the transaction
|
||||
automatically rolled back in case of a failure. For more information on Vavr's Try,
|
||||
refer to the https://docs.vavr.io/#_try[official Vavr documentation].
|
||||
|
||||
Here's an example of how to use Vavr's Try with a transactional method:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
@@ -42,6 +42,32 @@ Java::
|
||||
----
|
||||
======
|
||||
|
||||
As of Spring Framework 6.1, there is also special treatment of `CompletableFuture`
|
||||
(and general `Future`) return values, triggering a rollback for such a handle if it
|
||||
was exceptionally completed at the time of being returned from the original method.
|
||||
This is intended for `@Async` methods where the actual method implementation may
|
||||
need to comply with a `CompletableFuture` signature (auto-adapted to an actual
|
||||
asynchronous handle for a call to the proxy by `@Async` processing at runtime),
|
||||
preferring exposure in the returned handle rather than rethrowing an exception:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@Transactional @Async
|
||||
public CompletableFuture<String> myTransactionalMethod() {
|
||||
try {
|
||||
return CompletableFuture.completedFuture(delegate.myDataAccessOperation());
|
||||
}
|
||||
catch (DataAccessException ex) {
|
||||
return CompletableFuture.failedFuture(ex);
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
Checked exceptions that are thrown from a transactional method do not result in a rollback
|
||||
in the default configuration. You can configure exactly which `Exception` types mark a
|
||||
transaction for rollback, including checked exceptions by specifying _rollback rules_.
|
||||
|
||||
@@ -71,8 +71,34 @@ This can be done with `method(HttpMethod)` or with the convenience methods `get(
|
||||
Next, the request URI can be specified with the `uri` methods.
|
||||
This step is optional and can be skipped if the `RestClient` is configured with a default URI.
|
||||
The URL is typically specified as a `String`, with optional URI template variables.
|
||||
String URLs are encoded by default, but this can be changed by building a client with a custom `uriBuilderFactory`.
|
||||
The following example configures a GET request to `https://example.com/orders/42`:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
int id = 42;
|
||||
restClient.get()
|
||||
.uri("https://example.com/orders/{id}", id)
|
||||
....
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
val id = 42
|
||||
restClient.get()
|
||||
.uri("https://example.com/orders/{id}", id)
|
||||
...
|
||||
----
|
||||
======
|
||||
|
||||
A function can also be used for more controls, such as specifying xref:web/webmvc/mvc-uri-building.adoc[request parameters].
|
||||
|
||||
String URLs are encoded by default, but this can be changed by building a client with a custom `uriBuilderFactory`.
|
||||
The URL can also be provided with a function or as a `java.net.URI`, both of which are not encoded.
|
||||
For more details on working with and encoding URIs, see xref:web/webmvc/mvc-uri-building.adoc[URI Links].
|
||||
|
||||
@@ -468,6 +494,9 @@ If no request factory is specified when the `RestClient` was built, it will use
|
||||
Otherwise, if the `java.net.http` module is loaded, it will use Java's `HttpClient`.
|
||||
Finally, it will resort to the simple default.
|
||||
|
||||
TIP: Note that the `SimpleClientHttpRequestFactory` may raise an exception when accessing the status of a response that represents an error (e.g. 401).
|
||||
If this is an issue, use any of the alternative request factories.
|
||||
|
||||
[[rest-webclient]]
|
||||
== `WebClient`
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ existing Java application.
|
||||
|
||||
The Spring Framework provides a dedicated `ApplicationContext` that supports a Groovy-based
|
||||
Bean Definition DSL. For more details, see
|
||||
xref:core/beans/basics.adoc#groovy-bean-definition-dsl[The Groovy Bean Definition DSL].
|
||||
xref:core/beans/basics.adoc#beans-factory-groovy[The Groovy Bean Definition DSL].
|
||||
|
||||
Further support for Groovy, including beans written in Groovy, refreshable script beans,
|
||||
and more is available in xref:languages/dynamic.adoc[Dynamic Language Support].
|
||||
|
||||
@@ -166,7 +166,7 @@ public class SampleConfiguration {
|
||||
@Bean
|
||||
@NotNull
|
||||
public SampleBean sampleBean$demo_kotlin_internal_test() {
|
||||
return new SampleBean();
|
||||
return new SampleBean();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
+2
-1
@@ -111,7 +111,8 @@ a mock service with Mockito:
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
<bean id="accountService" class="org.mockito.Mockito" factory-method="mock">
|
||||
<constructor-arg value="org.example.AccountService"/>
|
||||
<constructor-arg type="java.lang.Class" value="org.example.AccountService"/>
|
||||
<constructor-arg type="java.lang.String" value="accountService"/>
|
||||
</bean>
|
||||
----
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[[spring-mvc-test-vs-end-to-end-integration-tests]]
|
||||
= MockMvc vs End-to-End Tests
|
||||
|
||||
MockMVc is built on Servlet API mock implementations from the
|
||||
MockMvc is built on Servlet API mock implementations from the
|
||||
`spring-test` module and does not rely on a running container. Therefore, there are
|
||||
some differences when compared to full end-to-end integration tests with an actual
|
||||
client and a live server running.
|
||||
|
||||
+10
-32
@@ -1,38 +1,16 @@
|
||||
[[spring-mvc-test-vs-streaming-response]]
|
||||
= Streaming Responses
|
||||
|
||||
The best way to test streaming responses such as Server-Sent Events is through the
|
||||
<<WebTestClient>> which can be used as a test client to connect to a `MockMvc` instance
|
||||
to perform tests on Spring MVC controllers without a running server. For example:
|
||||
You can use `WebTestClient` to test xref:testing/webtestclient.adoc#webtestclient-stream[streaming responses]
|
||||
such as Server-Sent Events. However, `MockMvcWebTestClient` doesn't support infinite
|
||||
streams because there is no way to cancel the server stream from the client side.
|
||||
To test infinite streams, you'll need to
|
||||
xref:testing/webtestclient.adoc#webtestclient-server-config[bind to] a running server,
|
||||
or when using Spring Boot,
|
||||
{spring-boot-docs}/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-running-server[test with a running server].
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
WebTestClient client = MockMvcWebTestClient.bindToController(new SseController()).build();
|
||||
|
||||
FluxExchangeResult<Person> exchangeResult = client.get()
|
||||
.uri("/persons")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().contentType("text/event-stream")
|
||||
.returnResult(Person.class);
|
||||
|
||||
// Use StepVerifier from Project Reactor to test the streaming response
|
||||
|
||||
StepVerifier.create(exchangeResult.getResponseBody())
|
||||
.expectNext(new Person("N0"), new Person("N1"), new Person("N2"))
|
||||
.expectNextCount(4)
|
||||
.consumeNextWith(person -> assertThat(person.getName()).endsWith("7"))
|
||||
.thenCancel()
|
||||
.verify();
|
||||
----
|
||||
======
|
||||
|
||||
`WebTestClient` can also connect to a live server and perform full end-to-end integration
|
||||
tests. This is also supported in Spring Boot where you can
|
||||
{spring-boot-docs}/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-running-server[test a running server].
|
||||
`MockMvcWebTestClient` does support asynchronous responses, and even streaming responses.
|
||||
The limitation is that it can't influence the server to stop, and therefore the server
|
||||
must finish writing the response on its own.
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
= Context Configuration with Groovy Scripts
|
||||
|
||||
To load an `ApplicationContext` for your tests by using Groovy scripts that use the
|
||||
xref:core/beans/basics.adoc#groovy-bean-definition-dsl[Groovy Bean Definition DSL], you can annotate
|
||||
xref:core/beans/basics.adoc#beans-factory-groovy[Groovy Bean Definition DSL], you can annotate
|
||||
your test class with `@ContextConfiguration` and configure the `locations` or `value`
|
||||
attribute with an array that contains the resource locations of Groovy scripts. Resource
|
||||
lookup semantics for Groovy scripts are the same as those described for
|
||||
|
||||
@@ -672,13 +672,13 @@ Kotlin::
|
||||
val result = client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(Person.class)
|
||||
.returnResult();
|
||||
.expectBody<Person>()
|
||||
.returnResult()
|
||||
|
||||
// For a response without a body
|
||||
val result = client.get().uri("/path")
|
||||
.exchange()
|
||||
.expectBody().isEmpty();
|
||||
.expectBody().isEmpty()
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
@@ -4,10 +4,13 @@
|
||||
`spring-webflux` depends on `reactor-core` and uses it internally to compose asynchronous
|
||||
logic and to provide Reactive Streams support. Generally, WebFlux APIs return `Flux` or
|
||||
`Mono` (since those are used internally) and leniently accept any Reactive Streams
|
||||
`Publisher` implementation as input. The use of `Flux` versus `Mono` is important, because
|
||||
it helps to express cardinality -- for example, whether a single or multiple asynchronous
|
||||
values are expected, and that can be essential for making decisions (for example, when
|
||||
encoding or decoding HTTP messages).
|
||||
`Publisher` implementation as input.
|
||||
When a `Publisher` is provided, it can be treated only as a stream with unknown semantics (0..N).
|
||||
If, however, the semantics are known, you should wrap it with `Flux` or `Mono.from(Publisher)` instead
|
||||
of passing the raw `Publisher`.
|
||||
The use of `Flux` versus `Mono` is important, because it helps to express cardinality --
|
||||
for example, whether a single or multiple asynchronous values are expected,
|
||||
and that can be essential for making decisions (for example, when encoding or decoding HTTP messages).
|
||||
|
||||
For annotated controllers, WebFlux transparently adapts to the reactive library chosen by
|
||||
the application. This is done with the help of the
|
||||
@@ -15,15 +18,3 @@ the application. This is done with the help of the
|
||||
provides pluggable support for reactive library and other asynchronous types. The registry
|
||||
has built-in support for RxJava 3, Kotlin coroutines and SmallRye Mutiny, but you can
|
||||
register others, too.
|
||||
|
||||
For functional APIs (such as <<webflux-fn>>, the `WebClient`, and others), the general rules
|
||||
for WebFlux APIs apply -- `Flux` and `Mono` as return values and a Reactive Streams
|
||||
`Publisher` as input. When a `Publisher`, whether custom or from another reactive library,
|
||||
is provided, it can be treated only as a stream with unknown semantics (0..N). If, however,
|
||||
the semantics are known, you can wrap it with `Flux` or `Mono.from(Publisher)` instead
|
||||
of passing the raw `Publisher`.
|
||||
|
||||
For example, given a `Publisher` that is not a `Mono`, the Jackson JSON message writer
|
||||
expects multiple values. If the media type implies an infinite stream (for example,
|
||||
`application/json+stream`), values are written and flushed individually. Otherwise,
|
||||
values are buffered into a list and rendered as a JSON array.
|
||||
@@ -5,14 +5,14 @@
|
||||
|
||||
A common requirement for REST services is to include details in the body of error
|
||||
responses. The Spring Framework supports the "Problem Details for HTTP APIs"
|
||||
specification, {rfc-site}/rfc7807.html[RFC 7807].
|
||||
specification, {rfc-site}/rfc9457.html[RFC 9457].
|
||||
|
||||
The following are the main abstractions for this support:
|
||||
|
||||
- `ProblemDetail` -- representation for an RFC 7807 problem detail; a simple container
|
||||
- `ProblemDetail` -- representation for an RFC 9457 problem detail; a simple container
|
||||
for both standard fields defined in the spec, and for non-standard ones.
|
||||
- `ErrorResponse` -- contract to expose HTTP error response details including HTTP
|
||||
status, response headers, and a body in the format of RFC 7807; this allows exceptions to
|
||||
status, response headers, and a body in the format of RFC 9457; this allows exceptions to
|
||||
encapsulate and expose the details of how they map to an HTTP response. All Spring WebFlux
|
||||
exceptions implement this.
|
||||
- `ErrorResponseException` -- basic `ErrorResponse` implementation that others
|
||||
@@ -28,7 +28,7 @@ and any `ErrorResponseException`, and renders an error response with a body.
|
||||
[.small]#xref:web/webmvc/mvc-ann-rest-exceptions.adoc#mvc-ann-rest-exceptions-render[See equivalent in the Servlet stack]#
|
||||
|
||||
You can return `ProblemDetail` or `ErrorResponse` from any `@ExceptionHandler` or from
|
||||
any `@RequestMapping` method to render an RFC 7807 response. This is processed as follows:
|
||||
any `@RequestMapping` method to render an RFC 9457 response. This is processed as follows:
|
||||
|
||||
- The `status` property of `ProblemDetail` determines the HTTP status.
|
||||
- The `instance` property of `ProblemDetail` is set from the current URL path, if not
|
||||
@@ -37,7 +37,7 @@ already set.
|
||||
"application/problem+json" over "application/json" when rendering a `ProblemDetail`,
|
||||
and also falls back on it if no compatible media type is found.
|
||||
|
||||
To enable RFC 7807 responses for Spring WebFlux exceptions and for any
|
||||
To enable RFC 9457 responses for Spring WebFlux exceptions and for any
|
||||
`ErrorResponseException`, extend `ResponseEntityExceptionHandler` and declare it as an
|
||||
xref:web/webflux/controller/ann-advice.adoc[@ControllerAdvice] in Spring configuration. The handler
|
||||
has an `@ExceptionHandler` method that handles any `ErrorResponse` exception, which
|
||||
@@ -50,7 +50,7 @@ use a protected method to map any exception to a `ProblemDetail`.
|
||||
== Non-Standard Fields
|
||||
[.small]#xref:web/webmvc/mvc-ann-rest-exceptions.adoc#mvc-ann-rest-exceptions-non-standard[See equivalent in the Servlet stack]#
|
||||
|
||||
You can extend an RFC 7807 response with non-standard fields in one of two ways.
|
||||
You can extend an RFC 9457 response with non-standard fields in one of two ways.
|
||||
|
||||
One, insert into the "properties" `Map` of `ProblemDetail`. When using the Jackson
|
||||
library, the Spring Framework registers `ProblemDetailJacksonMixin` that ensures this
|
||||
|
||||
+14
-3
@@ -4,9 +4,20 @@
|
||||
[.small]#xref:web/webmvc/mvc-controller/ann-methods/return-types.adoc[See equivalent in the Servlet stack]#
|
||||
|
||||
The following table shows the supported controller method return values. Note that reactive
|
||||
types from libraries such as Reactor, RxJava, xref:web-reactive.adoc#webflux-reactive-libraries[or other] are
|
||||
types from libraries such as Reactor, RxJava, xref:web/webflux-reactive-libraries.adoc[or other] are
|
||||
generally supported for all return values.
|
||||
|
||||
For return types like `Flux`, when multiple values are expected, elements are streamed as they come
|
||||
and are not buffered. This is the default behavior, as keeping a potentially large amount of elements in memory
|
||||
is not efficient. If the media type implies an infinite stream (for example,
|
||||
`application/json+stream`), values are written and flushed individually. Otherwise,
|
||||
values are written individually and the flushing happens separately.
|
||||
|
||||
NOTE: If an error happens while an element is encoded to JSON, the response might have been written to and committed already
|
||||
and it is impossible at that point to render a proper error response.
|
||||
In some cases, applications can choose to trade memory efficiency for better handling such errors by buffering elements and encoding them all at once.
|
||||
Controllers can then return a `Flux<List<B>>`; Reactor provides a dedicated operator for that, `Flux#collectList()`.
|
||||
|
||||
[cols="1,2", options="header"]
|
||||
|===
|
||||
| Controller method return value | Description
|
||||
@@ -24,11 +35,11 @@ generally supported for all return values.
|
||||
| For returning a response with headers and no body.
|
||||
|
||||
| `ErrorResponse`
|
||||
| To render an RFC 7807 error response with details in the body,
|
||||
| To render an RFC 9457 error response with details in the body,
|
||||
see xref:web/webflux/ann-rest-exceptions.adoc[Error Responses]
|
||||
|
||||
| `ProblemDetail`
|
||||
| To render an RFC 7807 error response with details in the body,
|
||||
| To render an RFC 9457 error response with details in the body,
|
||||
see xref:web/webflux/ann-rest-exceptions.adoc[Error Responses]
|
||||
|
||||
| `String`
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
|
||||
A common requirement for REST services is to include details in the body of error
|
||||
responses. The Spring Framework supports the "Problem Details for HTTP APIs"
|
||||
specification, {rfc-site}/rfc7807[RFC 7807].
|
||||
specification, {rfc-site}/rfc9457[RFC 9457].
|
||||
|
||||
The following are the main abstractions for this support:
|
||||
|
||||
- `ProblemDetail` -- representation for an RFC 7807 problem detail; a simple container
|
||||
- `ProblemDetail` -- representation for an RFC 9457 problem detail; a simple container
|
||||
for both standard fields defined in the spec, and for non-standard ones.
|
||||
- `ErrorResponse` -- contract to expose HTTP error response details including HTTP
|
||||
status, response headers, and a body in the format of RFC 7807; this allows exceptions to
|
||||
status, response headers, and a body in the format of RFC 9457; this allows exceptions to
|
||||
encapsulate and expose the details of how they map to an HTTP response. All Spring MVC
|
||||
exceptions implement this.
|
||||
- `ErrorResponseException` -- basic `ErrorResponse` implementation that others
|
||||
@@ -28,7 +28,7 @@ and any `ErrorResponseException`, and renders an error response with a body.
|
||||
[.small]#xref:web/webflux/ann-rest-exceptions.adoc#webflux-ann-rest-exceptions-render[See equivalent in the Reactive stack]#
|
||||
|
||||
You can return `ProblemDetail` or `ErrorResponse` from any `@ExceptionHandler` or from
|
||||
any `@RequestMapping` method to render an RFC 7807 response. This is processed as follows:
|
||||
any `@RequestMapping` method to render an RFC 9457 response. This is processed as follows:
|
||||
|
||||
- The `status` property of `ProblemDetail` determines the HTTP status.
|
||||
- The `instance` property of `ProblemDetail` is set from the current URL path, if not
|
||||
@@ -37,7 +37,7 @@ already set.
|
||||
"application/problem+json" over "application/json" when rendering a `ProblemDetail`,
|
||||
and also falls back on it if no compatible media type is found.
|
||||
|
||||
To enable RFC 7807 responses for Spring WebFlux exceptions and for any
|
||||
To enable RFC 9457 responses for Spring WebFlux exceptions and for any
|
||||
`ErrorResponseException`, extend `ResponseEntityExceptionHandler` and declare it as an
|
||||
xref:web/webmvc/mvc-controller/ann-advice.adoc[@ControllerAdvice] in Spring configuration. The handler
|
||||
has an `@ExceptionHandler` method that handles any `ErrorResponse` exception, which
|
||||
@@ -50,7 +50,7 @@ use a protected method to map any exception to a `ProblemDetail`.
|
||||
== Non-Standard Fields
|
||||
[.small]#xref:web/webflux/ann-rest-exceptions.adoc#webflux-ann-rest-exceptions-non-standard[See equivalent in the Reactive stack]#
|
||||
|
||||
You can extend an RFC 7807 response with non-standard fields in one of two ways.
|
||||
You can extend an RFC 9457 response with non-standard fields in one of two ways.
|
||||
|
||||
One, insert into the "properties" `Map` of `ProblemDetail`. When using the Jackson
|
||||
library, the Spring Framework registers `ProblemDetailJacksonMixin` that ensures this
|
||||
|
||||
@@ -52,14 +52,10 @@ The following example shows how to achieve the same configuration in XML:
|
||||
</mvc:interceptors>
|
||||
----
|
||||
|
||||
NOTE: Interceptors are not ideally suited as a security layer due to the potential
|
||||
for a mismatch with annotated controller path matching, which can also match trailing
|
||||
slashes and path extensions transparently, along with other path matching options. Many
|
||||
of these options have been deprecated but the potential for a mismatch remains.
|
||||
Generally, we recommend using Spring Security which includes a dedicated
|
||||
https://docs.spring.io/spring-security/reference/servlet/integrations/mvc.html#mvc-requestmatcher[MvcRequestMatcher]
|
||||
to align with Spring MVC path matching and also has a security firewall that blocks many
|
||||
unwanted characters in URL paths.
|
||||
WARNING: Interceptors are not ideally suited as a security layer due to the potential for
|
||||
a mismatch with annotated controller path matching. Generally, we recommend using Spring
|
||||
Security, or alternatively a similar approach integrated with the Servlet filter chain,
|
||||
and applied as early as possible.
|
||||
|
||||
NOTE: The XML config declares interceptors as `MappedInterceptor` beans, and those are in
|
||||
turn detected by any `HandlerMapping` bean, including those from other frameworks.
|
||||
|
||||
+2
-2
@@ -228,11 +228,11 @@ level, xref:web/webmvc/mvc-servlet/exceptionhandlers.adoc[HandlerExceptionResolv
|
||||
See xref:web/webmvc/mvc-controller/ann-methods/responseentity.adoc[ResponseEntity].
|
||||
|
||||
| `ErrorResponse`
|
||||
| To render an RFC 7807 error response with details in the body,
|
||||
| To render an RFC 9457 error response with details in the body,
|
||||
see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses]
|
||||
|
||||
| `ProblemDetail`
|
||||
| To render an RFC 7807 error response with details in the body,
|
||||
| To render an RFC 9457 error response with details in the body,
|
||||
see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses]
|
||||
|
||||
| `String`
|
||||
|
||||
+7
@@ -37,6 +37,13 @@ Kotlin::
|
||||
all controller methods. This is the effect of `@RestController`, which is nothing more
|
||||
than a meta-annotation marked with `@Controller` and `@ResponseBody`.
|
||||
|
||||
A `Resource` object can be returned for file content, copying the `InputStream`
|
||||
content of the provided resource to the response `OutputStream`. Note that the
|
||||
`InputStream` should be lazily retrieved by the `Resource` handle in order to reliably
|
||||
close it after it has been copied to the response. If you are using `InputStreamResource`
|
||||
for such a purpose, make sure to construct it with an on-demand `InputStreamSource`
|
||||
(e.g. through a lambda expression that retrieves the actual `InputStream`).
|
||||
|
||||
You can use `@ResponseBody` with reactive types.
|
||||
See xref:web/webmvc/mvc-ann-async.adoc[Asynchronous Requests] and xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-reactive-types[Reactive Types] for more details.
|
||||
|
||||
|
||||
+10
@@ -32,6 +32,16 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
The body will usually be provided as a value object to be rendered to a corresponding
|
||||
response representation (e.g. JSON) by one of the registered `HttpMessageConverters`.
|
||||
|
||||
A `ResponseEntity<Resource>` can be returned for file content, copying the `InputStream`
|
||||
content of the provided resource to the response `OutputStream`. Note that the
|
||||
`InputStream` should be lazily retrieved by the `Resource` handle in order to reliably
|
||||
close it after it has been copied to the response. If you are using `InputStreamResource`
|
||||
for such a purpose, make sure to construct it with an on-demand `InputStreamSource`
|
||||
(e.g. through a lambda expression that retrieves the actual `InputStream`).
|
||||
|
||||
Spring MVC supports using a single value xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-reactive-types[reactive type]
|
||||
to produce the `ResponseEntity` asynchronously, and/or single and multi-value reactive
|
||||
types for the body. This allows the following types of async responses:
|
||||
|
||||
+2
-2
@@ -23,11 +23,11 @@ supported for all return values.
|
||||
| For returning a response with headers and no body.
|
||||
|
||||
| `ErrorResponse`
|
||||
| To render an RFC 7807 error response with details in the body,
|
||||
| To render an RFC 9457 error response with details in the body,
|
||||
see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses]
|
||||
|
||||
| `ProblemDetail`
|
||||
| To render an RFC 7807 error response with details in the body,
|
||||
| To render an RFC 9457 error response with details in the body,
|
||||
see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses]
|
||||
|
||||
| `String`
|
||||
|
||||
+18
-23
@@ -1,34 +1,29 @@
|
||||
[[mvc-handlermapping-interceptor]]
|
||||
= Interception
|
||||
|
||||
All `HandlerMapping` implementations support handler interceptors that are useful when
|
||||
you want to apply specific functionality to certain requests -- for example, checking for
|
||||
a principal. Interceptors must implement `HandlerInterceptor` from the
|
||||
`org.springframework.web.servlet` package with three methods that should provide enough
|
||||
flexibility to do all kinds of pre-processing and post-processing:
|
||||
All `HandlerMapping` implementations support handler interception which is useful when
|
||||
you want to apply functionality across requests. A `HandlerInterceptor` can implement the
|
||||
following:
|
||||
|
||||
* `preHandle(..)`: Before the actual handler is run
|
||||
* `postHandle(..)`: After the handler is run
|
||||
* `afterCompletion(..)`: After the complete request has finished
|
||||
* `preHandle(..)` -- callback before the actual handler is run that returns a boolean.
|
||||
If the method returns `true`, execution continues; if it returns `false`, the rest of the
|
||||
execution chain is bypassed and the handler is not called.
|
||||
* `postHandle(..)` -- callback after the handler is run.
|
||||
* `afterCompletion(..)` -- callback after the complete request has finished.
|
||||
|
||||
The `preHandle(..)` method returns a boolean value. You can use this method to break or
|
||||
continue the processing of the execution chain. When this method returns `true`, the
|
||||
handler execution chain continues. When it returns false, the `DispatcherServlet`
|
||||
assumes the interceptor itself has taken care of requests (and, for example, rendered an
|
||||
appropriate view) and does not continue executing the other interceptors and the actual
|
||||
handler in the execution chain.
|
||||
NOTE: For `@ResponseBody` and `ResponseEntity` controller methods, the response is written
|
||||
and committed within the `HandlerAdapter`, before `postHandle` is called. That means it is
|
||||
too late to change the response, such as to add an extra header. You can implement
|
||||
`ResponseBodyAdvice` and declare it as an
|
||||
xref:web/webmvc/mvc-controller/ann-advice.adoc[Controller Advice] bean or configure it
|
||||
directly on `RequestMappingHandlerAdapter`.
|
||||
|
||||
See xref:web/webmvc/mvc-config/interceptors.adoc[Interceptors] in the section on MVC configuration for examples of how to
|
||||
configure interceptors. You can also register them directly by using setters on individual
|
||||
`HandlerMapping` implementations.
|
||||
|
||||
`postHandle` method is less useful with `@ResponseBody` and `ResponseEntity` methods for
|
||||
which the response is written and committed within the `HandlerAdapter` and before
|
||||
`postHandle`. That means it is too late to make any changes to the response, such as adding
|
||||
an extra header. For such scenarios, you can implement `ResponseBodyAdvice` and either
|
||||
declare it as an xref:web/webmvc/mvc-controller/ann-advice.adoc[Controller Advice] bean or configure it directly on
|
||||
`RequestMappingHandlerAdapter`.
|
||||
|
||||
|
||||
|
||||
WARNING: Interceptors are not ideally suited as a security layer due to the potential for
|
||||
a mismatch with annotated controller path matching. Generally, we recommend using Spring
|
||||
Security, or alternatively a similar approach integrated with the Servlet filter chain,
|
||||
and applied as early as possible.
|
||||
|
||||
|
||||
@@ -8,20 +8,20 @@ javaPlatform {
|
||||
|
||||
dependencies {
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.15.4"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.12.5"))
|
||||
api(platform("io.netty:netty-bom:4.1.108.Final"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.12.6"))
|
||||
api(platform("io.netty:netty-bom:4.1.109.Final"))
|
||||
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
|
||||
api(platform("io.projectreactor:reactor-bom:2023.0.5"))
|
||||
api(platform("io.projectreactor:reactor-bom:2023.0.6"))
|
||||
api(platform("io.rsocket:rsocket-bom:1.1.3"))
|
||||
api(platform("org.apache.groovy:groovy-bom:4.0.20"))
|
||||
api(platform("org.apache.groovy:groovy-bom:4.0.21"))
|
||||
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
|
||||
api(platform("org.assertj:assertj-bom:3.25.3"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.0.7"))
|
||||
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.7"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.0.9"))
|
||||
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.9"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.0"))
|
||||
api(platform("org.junit:junit-bom:5.10.2"))
|
||||
api(platform("org.mockito:mockito-bom:5.11.0"))
|
||||
api(platform("org.mockito:mockito-bom:5.12.0"))
|
||||
|
||||
constraints {
|
||||
api("com.fasterxml:aalto-xml:1.3.2")
|
||||
@@ -55,9 +55,9 @@ dependencies {
|
||||
api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE")
|
||||
api("io.reactivex.rxjava3:rxjava:3.1.8")
|
||||
api("io.smallrye.reactive:mutiny:1.10.0")
|
||||
api("io.undertow:undertow-core:2.3.12.Final")
|
||||
api("io.undertow:undertow-servlet:2.3.12.Final")
|
||||
api("io.undertow:undertow-websockets-jsr:2.3.12.Final")
|
||||
api("io.undertow:undertow-core:2.3.13.Final")
|
||||
api("io.undertow:undertow-servlet:2.3.13.Final")
|
||||
api("io.undertow:undertow-websockets-jsr:2.3.13.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")
|
||||
@@ -103,13 +103,13 @@ dependencies {
|
||||
api("org.apache.httpcomponents.client5:httpclient5:5.3.1")
|
||||
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.2.4")
|
||||
api("org.apache.poi:poi-ooxml:5.2.5")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-core:10.1.20")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.20")
|
||||
api("org.apache.tomcat:tomcat-util:10.1.20")
|
||||
api("org.apache.tomcat:tomcat-websocket:10.1.20")
|
||||
api("org.aspectj:aspectjrt:1.9.22")
|
||||
api("org.aspectj:aspectjtools:1.9.22")
|
||||
api("org.aspectj:aspectjweaver:1.9.22")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-core:10.1.24")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.24")
|
||||
api("org.apache.tomcat:tomcat-util:10.1.24")
|
||||
api("org.apache.tomcat:tomcat-websocket:10.1.24")
|
||||
api("org.aspectj:aspectjrt:1.9.22.1")
|
||||
api("org.aspectj:aspectjtools:1.9.22.1")
|
||||
api("org.aspectj:aspectjweaver:1.9.22.1")
|
||||
api("org.awaitility:awaitility:4.2.0")
|
||||
api("org.bouncycastle:bcpkix-jdk18on:1.72")
|
||||
api("org.codehaus.jettison:jettison:1.5.4")
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
version=6.1.6-SNAPSHOT
|
||||
version=6.1.7
|
||||
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
|
||||
@@ -6,6 +6,7 @@ tasks.findByName("dokkaHtmlPartial")?.configure {
|
||||
classpath.from(sourceSets["main"].runtimeClasspath)
|
||||
externalDocumentationLink {
|
||||
url.set(new URL("https://docs.spring.io/spring-framework/docs/current/javadoc-api/"))
|
||||
packageListUrl.set(new URL("https://docs.spring.io/spring-framework/docs/current/javadoc-api/element-list"))
|
||||
}
|
||||
externalDocumentationLink {
|
||||
url.set(new URL("https://projectreactor.io/docs/core/release/api/"))
|
||||
@@ -21,6 +22,7 @@ tasks.findByName("dokkaHtmlPartial")?.configure {
|
||||
}
|
||||
externalDocumentationLink {
|
||||
url.set(new URL("https://javadoc.io/doc/jakarta.servlet/jakarta.servlet-api/latest/"))
|
||||
packageListUrl.set(new URL("https://javadoc.io/doc/jakarta.servlet/jakarta.servlet-api/latest/element-list"))
|
||||
}
|
||||
externalDocumentationLink {
|
||||
url.set(new URL("https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/"))
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
plugins {
|
||||
id "com.gradle.enterprise" version "3.16.2"
|
||||
id "io.spring.ge.conventions" version "0.0.15"
|
||||
id "com.gradle.develocity" version "3.17.2"
|
||||
id "io.spring.ge.conventions" version "0.0.17"
|
||||
id "org.gradle.toolchains.foojay-resolver-convention" version "0.7.0"
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ rootProject.children.each {project ->
|
||||
}
|
||||
|
||||
settings.gradle.projectsLoaded {
|
||||
gradleEnterprise {
|
||||
develocity {
|
||||
buildScan {
|
||||
File buildDir = settings.gradle.rootProject
|
||||
.getLayout().getBuildDirectory().getAsFile().get()
|
||||
|
||||
+14
-9
@@ -170,25 +170,30 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
|
||||
|
||||
@Override
|
||||
public ClassFilter getClassFilter() {
|
||||
obtainPointcutExpression();
|
||||
checkExpression();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodMatcher getMethodMatcher() {
|
||||
obtainPointcutExpression();
|
||||
checkExpression();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check whether this pointcut is ready to match,
|
||||
* lazily building the underlying AspectJ pointcut expression.
|
||||
* Check whether this pointcut is ready to match.
|
||||
*/
|
||||
private PointcutExpression obtainPointcutExpression() {
|
||||
private void checkExpression() {
|
||||
if (getExpression() == null) {
|
||||
throw new IllegalStateException("Must set property 'expression' before attempting to match");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily build the underlying AspectJ pointcut expression.
|
||||
*/
|
||||
private PointcutExpression obtainPointcutExpression() {
|
||||
if (this.pointcutExpression == null) {
|
||||
this.pointcutClassLoader = determinePointcutClassLoader();
|
||||
this.pointcutExpression = buildPointcutExpression(this.pointcutClassLoader);
|
||||
@@ -265,10 +270,9 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
|
||||
|
||||
@Override
|
||||
public boolean matches(Class<?> targetClass) {
|
||||
PointcutExpression pointcutExpression = obtainPointcutExpression();
|
||||
try {
|
||||
try {
|
||||
return pointcutExpression.couldMatchJoinPointsInType(targetClass);
|
||||
return obtainPointcutExpression().couldMatchJoinPointsInType(targetClass);
|
||||
}
|
||||
catch (ReflectionWorldException ex) {
|
||||
logger.debug("PointcutExpression matching rejected target class - trying fallback expression", ex);
|
||||
@@ -279,6 +283,9 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException | IllegalStateException ex) {
|
||||
throw ex;
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
logger.debug("PointcutExpression matching rejected target class", ex);
|
||||
}
|
||||
@@ -287,7 +294,6 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
|
||||
|
||||
@Override
|
||||
public boolean matches(Method method, Class<?> targetClass, boolean hasIntroductions) {
|
||||
obtainPointcutExpression();
|
||||
ShadowMatch shadowMatch = getTargetShadowMatch(method, targetClass);
|
||||
|
||||
// Special handling for this, target, @this, @target, @annotation
|
||||
@@ -325,7 +331,6 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
|
||||
|
||||
@Override
|
||||
public boolean matches(Method method, Class<?> targetClass, Object... args) {
|
||||
obtainPointcutExpression();
|
||||
ShadowMatch shadowMatch = getTargetShadowMatch(method, targetClass);
|
||||
|
||||
// Bind Spring AOP proxy to AspectJ "this" and Spring AOP target to AspectJ target,
|
||||
|
||||
+2
-30
@@ -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.
|
||||
@@ -18,7 +18,6 @@ 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;
|
||||
@@ -56,8 +55,6 @@ import org.springframework.lang.Nullable;
|
||||
*/
|
||||
public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFactory {
|
||||
|
||||
private static final String AJC_MAGIC = "ajc$";
|
||||
|
||||
private static final Class<?>[] ASPECTJ_ANNOTATION_CLASSES = new Class<?>[] {
|
||||
Pointcut.class, Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class};
|
||||
|
||||
@@ -68,37 +65,11 @@ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFac
|
||||
protected final ParameterNameDiscoverer parameterNameDiscoverer = new AspectJAnnotationParameterNameDiscoverer();
|
||||
|
||||
|
||||
/**
|
||||
* We consider something to be an AspectJ aspect suitable for use by the Spring AOP system
|
||||
* if it has the @Aspect annotation, and was not compiled by ajc. The reason for this latter test
|
||||
* is that aspects written in the code-style (AspectJ language) also have the annotation present
|
||||
* when compiled by ajc with the -1.5 flag, yet they cannot be consumed by Spring AOP.
|
||||
*/
|
||||
@Override
|
||||
public boolean isAspect(Class<?> clazz) {
|
||||
return (hasAspectAnnotation(clazz) && !compiledByAjc(clazz));
|
||||
}
|
||||
|
||||
private boolean hasAspectAnnotation(Class<?> clazz) {
|
||||
return (AnnotationUtils.findAnnotation(clazz, Aspect.class) != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to detect this as "code-style" AspectJ aspects should not be
|
||||
* interpreted by Spring AOP.
|
||||
*/
|
||||
static boolean compiledByAjc(Class<?> clazz) {
|
||||
// The AJTypeSystem goes to great lengths to provide a uniform appearance between code-style and
|
||||
// annotation-style aspects. Therefore there is no 'clean' way to tell them apart. Here we rely on
|
||||
// an implementation detail of the AspectJ compiler.
|
||||
for (Field field : clazz.getDeclaredFields()) {
|
||||
if (field.getName().startsWith(AJC_MAGIC)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(Class<?> aspectClass) throws AopConfigException {
|
||||
AjType<?> ajType = AjTypeSystem.getAjType(aspectClass);
|
||||
@@ -115,6 +86,7 @@ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFac
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find and return the first AspectJ annotation on the given method
|
||||
* (there <i>should</i> only be one anyway...).
|
||||
|
||||
+15
-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.
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.aop.aspectj.annotation;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import org.springframework.aot.generate.GenerationContext;
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
|
||||
@@ -34,6 +36,8 @@ import org.springframework.util.ClassUtils;
|
||||
*/
|
||||
class AspectJAdvisorBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor {
|
||||
|
||||
private static final String AJC_MAGIC = "ajc$";
|
||||
|
||||
private static final boolean aspectjPresent = ClassUtils.isPresent("org.aspectj.lang.annotation.Pointcut",
|
||||
AspectJAdvisorBeanRegistrationAotProcessor.class.getClassLoader());
|
||||
|
||||
@@ -43,13 +47,22 @@ class AspectJAdvisorBeanRegistrationAotProcessor implements BeanRegistrationAotP
|
||||
public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) {
|
||||
if (aspectjPresent) {
|
||||
Class<?> beanClass = registeredBean.getBeanClass();
|
||||
if (AbstractAspectJAdvisorFactory.compiledByAjc(beanClass)) {
|
||||
if (compiledByAjc(beanClass)) {
|
||||
return new AspectJAdvisorContribution(beanClass);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean compiledByAjc(Class<?> clazz) {
|
||||
for (Field field : clazz.getDeclaredFields()) {
|
||||
if (field.getName().startsWith(AJC_MAGIC)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private static class AspectJAdvisorContribution implements BeanRegistrationAotContribution {
|
||||
|
||||
|
||||
+11
-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.
|
||||
@@ -124,10 +124,16 @@ public class AspectMetadata implements Serializable {
|
||||
* Extract contents from String of form {@code pertarget(contents)}.
|
||||
*/
|
||||
private String findPerClause(Class<?> aspectClass) {
|
||||
String str = aspectClass.getAnnotation(Aspect.class).value();
|
||||
int beginIndex = str.indexOf('(') + 1;
|
||||
int endIndex = str.length() - 1;
|
||||
return str.substring(beginIndex, endIndex);
|
||||
Aspect ann = aspectClass.getAnnotation(Aspect.class);
|
||||
if (ann == null) {
|
||||
return "";
|
||||
}
|
||||
String value = ann.value();
|
||||
int beginIndex = value.indexOf('(');
|
||||
if (beginIndex < 0) {
|
||||
return "";
|
||||
}
|
||||
return value.substring(beginIndex + 1, value.length() - 1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -369,8 +369,7 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
|
||||
private void validateIntroductionAdvisor(IntroductionAdvisor advisor) {
|
||||
advisor.validateInterfaces();
|
||||
// If the advisor passed validation, we can make the change.
|
||||
Class<?>[] ifcs = advisor.getInterfaces();
|
||||
for (Class<?> ifc : ifcs) {
|
||||
for (Class<?> ifc : advisor.getInterfaces()) {
|
||||
addInterface(ifc);
|
||||
}
|
||||
}
|
||||
|
||||
+7
-12
@@ -23,8 +23,6 @@ import java.util.Map;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.aspectj.weaver.tools.PointcutPrimitive;
|
||||
import org.aspectj.weaver.tools.UnsupportedPointcutPrimitiveException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import test.annotation.EmptySpringAnnotation;
|
||||
@@ -41,7 +39,6 @@ import org.springframework.beans.testfixture.beans.TestBean;
|
||||
import org.springframework.beans.testfixture.beans.subpkg.DeepBean;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
@@ -63,7 +60,7 @@ class AspectJExpressionPointcutTests {
|
||||
|
||||
|
||||
@BeforeEach
|
||||
public void setup() throws NoSuchMethodException {
|
||||
void setup() throws NoSuchMethodException {
|
||||
getAge = TestBean.class.getMethod("getAge");
|
||||
setAge = TestBean.class.getMethod("setAge", int.class);
|
||||
setSomeNumber = TestBean.class.getMethod("setSomeNumber", Number.class);
|
||||
@@ -174,7 +171,7 @@ class AspectJExpressionPointcutTests {
|
||||
void testFriendlyErrorOnNoLocationClassMatching() {
|
||||
AspectJExpressionPointcut pc = new AspectJExpressionPointcut();
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> pc.matches(ITestBean.class))
|
||||
.isThrownBy(() -> pc.getClassFilter().matches(ITestBean.class))
|
||||
.withMessageContaining("expression");
|
||||
}
|
||||
|
||||
@@ -182,7 +179,7 @@ class AspectJExpressionPointcutTests {
|
||||
void testFriendlyErrorOnNoLocation2ArgMatching() {
|
||||
AspectJExpressionPointcut pc = new AspectJExpressionPointcut();
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> pc.matches(getAge, ITestBean.class))
|
||||
.isThrownBy(() -> pc.getMethodMatcher().matches(getAge, ITestBean.class))
|
||||
.withMessageContaining("expression");
|
||||
}
|
||||
|
||||
@@ -190,13 +187,13 @@ class AspectJExpressionPointcutTests {
|
||||
void testFriendlyErrorOnNoLocation3ArgMatching() {
|
||||
AspectJExpressionPointcut pc = new AspectJExpressionPointcut();
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> pc.matches(getAge, ITestBean.class, (Object[]) null))
|
||||
.isThrownBy(() -> pc.getMethodMatcher().matches(getAge, ITestBean.class, (Object[]) null))
|
||||
.withMessageContaining("expression");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testMatchWithArgs() throws Exception {
|
||||
void testMatchWithArgs() {
|
||||
String expression = "execution(void org.springframework.beans.testfixture.beans.TestBean.setSomeNumber(Number)) && args(Double)";
|
||||
|
||||
Pointcut pointcut = getPointcut(expression);
|
||||
@@ -246,7 +243,7 @@ class AspectJExpressionPointcutTests {
|
||||
@Test
|
||||
void testInvalidExpression() {
|
||||
String expression = "execution(void org.springframework.beans.testfixture.beans.TestBean.setSomeNumber(Number) && args(Double)";
|
||||
assertThatIllegalArgumentException().isThrownBy(getPointcut(expression)::getClassFilter); // call to getClassFilter forces resolution
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> getPointcut(expression).getClassFilter().matches(Object.class));
|
||||
}
|
||||
|
||||
private TestBean getAdvisedProxy(String pointcutExpression, CallCountingInterceptor interceptor) {
|
||||
@@ -276,9 +273,7 @@ class AspectJExpressionPointcutTests {
|
||||
@Test
|
||||
void testWithUnsupportedPointcutPrimitive() {
|
||||
String expression = "call(int org.springframework.beans.testfixture.beans.TestBean.getAge())";
|
||||
assertThatExceptionOfType(UnsupportedPointcutPrimitiveException.class)
|
||||
.isThrownBy(() -> getPointcut(expression).getClassFilter()) // call to getClassFilter forces resolution...
|
||||
.satisfies(ex -> assertThat(ex.getUnsupportedPrimitive()).isEqualTo(PointcutPrimitive.CALL));
|
||||
assertThat(getPointcut(expression).getClassFilter().matches(Object.class)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+17
-16
@@ -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.
|
||||
@@ -17,7 +17,6 @@
|
||||
package org.springframework.beans.factory.aot;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Executable;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
@@ -35,6 +34,7 @@ import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.BeanDefinitionHolder;
|
||||
import org.springframework.beans.factory.support.InstanceSupplier;
|
||||
import org.springframework.beans.factory.support.RegisteredBean;
|
||||
import org.springframework.beans.factory.support.RegisteredBean.InstantiationDescriptor;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.javapoet.ClassName;
|
||||
@@ -62,7 +62,7 @@ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragme
|
||||
|
||||
private final BeanDefinitionMethodGeneratorFactory beanDefinitionMethodGeneratorFactory;
|
||||
|
||||
private final Supplier<Executable> constructorOrFactoryMethod;
|
||||
private final Supplier<InstantiationDescriptor> instantiationDescriptor;
|
||||
|
||||
|
||||
DefaultBeanRegistrationCodeFragments(BeanRegistrationsCode beanRegistrationsCode,
|
||||
@@ -72,17 +72,18 @@ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragme
|
||||
this.beanRegistrationsCode = beanRegistrationsCode;
|
||||
this.registeredBean = registeredBean;
|
||||
this.beanDefinitionMethodGeneratorFactory = beanDefinitionMethodGeneratorFactory;
|
||||
this.constructorOrFactoryMethod = SingletonSupplier.of(registeredBean::resolveConstructorOrFactoryMethod);
|
||||
this.instantiationDescriptor = SingletonSupplier.of(registeredBean::resolveInstantiationDescriptor);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ClassName getTarget(RegisteredBean registeredBean) {
|
||||
if (hasInstanceSupplier()) {
|
||||
throw new IllegalStateException("Default code generation is not supported for bean definitions "
|
||||
+ "declaring an instance supplier callback: " + registeredBean.getMergedBeanDefinition());
|
||||
String resourceDescription = registeredBean.getMergedBeanDefinition().getResourceDescription();
|
||||
throw new IllegalStateException("Error processing bean with name '" + registeredBean.getBeanName() + "'" +
|
||||
(resourceDescription != null ? " defined in " + resourceDescription : "") + ": instance supplier is not supported");
|
||||
}
|
||||
Class<?> target = extractDeclaringClass(registeredBean.getBeanType(), this.constructorOrFactoryMethod.get());
|
||||
Class<?> target = extractDeclaringClass(registeredBean, this.instantiationDescriptor.get());
|
||||
while (target.getName().startsWith("java.") && registeredBean.isInnerBean()) {
|
||||
RegisteredBean parent = registeredBean.getParent();
|
||||
Assert.state(parent != null, "No parent available for inner bean");
|
||||
@@ -91,14 +92,14 @@ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragme
|
||||
return (target.isArray() ? ClassName.get(target.getComponentType()) : ClassName.get(target));
|
||||
}
|
||||
|
||||
private Class<?> extractDeclaringClass(ResolvableType beanType, Executable executable) {
|
||||
Class<?> declaringClass = ClassUtils.getUserClass(executable.getDeclaringClass());
|
||||
if (executable instanceof Constructor<?>
|
||||
&& AccessControl.forMember(executable).isPublic()
|
||||
private Class<?> extractDeclaringClass(RegisteredBean registeredBean, InstantiationDescriptor instantiationDescriptor) {
|
||||
Class<?> declaringClass = ClassUtils.getUserClass(instantiationDescriptor.targetClass());
|
||||
if (instantiationDescriptor.executable() instanceof Constructor<?>
|
||||
&& AccessControl.forMember(instantiationDescriptor.executable()).isPublic()
|
||||
&& FactoryBean.class.isAssignableFrom(declaringClass)) {
|
||||
return extractTargetClassFromFactoryBean(declaringClass, beanType);
|
||||
return extractTargetClassFromFactoryBean(declaringClass, registeredBean.getBeanType());
|
||||
}
|
||||
return executable.getDeclaringClass();
|
||||
return declaringClass;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -238,9 +239,9 @@ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragme
|
||||
throw new IllegalStateException("Default code generation is not supported for bean definitions declaring "
|
||||
+ "an instance supplier callback: " + this.registeredBean.getMergedBeanDefinition());
|
||||
}
|
||||
return new InstanceSupplierCodeGenerator(generationContext,
|
||||
beanRegistrationCode.getClassName(), beanRegistrationCode.getMethods(), allowDirectSupplierShortcut)
|
||||
.generateCode(this.registeredBean, this.constructorOrFactoryMethod.get());
|
||||
return new InstanceSupplierCodeGenerator(generationContext, beanRegistrationCode.getClassName(),
|
||||
beanRegistrationCode.getMethods(), allowDirectSupplierShortcut).generateCode(
|
||||
this.registeredBean, this.instantiationDescriptor.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+39
-23
@@ -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.
|
||||
@@ -46,6 +46,7 @@ import org.springframework.beans.factory.support.AutowireCandidateResolver;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.InstanceSupplier;
|
||||
import org.springframework.beans.factory.support.RegisteredBean;
|
||||
import org.springframework.beans.factory.support.RegisteredBean.InstantiationDescriptor;
|
||||
import org.springframework.core.KotlinDetector;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
@@ -120,14 +121,29 @@ public class InstanceSupplierCodeGenerator {
|
||||
* @param registeredBean the bean to handle
|
||||
* @param constructorOrFactoryMethod the executable to use to create the bean
|
||||
* @return the generated code
|
||||
* @deprecated in favor of {@link #generateCode(RegisteredBean, InstantiationDescriptor)}
|
||||
*/
|
||||
@Deprecated(since = "6.1.7")
|
||||
public CodeBlock generateCode(RegisteredBean registeredBean, Executable constructorOrFactoryMethod) {
|
||||
return generateCode(registeredBean, new InstantiationDescriptor(
|
||||
constructorOrFactoryMethod, constructorOrFactoryMethod.getDeclaringClass()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the instance supplier code.
|
||||
* @param registeredBean the bean to handle
|
||||
* @param instantiationDescriptor the executable to use to create the bean
|
||||
* @return the generated code
|
||||
* @since 6.1.7
|
||||
*/
|
||||
public CodeBlock generateCode(RegisteredBean registeredBean, InstantiationDescriptor instantiationDescriptor) {
|
||||
Executable constructorOrFactoryMethod = instantiationDescriptor.executable();
|
||||
registerRuntimeHintsIfNecessary(registeredBean, constructorOrFactoryMethod);
|
||||
if (constructorOrFactoryMethod instanceof Constructor<?> constructor) {
|
||||
return generateCodeForConstructor(registeredBean, constructor);
|
||||
}
|
||||
if (constructorOrFactoryMethod instanceof Method method) {
|
||||
return generateCodeForFactoryMethod(registeredBean, method);
|
||||
return generateCodeForFactoryMethod(registeredBean, method, instantiationDescriptor.targetClass());
|
||||
}
|
||||
throw new IllegalStateException(
|
||||
"No suitable executor found for " + registeredBean.getBeanName());
|
||||
@@ -253,21 +269,21 @@ public class InstanceSupplierCodeGenerator {
|
||||
declaringClass.getSimpleName(), args);
|
||||
}
|
||||
|
||||
private CodeBlock generateCodeForFactoryMethod(RegisteredBean registeredBean, Method factoryMethod) {
|
||||
private CodeBlock generateCodeForFactoryMethod(RegisteredBean registeredBean, Method factoryMethod, Class<?> targetClass) {
|
||||
String beanName = registeredBean.getBeanName();
|
||||
Class<?> declaringClass = ClassUtils.getUserClass(factoryMethod.getDeclaringClass());
|
||||
Class<?> targetClassToUse = ClassUtils.getUserClass(targetClass);
|
||||
boolean dependsOnBean = !Modifier.isStatic(factoryMethod.getModifiers());
|
||||
|
||||
Visibility accessVisibility = getAccessVisibility(registeredBean, factoryMethod);
|
||||
if (accessVisibility != Visibility.PRIVATE) {
|
||||
return generateCodeForAccessibleFactoryMethod(
|
||||
beanName, factoryMethod, declaringClass, dependsOnBean);
|
||||
beanName, factoryMethod, targetClassToUse, dependsOnBean);
|
||||
}
|
||||
return generateCodeForInaccessibleFactoryMethod(beanName, factoryMethod, declaringClass);
|
||||
return generateCodeForInaccessibleFactoryMethod(beanName, factoryMethod, targetClassToUse);
|
||||
}
|
||||
|
||||
private CodeBlock generateCodeForAccessibleFactoryMethod(String beanName,
|
||||
Method factoryMethod, Class<?> declaringClass, boolean dependsOnBean) {
|
||||
Method factoryMethod, Class<?> targetClass, boolean dependsOnBean) {
|
||||
|
||||
this.generationContext.getRuntimeHints().reflection().registerMethod(
|
||||
factoryMethod, ExecutableMode.INTROSPECT);
|
||||
@@ -276,20 +292,20 @@ public class InstanceSupplierCodeGenerator {
|
||||
Class<?> suppliedType = ClassUtils.resolvePrimitiveIfNecessary(factoryMethod.getReturnType());
|
||||
CodeBlock.Builder code = CodeBlock.builder();
|
||||
code.add("$T.<$T>forFactoryMethod($T.class, $S)", BeanInstanceSupplier.class,
|
||||
suppliedType, declaringClass, factoryMethod.getName());
|
||||
suppliedType, targetClass, factoryMethod.getName());
|
||||
code.add(".withGenerator(($L) -> $T.$L())", REGISTERED_BEAN_PARAMETER_NAME,
|
||||
declaringClass, factoryMethod.getName());
|
||||
targetClass, factoryMethod.getName());
|
||||
return code.build();
|
||||
}
|
||||
|
||||
GeneratedMethod getInstanceMethod = generateGetInstanceSupplierMethod(method ->
|
||||
buildGetInstanceMethodForFactoryMethod(method, beanName, factoryMethod,
|
||||
declaringClass, dependsOnBean, PRIVATE_STATIC));
|
||||
targetClass, dependsOnBean, PRIVATE_STATIC));
|
||||
return generateReturnStatement(getInstanceMethod);
|
||||
}
|
||||
|
||||
private CodeBlock generateCodeForInaccessibleFactoryMethod(
|
||||
String beanName, Method factoryMethod, Class<?> declaringClass) {
|
||||
String beanName, Method factoryMethod, Class<?> targetClass) {
|
||||
|
||||
this.generationContext.getRuntimeHints().reflection().registerMethod(factoryMethod, ExecutableMode.INVOKE);
|
||||
GeneratedMethod getInstanceMethod = generateGetInstanceSupplierMethod(method -> {
|
||||
@@ -298,19 +314,19 @@ public class InstanceSupplierCodeGenerator {
|
||||
method.addModifiers(PRIVATE_STATIC);
|
||||
method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, suppliedType));
|
||||
method.addStatement(generateInstanceSupplierForFactoryMethod(
|
||||
factoryMethod, suppliedType, declaringClass, factoryMethod.getName()));
|
||||
factoryMethod, suppliedType, targetClass, factoryMethod.getName()));
|
||||
});
|
||||
return generateReturnStatement(getInstanceMethod);
|
||||
}
|
||||
|
||||
private void buildGetInstanceMethodForFactoryMethod(MethodSpec.Builder method,
|
||||
String beanName, Method factoryMethod, Class<?> declaringClass,
|
||||
String beanName, Method factoryMethod, Class<?> targetClass,
|
||||
boolean dependsOnBean, javax.lang.model.element.Modifier... modifiers) {
|
||||
|
||||
String factoryMethodName = factoryMethod.getName();
|
||||
Class<?> suppliedType = ClassUtils.resolvePrimitiveIfNecessary(factoryMethod.getReturnType());
|
||||
CodeWarnings codeWarnings = new CodeWarnings();
|
||||
codeWarnings.detectDeprecation(declaringClass, factoryMethod, suppliedType)
|
||||
codeWarnings.detectDeprecation(targetClass, factoryMethod, suppliedType)
|
||||
.detectDeprecation(Arrays.stream(factoryMethod.getParameters()).map(Parameter::getType));
|
||||
|
||||
method.addJavadoc("Get the bean instance supplier for '$L'.", beanName);
|
||||
@@ -320,41 +336,41 @@ public class InstanceSupplierCodeGenerator {
|
||||
|
||||
CodeBlock.Builder code = CodeBlock.builder();
|
||||
code.add(generateInstanceSupplierForFactoryMethod(
|
||||
factoryMethod, suppliedType, declaringClass, factoryMethodName));
|
||||
factoryMethod, suppliedType, targetClass, factoryMethodName));
|
||||
|
||||
boolean hasArguments = factoryMethod.getParameterCount() > 0;
|
||||
CodeBlock arguments = hasArguments ?
|
||||
new AutowiredArgumentsCodeGenerator(declaringClass, factoryMethod)
|
||||
new AutowiredArgumentsCodeGenerator(targetClass, factoryMethod)
|
||||
.generateCode(factoryMethod.getParameterTypes())
|
||||
: NO_ARGS;
|
||||
|
||||
CodeBlock newInstance = generateNewInstanceCodeForMethod(
|
||||
dependsOnBean, declaringClass, factoryMethodName, arguments);
|
||||
dependsOnBean, targetClass, factoryMethodName, arguments);
|
||||
code.add(generateWithGeneratorCode(hasArguments, newInstance));
|
||||
method.addStatement(code.build());
|
||||
}
|
||||
|
||||
private CodeBlock generateInstanceSupplierForFactoryMethod(Method factoryMethod,
|
||||
Class<?> suppliedType, Class<?> declaringClass, String factoryMethodName) {
|
||||
Class<?> suppliedType, Class<?> targetClass, String factoryMethodName) {
|
||||
|
||||
if (factoryMethod.getParameterCount() == 0) {
|
||||
return CodeBlock.of("return $T.<$T>forFactoryMethod($T.class, $S)",
|
||||
BeanInstanceSupplier.class, suppliedType, declaringClass, factoryMethodName);
|
||||
BeanInstanceSupplier.class, suppliedType, targetClass, factoryMethodName);
|
||||
}
|
||||
|
||||
CodeBlock parameterTypes = generateParameterTypesCode(factoryMethod.getParameterTypes(), 0);
|
||||
return CodeBlock.of("return $T.<$T>forFactoryMethod($T.class, $S, $L)",
|
||||
BeanInstanceSupplier.class, suppliedType, declaringClass, factoryMethodName, parameterTypes);
|
||||
BeanInstanceSupplier.class, suppliedType, targetClass, factoryMethodName, parameterTypes);
|
||||
}
|
||||
|
||||
private CodeBlock generateNewInstanceCodeForMethod(boolean dependsOnBean,
|
||||
Class<?> declaringClass, String factoryMethodName, CodeBlock args) {
|
||||
Class<?> targetClass, String factoryMethodName, CodeBlock args) {
|
||||
|
||||
if (!dependsOnBean) {
|
||||
return CodeBlock.of("$T.$L($L)", declaringClass, factoryMethodName, args);
|
||||
return CodeBlock.of("$T.$L($L)", targetClass, factoryMethodName, args);
|
||||
}
|
||||
return CodeBlock.of("$L.getBeanFactory().getBean($T.class).$L($L)",
|
||||
REGISTERED_BEAN_PARAMETER_NAME, declaringClass, factoryMethodName, args);
|
||||
REGISTERED_BEAN_PARAMETER_NAME, targetClass, factoryMethodName, args);
|
||||
}
|
||||
|
||||
private CodeBlock generateReturnStatement(GeneratedMethod generatedMethod) {
|
||||
|
||||
+9
-7
@@ -822,11 +822,11 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac
|
||||
/**
|
||||
* This implementation attempts to query the FactoryBean's generic parameter metadata
|
||||
* if present to determine the object type. If not present, i.e. the FactoryBean is
|
||||
* declared as a raw type, checks the FactoryBean's {@code getObjectType} method
|
||||
* declared as a raw type, it checks the FactoryBean's {@code getObjectType} method
|
||||
* on a plain instance of the FactoryBean, without bean properties applied yet.
|
||||
* If this doesn't return a type yet, and {@code allowInit} is {@code true} a
|
||||
* full creation of the FactoryBean is used as fallback (through delegation to the
|
||||
* superclass's implementation).
|
||||
* If this doesn't return a type yet and {@code allowInit} is {@code true}, full
|
||||
* creation of the FactoryBean is attempted as fallback (through delegation to the
|
||||
* superclass implementation).
|
||||
* <p>The shortcut check for a FactoryBean is only applied in case of a singleton
|
||||
* FactoryBean. If the FactoryBean instance itself is not kept as singleton,
|
||||
* it will be fully created to check the type of its exposed object.
|
||||
@@ -1156,9 +1156,11 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac
|
||||
"Bean class isn't public, and non-public access not allowed: " + beanClass.getName());
|
||||
}
|
||||
|
||||
Supplier<?> instanceSupplier = mbd.getInstanceSupplier();
|
||||
if (instanceSupplier != null) {
|
||||
return obtainFromSupplier(instanceSupplier, beanName, mbd);
|
||||
if (args == null) {
|
||||
Supplier<?> instanceSupplier = mbd.getInstanceSupplier();
|
||||
if (instanceSupplier != null) {
|
||||
return obtainFromSupplier(instanceSupplier, beanName, mbd);
|
||||
}
|
||||
}
|
||||
|
||||
if (mbd.getFactoryMethodName() != null) {
|
||||
|
||||
+13
-2
@@ -316,6 +316,17 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
|
||||
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
|
||||
"'" + beanName + "' depends on missing bean '" + dep + "'", ex);
|
||||
}
|
||||
catch (BeanCreationException ex) {
|
||||
if (requiredType != null) {
|
||||
// Wrap exception with current bean metadata but only if specifically
|
||||
// requested (indicated by required type), not for depends-on cascades.
|
||||
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
|
||||
"Failed to initialize dependency '" + ex.getBeanName() + "' of " +
|
||||
requiredType.getSimpleName() + " bean '" + beanName + "': " +
|
||||
ex.getMessage(), ex);
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1087,7 +1098,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
|
||||
|
||||
@Override
|
||||
public void setApplicationStartup(ApplicationStartup applicationStartup) {
|
||||
Assert.notNull(applicationStartup, "applicationStartup must not be null");
|
||||
Assert.notNull(applicationStartup, "ApplicationStartup must not be null");
|
||||
this.applicationStartup = applicationStartup;
|
||||
}
|
||||
|
||||
@@ -1686,7 +1697,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
|
||||
* already. The implementation is allowed to instantiate the target factory bean if
|
||||
* {@code allowInit} is {@code true} and the type cannot be determined another way;
|
||||
* otherwise it is restricted to introspecting signatures and related metadata.
|
||||
* <p>If no {@link FactoryBean#OBJECT_TYPE_ATTRIBUTE} if set on the bean definition
|
||||
* <p>If no {@link FactoryBean#OBJECT_TYPE_ATTRIBUTE} is set on the bean definition
|
||||
* and {@code allowInit} is {@code true}, the default implementation will create
|
||||
* the FactoryBean via {@code getBean} to call its {@code getObjectType} method.
|
||||
* Subclasses are encouraged to optimize this, typically by inspecting the generic
|
||||
|
||||
+43
-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.
|
||||
@@ -17,6 +17,8 @@
|
||||
package org.springframework.beans.factory.support;
|
||||
|
||||
import java.lang.reflect.Executable;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Supplier;
|
||||
@@ -41,6 +43,8 @@ import org.springframework.util.StringUtils;
|
||||
* In the case of inner-beans, the bean name may have been generated.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Stephane Nicoll
|
||||
* @author Juergen Hoeller
|
||||
* @since 6.0
|
||||
*/
|
||||
public final class RegisteredBean {
|
||||
@@ -206,12 +210,32 @@ public final class RegisteredBean {
|
||||
/**
|
||||
* Resolve the constructor or factory method to use for this bean.
|
||||
* @return the {@link java.lang.reflect.Constructor} or {@link java.lang.reflect.Method}
|
||||
* @deprecated in favor of {@link #resolveInstantiationDescriptor()}
|
||||
*/
|
||||
@Deprecated(since = "6.1.7")
|
||||
public Executable resolveConstructorOrFactoryMethod() {
|
||||
return new ConstructorResolver((AbstractAutowireCapableBeanFactory) getBeanFactory())
|
||||
.resolveConstructorOrFactoryMethod(getBeanName(), getMergedBeanDefinition());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the {@linkplain InstantiationDescriptor descriptor} to use to
|
||||
* instantiate this bean. It defines the {@link java.lang.reflect.Constructor}
|
||||
* or {@link java.lang.reflect.Method} to use as well as additional metadata.
|
||||
* @since 6.1.7
|
||||
*/
|
||||
public InstantiationDescriptor resolveInstantiationDescriptor() {
|
||||
Executable executable = resolveConstructorOrFactoryMethod();
|
||||
if (executable instanceof Method method && !Modifier.isStatic(method.getModifiers())) {
|
||||
String factoryBeanName = getMergedBeanDefinition().getFactoryBeanName();
|
||||
if (factoryBeanName != null && this.beanFactory.containsBean(factoryBeanName)) {
|
||||
return new InstantiationDescriptor(executable,
|
||||
this.beanFactory.getMergedBeanDefinition(factoryBeanName).getResolvableType().toClass());
|
||||
}
|
||||
}
|
||||
return new InstantiationDescriptor(executable, executable.getDeclaringClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an autowired argument.
|
||||
* @param descriptor the descriptor for the dependency (field/method/constructor)
|
||||
@@ -238,6 +262,24 @@ public final class RegisteredBean {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Descriptor for how a bean should be instantiated. While the {@code targetClass}
|
||||
* is usually the declaring class of the {@code executable} (in case of a constructor
|
||||
* or a locally declared factory method), there are cases where retaining the actual
|
||||
* concrete class is necessary (e.g. for an inherited factory method).
|
||||
* @param executable the {@link Executable} ({@link java.lang.reflect.Constructor}
|
||||
* or {@link java.lang.reflect.Method}) to invoke
|
||||
* @param targetClass the target {@link Class} of the executable
|
||||
* @since 6.1.7
|
||||
*/
|
||||
public record InstantiationDescriptor(Executable executable, Class<?> targetClass) {
|
||||
|
||||
public InstantiationDescriptor(Executable executable) {
|
||||
this(executable, executable.getDeclaringClass());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Resolver used to obtain inner-bean details.
|
||||
*/
|
||||
|
||||
+40
@@ -1967,6 +1967,42 @@ class DefaultListableBeanFactoryTests {
|
||||
lbf.getBean(TestBean.class, 67));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBeanByTypeInstanceWithConstructorIgnoresInstanceSupplier() {
|
||||
RootBeanDefinition bd1 = createConstructorDependencyBeanDefinition(99);
|
||||
bd1.setInstanceSupplier(() -> new ConstructorDependency(new TestBean("test")));
|
||||
lbf.registerBeanDefinition("bd1", bd1);
|
||||
|
||||
ConstructorDependency defaultInstance = lbf.getBean(ConstructorDependency.class);
|
||||
assertThat(defaultInstance.beanName).isEqualTo("bd1");
|
||||
assertThat(defaultInstance.spouseAge).isEqualTo(0);
|
||||
|
||||
ConstructorDependency argsInstance = lbf.getBean(ConstructorDependency.class, 42);
|
||||
assertThat(argsInstance.beanName).isEqualTo("bd1");
|
||||
assertThat(argsInstance.spouseAge).isEqualTo(42);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBeanByTypeInstanceWithFactoryMethodIgnoresInstanceSupplier() {
|
||||
RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class);
|
||||
bd1.setScope(BeanDefinition.SCOPE_PROTOTYPE);
|
||||
bd1.setFactoryBeanName("config");
|
||||
bd1.setFactoryMethodName("create");
|
||||
bd1.setInstanceSupplier(() -> new TestBean("test"));
|
||||
lbf.registerBeanDefinition("config", new RootBeanDefinition(BeanWithFactoryMethod.class));
|
||||
lbf.registerBeanDefinition("bd1", bd1);
|
||||
|
||||
TestBean defaultInstance = lbf.getBean(TestBean.class);
|
||||
assertThat(defaultInstance.getBeanName()).isEqualTo("bd1");
|
||||
assertThat(defaultInstance.getName()).isEqualTo("test");
|
||||
assertThat(defaultInstance.getAge()).isEqualTo(0);
|
||||
|
||||
TestBean argsInstance = lbf.getBean(TestBean.class, "another", 42);
|
||||
assertThat(argsInstance.getBeanName()).isEqualTo("bd1");
|
||||
assertThat(argsInstance.getName()).isEqualTo("another");
|
||||
assertThat(argsInstance.getAge()).isEqualTo(42);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("rawtypes")
|
||||
void beanProviderSerialization() throws Exception {
|
||||
@@ -3173,6 +3209,10 @@ class DefaultListableBeanFactoryTests {
|
||||
return tb;
|
||||
}
|
||||
|
||||
public TestBean create(String name, int age) {
|
||||
return new TestBean(name, age);
|
||||
}
|
||||
|
||||
public TestBean createWithArgs(String arg) {
|
||||
TestBean tb = new TestBean();
|
||||
tb.setName(arg);
|
||||
|
||||
+9
-7
@@ -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.
|
||||
@@ -161,7 +161,8 @@ class BeanDefinitionMethodGeneratorTests {
|
||||
|
||||
@Test
|
||||
void generateWithBeanClassAndFactoryMethodNameSetsTargetTypeAndBeanClass() {
|
||||
this.beanFactory.registerSingleton("factory", new SimpleBeanConfiguration());
|
||||
this.beanFactory.registerBeanDefinition("factory",
|
||||
new RootBeanDefinition(SimpleBeanConfiguration.class));
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition(SimpleBean.class);
|
||||
beanDefinition.setFactoryBeanName("factory");
|
||||
beanDefinition.setFactoryMethodName("simpleBean");
|
||||
@@ -182,7 +183,8 @@ class BeanDefinitionMethodGeneratorTests {
|
||||
|
||||
@Test
|
||||
void generateWithTargetTypeAndFactoryMethodNameSetsOnlyBeanClass() {
|
||||
this.beanFactory.registerSingleton("factory", new SimpleBeanConfiguration());
|
||||
this.beanFactory.registerBeanDefinition("factory",
|
||||
new RootBeanDefinition(SimpleBeanConfiguration.class));
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition();
|
||||
beanDefinition.setTargetType(SimpleBean.class);
|
||||
beanDefinition.setFactoryBeanName("factory");
|
||||
@@ -690,8 +692,8 @@ class BeanDefinitionMethodGeneratorTests {
|
||||
this.methodGeneratorFactory, registeredBean, null,
|
||||
List.of());
|
||||
assertThatIllegalStateException().isThrownBy(() -> generator.generateBeanDefinitionMethod(
|
||||
this.generationContext, this.beanRegistrationsCode)).withMessageStartingWith(
|
||||
"Default code generation is not supported for bean definitions declaring an instance supplier callback");
|
||||
this.generationContext, this.beanRegistrationsCode)).withMessage(
|
||||
"Error processing bean with name 'testBean': instance supplier is not supported");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -727,8 +729,8 @@ class BeanDefinitionMethodGeneratorTests {
|
||||
this.methodGeneratorFactory, registeredBean, null,
|
||||
List.of(aotContribution));
|
||||
assertThatIllegalStateException().isThrownBy(() -> generator.generateBeanDefinitionMethod(
|
||||
this.generationContext, this.beanRegistrationsCode)).withMessageStartingWith(
|
||||
"Default code generation is not supported for bean definitions declaring an instance supplier callback");
|
||||
this.generationContext, this.beanRegistrationsCode)).withMessage(
|
||||
"Error processing bean with name 'testBean': instance supplier is not supported");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+50
-7
@@ -31,6 +31,7 @@ import org.springframework.beans.factory.support.RegisteredBean;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.beans.testfixture.beans.factory.DummyFactory;
|
||||
import org.springframework.beans.testfixture.beans.factory.StringFactoryBean;
|
||||
import org.springframework.beans.testfixture.beans.factory.aot.DefaultSimpleBeanContract;
|
||||
import org.springframework.beans.testfixture.beans.factory.aot.GenericFactoryBean;
|
||||
import org.springframework.beans.testfixture.beans.factory.aot.MockBeanRegistrationCode;
|
||||
import org.springframework.beans.testfixture.beans.factory.aot.MockBeanRegistrationsCode;
|
||||
@@ -38,6 +39,7 @@ import org.springframework.beans.testfixture.beans.factory.aot.NumberFactoryBean
|
||||
import org.springframework.beans.testfixture.beans.factory.aot.SimpleBean;
|
||||
import org.springframework.beans.testfixture.beans.factory.aot.SimpleBeanArrayFactoryBean;
|
||||
import org.springframework.beans.testfixture.beans.factory.aot.SimpleBeanConfiguration;
|
||||
import org.springframework.beans.testfixture.beans.factory.aot.SimpleBeanContract;
|
||||
import org.springframework.beans.testfixture.beans.factory.aot.SimpleBeanFactoryBean;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.javapoet.ClassName;
|
||||
@@ -46,6 +48,7 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -63,6 +66,28 @@ class DefaultBeanRegistrationCodeFragmentsTests {
|
||||
|
||||
private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
|
||||
|
||||
@Test
|
||||
public void getTargetWithInstanceSupplier() {
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition(SimpleBean.class);
|
||||
beanDefinition.setInstanceSupplier(SimpleBean::new);
|
||||
RegisteredBean registeredBean = registerTestBean(beanDefinition);
|
||||
BeanRegistrationCodeFragments codeFragments = createInstance(registeredBean);
|
||||
assertThatIllegalStateException().isThrownBy(() -> codeFragments.getTarget(registeredBean))
|
||||
.withMessageContaining("Error processing bean with name 'testBean': instance supplier is not supported");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getTargetWithInstanceSupplierAndResourceDescription() {
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition(SimpleBean.class);
|
||||
beanDefinition.setInstanceSupplier(SimpleBean::new);
|
||||
beanDefinition.setResourceDescription("my test resource");
|
||||
RegisteredBean registeredBean = registerTestBean(beanDefinition);
|
||||
BeanRegistrationCodeFragments codeFragments = createInstance(registeredBean);
|
||||
assertThatIllegalStateException().isThrownBy(() -> codeFragments.getTarget(registeredBean))
|
||||
.withMessageContaining("Error processing bean with name 'testBean' defined in my test resource: "
|
||||
+ "instance supplier is not supported");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTargetOnConstructor() {
|
||||
RegisteredBean registeredBean = registerTestBean(SimpleBean.class,
|
||||
@@ -126,6 +151,21 @@ class DefaultBeanRegistrationCodeFragmentsTests {
|
||||
SimpleBeanConfiguration.class);
|
||||
}
|
||||
|
||||
@Test // gh-32609
|
||||
void getTargetOnMethodFromInterface() {
|
||||
this.beanFactory.registerBeanDefinition("configuration",
|
||||
new RootBeanDefinition(DefaultSimpleBeanContract.class));
|
||||
Method method = ReflectionUtils.findMethod(SimpleBeanContract.class, "simpleBean");
|
||||
assertThat(method).isNotNull();
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition(SimpleBean.class);
|
||||
applyConstructorOrFactoryMethod(beanDefinition, method);
|
||||
beanDefinition.setFactoryBeanName("configuration");
|
||||
this.beanFactory.registerBeanDefinition("testBean", beanDefinition);
|
||||
RegisteredBean registeredBean = RegisteredBean.of(this.beanFactory, "testBean");
|
||||
assertTarget(createInstance(registeredBean).getTarget(registeredBean),
|
||||
DefaultSimpleBeanContract.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTargetOnMethodWithInnerBeanInJavaPackage() {
|
||||
RegisteredBean registeredBean = registerTestBean(SimpleBean.class);
|
||||
@@ -190,7 +230,7 @@ class DefaultBeanRegistrationCodeFragmentsTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizedGetTargetDoesNotResolveConstructorOrFactoryMethod() {
|
||||
void customizedGetTargetDoesNotResolveInstantiationDescriptor() {
|
||||
RegisteredBean registeredBean = spy(registerTestBean(SimpleBean.class));
|
||||
BeanRegistrationCodeFragments customCodeFragments = createCustomCodeFragments(registeredBean, codeFragments -> new BeanRegistrationCodeFragmentsDecorator(codeFragments) {
|
||||
@Override
|
||||
@@ -199,11 +239,11 @@ class DefaultBeanRegistrationCodeFragmentsTests {
|
||||
}
|
||||
});
|
||||
assertTarget(customCodeFragments.getTarget(registeredBean), String.class);
|
||||
verify(registeredBean, never()).resolveConstructorOrFactoryMethod();
|
||||
verify(registeredBean, never()).resolveInstantiationDescriptor();
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizedGenerateInstanceSupplierCodeDoesNotResolveConstructorOrFactoryMethod() {
|
||||
void customizedGenerateInstanceSupplierCodeDoesNotResolveInstantiationDescriptor() {
|
||||
RegisteredBean registeredBean = spy(registerTestBean(SimpleBean.class));
|
||||
BeanRegistrationCodeFragments customCodeFragments = createCustomCodeFragments(registeredBean, codeFragments -> new BeanRegistrationCodeFragmentsDecorator(codeFragments) {
|
||||
@Override
|
||||
@@ -214,7 +254,7 @@ class DefaultBeanRegistrationCodeFragmentsTests {
|
||||
});
|
||||
assertThat(customCodeFragments.generateInstanceSupplierCode(this.generationContext,
|
||||
new MockBeanRegistrationCode(this.generationContext), false)).hasToString("// Hello");
|
||||
verify(registeredBean, never()).resolveConstructorOrFactoryMethod();
|
||||
verify(registeredBean, never()).resolveInstantiationDescriptor();
|
||||
}
|
||||
|
||||
private BeanRegistrationCodeFragments createCustomCodeFragments(RegisteredBean registeredBean, UnaryOperator<BeanRegistrationCodeFragments> customFragments) {
|
||||
@@ -240,13 +280,16 @@ class DefaultBeanRegistrationCodeFragmentsTests {
|
||||
return RegisteredBean.of(this.beanFactory, "testBean");
|
||||
}
|
||||
|
||||
|
||||
private RegisteredBean registerTestBean(ResolvableType beanType,
|
||||
@Nullable Executable constructorOrFactoryMethod) {
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition();
|
||||
beanDefinition.setTargetType(beanType);
|
||||
this.beanFactory.registerBeanDefinition("testBean",
|
||||
applyConstructorOrFactoryMethod(beanDefinition, constructorOrFactoryMethod));
|
||||
return registerTestBean(applyConstructorOrFactoryMethod(
|
||||
beanDefinition, constructorOrFactoryMethod));
|
||||
}
|
||||
|
||||
private RegisteredBean registerTestBean(RootBeanDefinition beanDefinition) {
|
||||
this.beanFactory.registerBeanDefinition("testBean", beanDefinition);
|
||||
return RegisteredBean.of(this.beanFactory, "testBean");
|
||||
}
|
||||
|
||||
|
||||
+25
-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.
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package org.springframework.beans.factory.aot;
|
||||
|
||||
import java.lang.reflect.Executable;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -38,10 +37,14 @@ import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.InstanceSupplier;
|
||||
import org.springframework.beans.factory.support.RegisteredBean;
|
||||
import org.springframework.beans.factory.support.RegisteredBean.InstantiationDescriptor;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.beans.testfixture.beans.TestBean;
|
||||
import org.springframework.beans.testfixture.beans.TestBeanWithPrivateConstructor;
|
||||
import org.springframework.beans.testfixture.beans.factory.aot.DefaultSimpleBeanContract;
|
||||
import org.springframework.beans.testfixture.beans.factory.aot.DeferredTypeBuilder;
|
||||
import org.springframework.beans.testfixture.beans.factory.aot.SimpleBean;
|
||||
import org.springframework.beans.testfixture.beans.factory.aot.SimpleBeanContract;
|
||||
import org.springframework.beans.testfixture.beans.factory.generator.InnerComponentConfiguration;
|
||||
import org.springframework.beans.testfixture.beans.factory.generator.InnerComponentConfiguration.EnvironmentAwareComponent;
|
||||
import org.springframework.beans.testfixture.beans.factory.generator.InnerComponentConfiguration.NoDependencyComponent;
|
||||
@@ -185,6 +188,23 @@ class InstanceSupplierCodeGeneratorTests {
|
||||
.satisfies(hasMethodWithMode(ExecutableMode.INTROSPECT));
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateWhenHasFactoryMethodOnInterface() {
|
||||
BeanDefinition beanDefinition = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(SimpleBean.class)
|
||||
.setFactoryMethodOnBean("simpleBean", "config").getBeanDefinition();
|
||||
this.beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder
|
||||
.rootBeanDefinition(DefaultSimpleBeanContract.class).getBeanDefinition());
|
||||
compile(beanDefinition, (instanceSupplier, compiled) -> {
|
||||
Object bean = getBean(beanDefinition, instanceSupplier);
|
||||
assertThat(bean).isInstanceOf(SimpleBean.class);
|
||||
assertThat(compiled.getSourceFile()).contains(
|
||||
"getBeanFactory().getBean(DefaultSimpleBeanContract.class).simpleBean()");
|
||||
});
|
||||
assertThat(getReflectionHints().getTypeHint(SimpleBeanContract.class))
|
||||
.satisfies(hasMethodWithMode(ExecutableMode.INTROSPECT));
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateWhenHasPrivateStaticFactoryMethodWithNoArg() {
|
||||
BeanDefinition beanDefinition = BeanDefinitionBuilder
|
||||
@@ -402,9 +422,9 @@ class InstanceSupplierCodeGeneratorTests {
|
||||
InstanceSupplierCodeGenerator generator = new InstanceSupplierCodeGenerator(
|
||||
this.generationContext, generateClass.getName(),
|
||||
generateClass.getMethods(), false);
|
||||
Executable constructorOrFactoryMethod = registeredBean.resolveConstructorOrFactoryMethod();
|
||||
assertThat(constructorOrFactoryMethod).isNotNull();
|
||||
CodeBlock generatedCode = generator.generateCode(registeredBean, constructorOrFactoryMethod);
|
||||
InstantiationDescriptor instantiationDescriptor = registeredBean.resolveInstantiationDescriptor();
|
||||
assertThat(instantiationDescriptor).isNotNull();
|
||||
CodeBlock generatedCode = generator.generateCode(registeredBean, instantiationDescriptor);
|
||||
typeBuilder.set(type -> {
|
||||
type.addModifiers(Modifier.PUBLIC);
|
||||
type.addSuperinterface(ParameterizedTypeName.get(Supplier.class, InstanceSupplier.class));
|
||||
|
||||
+4
-4
@@ -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.
|
||||
@@ -116,9 +116,9 @@ class InstanceSupplierCodeGeneratorKotlinTests {
|
||||
generationContext, generateClass.name,
|
||||
generateClass.methods, false
|
||||
)
|
||||
val constructorOrFactoryMethod = registeredBean.resolveConstructorOrFactoryMethod()
|
||||
Assertions.assertThat(constructorOrFactoryMethod).isNotNull()
|
||||
val generatedCode = generator.generateCode(registeredBean, constructorOrFactoryMethod)
|
||||
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(
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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.beans.testfixture.beans.factory.aot;
|
||||
|
||||
public class DefaultSimpleBeanContract implements SimpleBeanContract {
|
||||
|
||||
public SimpleBean anotherSimpleBean() {
|
||||
return new SimpleBean();
|
||||
}
|
||||
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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.beans.testfixture.beans.factory.aot;
|
||||
|
||||
/**
|
||||
* Showcase a factory method that is defined on an interface.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
public interface SimpleBeanContract {
|
||||
|
||||
default SimpleBean simpleBean() {
|
||||
return new SimpleBean();
|
||||
}
|
||||
|
||||
}
|
||||
+4
-4
@@ -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.
|
||||
@@ -27,7 +27,7 @@ import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* {@link RuntimeHintsRegistrar} implementation that makes sure {@link SchedulerFactoryBean}
|
||||
* reflection entries are registered.
|
||||
* reflection hints are registered.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @author Stephane Nicoll
|
||||
@@ -37,7 +37,7 @@ class SchedulerFactoryBeanRuntimeHints implements RuntimeHintsRegistrar {
|
||||
|
||||
private static final String SCHEDULER_FACTORY_CLASS_NAME = "org.quartz.impl.StdSchedulerFactory";
|
||||
|
||||
private final ReflectiveRuntimeHintsRegistrar reflectiveRegistrar = new ReflectiveRuntimeHintsRegistrar();
|
||||
private static final ReflectiveRuntimeHintsRegistrar registrar = new ReflectiveRuntimeHintsRegistrar();
|
||||
|
||||
|
||||
@Override
|
||||
@@ -49,7 +49,7 @@ class SchedulerFactoryBeanRuntimeHints implements RuntimeHintsRegistrar {
|
||||
.registerType(TypeReference.of(SCHEDULER_FACTORY_CLASS_NAME), this::typeHint)
|
||||
.registerTypes(TypeReference.listOf(ResourceLoaderClassLoadHelper.class,
|
||||
LocalTaskExecutorThreadPool.class, LocalDataSourceJobStore.class), this::typeHint);
|
||||
this.reflectiveRegistrar.registerRuntimeHints(hints, LocalTaskExecutorThreadPool.class);
|
||||
registrar.registerRuntimeHints(hints, LocalTaskExecutorThreadPool.class);
|
||||
}
|
||||
|
||||
private void typeHint(Builder typeHint) {
|
||||
|
||||
+15
-7
@@ -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.
|
||||
@@ -28,6 +28,7 @@ import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.aop.scope.ScopedProxyFactoryBean;
|
||||
import org.springframework.asm.Opcodes;
|
||||
import org.springframework.asm.Type;
|
||||
import org.springframework.beans.factory.BeanDefinitionStoreException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
@@ -37,6 +38,7 @@ import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.beans.factory.support.SimpleInstantiationStrategy;
|
||||
import org.springframework.cglib.core.ClassGenerator;
|
||||
import org.springframework.cglib.core.ClassLoaderAwareGeneratorStrategy;
|
||||
import org.springframework.cglib.core.CodeGenerationException;
|
||||
import org.springframework.cglib.core.SpringNamingPolicy;
|
||||
import org.springframework.cglib.proxy.Callback;
|
||||
import org.springframework.cglib.proxy.CallbackFilter;
|
||||
@@ -106,12 +108,19 @@ class ConfigurationClassEnhancer {
|
||||
}
|
||||
return configClass;
|
||||
}
|
||||
Class<?> enhancedClass = createClass(newEnhancer(configClass, classLoader));
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace(String.format("Successfully enhanced %s; enhanced class name is: %s",
|
||||
configClass.getName(), enhancedClass.getName()));
|
||||
try {
|
||||
Class<?> enhancedClass = createClass(newEnhancer(configClass, classLoader));
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace(String.format("Successfully enhanced %s; enhanced class name is: %s",
|
||||
configClass.getName(), enhancedClass.getName()));
|
||||
}
|
||||
return enhancedClass;
|
||||
}
|
||||
catch (CodeGenerationException ex) {
|
||||
throw new BeanDefinitionStoreException("Could not enhance configuration class [" + configClass.getName() +
|
||||
"]. Consider declaring @Configuration(proxyBeanMethods=false) without inter-bean references " +
|
||||
"between @Bean methods on the configuration class, avoiding the need for CGLIB enhancement.", ex);
|
||||
}
|
||||
return enhancedClass;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -225,7 +234,6 @@ class ConfigurationClassEnhancer {
|
||||
};
|
||||
return new TransformingClassGenerator(cg, transformer);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
+24
-20
@@ -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.
|
||||
@@ -75,6 +75,7 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProce
|
||||
import org.springframework.beans.factory.support.BeanNameGenerator;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.RegisteredBean;
|
||||
import org.springframework.beans.factory.support.RegisteredBean.InstantiationDescriptor;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.context.ApplicationStartupAware;
|
||||
import org.springframework.context.EnvironmentAware;
|
||||
@@ -387,11 +388,11 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
|
||||
});
|
||||
|
||||
// Detect any custom bean name generation strategy supplied through the enclosing application context
|
||||
SingletonBeanRegistry sbr = null;
|
||||
if (registry instanceof SingletonBeanRegistry _sbr) {
|
||||
sbr = _sbr;
|
||||
SingletonBeanRegistry singletonRegistry = null;
|
||||
if (registry instanceof SingletonBeanRegistry sbr) {
|
||||
singletonRegistry = sbr;
|
||||
if (!this.localBeanNameGeneratorSet) {
|
||||
BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(
|
||||
BeanNameGenerator generator = (BeanNameGenerator) singletonRegistry.getSingleton(
|
||||
AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR);
|
||||
if (generator != null) {
|
||||
this.componentScanBeanNameGenerator = generator;
|
||||
@@ -452,8 +453,8 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
|
||||
while (!candidates.isEmpty());
|
||||
|
||||
// Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
|
||||
if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
|
||||
sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
|
||||
if (singletonRegistry != null && !singletonRegistry.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
|
||||
singletonRegistry.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
|
||||
}
|
||||
|
||||
// Store the PropertySourceDescriptors to contribute them Ahead-of-time if necessary
|
||||
@@ -552,6 +553,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public PropertyValues postProcessProperties(@Nullable PropertyValues pvs, Object bean, String beanName) {
|
||||
// Inject the BeanFactory before AutowiredAnnotationBeanPostProcessor's
|
||||
// postProcessProperties method attempts to autowire other configuration beans.
|
||||
@@ -647,9 +649,9 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
|
||||
}
|
||||
return mappings;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static class PropertySourcesAotContribution implements BeanFactoryInitializationAotContribution {
|
||||
|
||||
private static final String ENVIRONMENT_VARIABLE = "environment";
|
||||
@@ -761,17 +763,18 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
|
||||
return nonNull.get();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static class ConfigurationClassProxyBeanRegistrationCodeFragments extends BeanRegistrationCodeFragmentsDecorator {
|
||||
|
||||
private final RegisteredBean registeredBean;
|
||||
|
||||
private final Class<?> proxyClass;
|
||||
|
||||
public ConfigurationClassProxyBeanRegistrationCodeFragments(BeanRegistrationCodeFragments codeFragments,
|
||||
RegisteredBean registeredBean) {
|
||||
public ConfigurationClassProxyBeanRegistrationCodeFragments(
|
||||
BeanRegistrationCodeFragments codeFragments, RegisteredBean registeredBean) {
|
||||
|
||||
super(codeFragments);
|
||||
this.registeredBean = registeredBean;
|
||||
this.proxyClass = registeredBean.getBeanType().toClass();
|
||||
@@ -780,6 +783,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
|
||||
@Override
|
||||
public CodeBlock generateSetBeanDefinitionPropertiesCode(GenerationContext generationContext,
|
||||
BeanRegistrationCode beanRegistrationCode, RootBeanDefinition beanDefinition, Predicate<String> attributeFilter) {
|
||||
|
||||
CodeBlock.Builder code = CodeBlock.builder();
|
||||
code.add(super.generateSetBeanDefinitionPropertiesCode(generationContext,
|
||||
beanRegistrationCode, beanDefinition, attributeFilter));
|
||||
@@ -790,29 +794,29 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
|
||||
|
||||
@Override
|
||||
public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext,
|
||||
BeanRegistrationCode beanRegistrationCode,
|
||||
boolean allowDirectSupplierShortcut) {
|
||||
BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) {
|
||||
|
||||
Executable executableToUse = proxyExecutable(generationContext.getRuntimeHints(),
|
||||
this.registeredBean.resolveConstructorOrFactoryMethod());
|
||||
InstantiationDescriptor instantiationDescriptor = proxyInstantiationDescriptor(
|
||||
generationContext.getRuntimeHints(), this.registeredBean.resolveInstantiationDescriptor());
|
||||
return new InstanceSupplierCodeGenerator(generationContext,
|
||||
beanRegistrationCode.getClassName(), beanRegistrationCode.getMethods(), allowDirectSupplierShortcut)
|
||||
.generateCode(this.registeredBean, executableToUse);
|
||||
.generateCode(this.registeredBean, instantiationDescriptor);
|
||||
}
|
||||
|
||||
private Executable proxyExecutable(RuntimeHints runtimeHints, Executable userExecutable) {
|
||||
private InstantiationDescriptor proxyInstantiationDescriptor(RuntimeHints runtimeHints, InstantiationDescriptor instantiationDescriptor) {
|
||||
Executable userExecutable = instantiationDescriptor.executable();
|
||||
if (userExecutable instanceof Constructor<?> userConstructor) {
|
||||
try {
|
||||
runtimeHints.reflection().registerConstructor(userConstructor, ExecutableMode.INTROSPECT);
|
||||
return this.proxyClass.getConstructor(userExecutable.getParameterTypes());
|
||||
Constructor<?> constructor = this.proxyClass.getConstructor(userExecutable.getParameterTypes());
|
||||
return new InstantiationDescriptor(constructor);
|
||||
}
|
||||
catch (NoSuchMethodException ex) {
|
||||
throw new IllegalStateException("No matching constructor found on proxy " + this.proxyClass, ex);
|
||||
}
|
||||
}
|
||||
return userExecutable;
|
||||
return instantiationDescriptor;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+7
-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.
|
||||
@@ -39,7 +39,8 @@ import org.springframework.beans.factory.support.RegisteredBean;
|
||||
*/
|
||||
class ReflectiveProcessorBeanFactoryInitializationAotProcessor implements BeanFactoryInitializationAotProcessor {
|
||||
|
||||
private static final ReflectiveRuntimeHintsRegistrar REGISTRAR = new ReflectiveRuntimeHintsRegistrar();
|
||||
private static final ReflectiveRuntimeHintsRegistrar registrar = new ReflectiveRuntimeHintsRegistrar();
|
||||
|
||||
|
||||
@Override
|
||||
public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) {
|
||||
@@ -49,7 +50,9 @@ class ReflectiveProcessorBeanFactoryInitializationAotProcessor implements BeanFa
|
||||
return new ReflectiveProcessorBeanFactoryInitializationAotContribution(beanTypes);
|
||||
}
|
||||
|
||||
private static class ReflectiveProcessorBeanFactoryInitializationAotContribution implements BeanFactoryInitializationAotContribution {
|
||||
|
||||
private static class ReflectiveProcessorBeanFactoryInitializationAotContribution
|
||||
implements BeanFactoryInitializationAotContribution {
|
||||
|
||||
private final Class<?>[] types;
|
||||
|
||||
@@ -60,9 +63,8 @@ class ReflectiveProcessorBeanFactoryInitializationAotProcessor implements BeanFa
|
||||
@Override
|
||||
public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) {
|
||||
RuntimeHints runtimeHints = generationContext.getRuntimeHints();
|
||||
REGISTRAR.registerRuntimeHints(runtimeHints, this.types);
|
||||
registrar.registerRuntimeHints(runtimeHints, this.types);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -949,7 +949,7 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
|
||||
// Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early.
|
||||
String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
|
||||
for (String weaverAwareName : weaverAwareNames) {
|
||||
getBean(weaverAwareName);
|
||||
beanFactory.getBean(weaverAwareName, LoadTimeWeaverAware.class);
|
||||
}
|
||||
|
||||
// Stop using the temporary ClassLoader for type matching.
|
||||
|
||||
+2
-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.
|
||||
@@ -126,6 +126,7 @@ public abstract class AbstractRefreshableApplicationContext extends AbstractAppl
|
||||
try {
|
||||
DefaultListableBeanFactory beanFactory = createBeanFactory();
|
||||
beanFactory.setSerializationId(getId());
|
||||
beanFactory.setApplicationStartup(getApplicationStartup());
|
||||
customizeBeanFactory(beanFactory);
|
||||
loadBeanDefinitions(beanFactory);
|
||||
this.beanFactory = beanFactory;
|
||||
|
||||
+32
-10
@@ -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.
|
||||
@@ -18,6 +18,7 @@ package org.springframework.context.support;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Supplier;
|
||||
@@ -428,16 +429,37 @@ public class GenericApplicationContext extends AbstractApplicationContext implem
|
||||
PostProcessorRegistrationDelegate.loadBeanPostProcessors(
|
||||
this.beanFactory, SmartInstantiationAwareBeanPostProcessor.class);
|
||||
|
||||
List<String> lazyBeans = new ArrayList<>();
|
||||
|
||||
// First round: non-lazy singleton beans in definition order,
|
||||
// matching preInstantiateSingletons.
|
||||
for (String beanName : this.beanFactory.getBeanDefinitionNames()) {
|
||||
Class<?> beanType = this.beanFactory.getType(beanName);
|
||||
if (beanType != null) {
|
||||
ClassHintUtils.registerProxyIfNecessary(beanType, runtimeHints);
|
||||
for (SmartInstantiationAwareBeanPostProcessor bpp : bpps) {
|
||||
Class<?> newBeanType = bpp.determineBeanType(beanType, beanName);
|
||||
if (newBeanType != beanType) {
|
||||
ClassHintUtils.registerProxyIfNecessary(newBeanType, runtimeHints);
|
||||
beanType = newBeanType;
|
||||
}
|
||||
BeanDefinition bd = getBeanDefinition(beanName);
|
||||
if (bd.isSingleton() && !bd.isLazyInit()) {
|
||||
preDetermineBeanType(beanName, bpps, runtimeHints);
|
||||
}
|
||||
else {
|
||||
lazyBeans.add(beanName);
|
||||
}
|
||||
}
|
||||
|
||||
// Second round: lazy singleton beans and scoped beans.
|
||||
for (String beanName : lazyBeans) {
|
||||
preDetermineBeanType(beanName, bpps, runtimeHints);
|
||||
}
|
||||
}
|
||||
|
||||
private void preDetermineBeanType(String beanName, List<SmartInstantiationAwareBeanPostProcessor> bpps,
|
||||
RuntimeHints runtimeHints) {
|
||||
|
||||
Class<?> beanType = this.beanFactory.getType(beanName);
|
||||
if (beanType != null) {
|
||||
ClassHintUtils.registerProxyIfNecessary(beanType, runtimeHints);
|
||||
for (SmartInstantiationAwareBeanPostProcessor bpp : bpps) {
|
||||
Class<?> newBeanType = bpp.determineBeanType(beanType, beanName);
|
||||
if (newBeanType != beanType) {
|
||||
ClassHintUtils.registerProxyIfNecessary(newBeanType, runtimeHints);
|
||||
beanType = newBeanType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -411,7 +411,7 @@ public class ScheduledAnnotationBeanPostProcessor
|
||||
}
|
||||
catch (RuntimeException ex) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into long");
|
||||
"Invalid initialDelayString value \"" + initialDelayString + "\"; " + ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -463,7 +463,7 @@ public class ScheduledAnnotationBeanPostProcessor
|
||||
}
|
||||
catch (RuntimeException ex) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into long");
|
||||
"Invalid fixedDelayString value \"" + fixedDelayString + "\"; " + ex);
|
||||
}
|
||||
tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, delayToUse)));
|
||||
}
|
||||
@@ -489,7 +489,7 @@ public class ScheduledAnnotationBeanPostProcessor
|
||||
}
|
||||
catch (RuntimeException ex) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into long");
|
||||
"Invalid fixedRateString value \"" + fixedRateString + "\"; " + ex);
|
||||
}
|
||||
tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, delayToUse)));
|
||||
}
|
||||
|
||||
+6
-6
@@ -101,8 +101,8 @@ class ConfigurationClassPostProcessorAotContributionTests {
|
||||
initializer.accept(freshBeanFactory);
|
||||
freshContext.refresh();
|
||||
assertThat(freshBeanFactory.getBeanPostProcessors()).filteredOn(ImportAwareAotBeanPostProcessor.class::isInstance)
|
||||
.singleElement().satisfies(postProcessor -> assertPostProcessorEntry(postProcessor, ImportAwareConfiguration.class,
|
||||
ImportConfiguration.class));
|
||||
.singleElement().satisfies(postProcessor ->
|
||||
assertPostProcessorEntry(postProcessor, ImportAwareConfiguration.class, ImportConfiguration.class));
|
||||
freshContext.close();
|
||||
});
|
||||
}
|
||||
@@ -245,6 +245,7 @@ class ConfigurationClassPostProcessorAotContributionTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
class PropertySourceTests {
|
||||
|
||||
@@ -410,9 +411,9 @@ class ConfigurationClassPostProcessorAotContributionTests {
|
||||
@PropertySource("classpath:org/springframework/context/annotation/p?.properties")
|
||||
static class PropertySourceWithWildcardLocationPatternConfiguration {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
class ConfigurationClassProxyTests {
|
||||
|
||||
@@ -432,15 +433,14 @@ class ConfigurationClassPostProcessorAotContributionTests {
|
||||
getRegisteredBean(CglibConfiguration.class))).isNotNull();
|
||||
}
|
||||
|
||||
|
||||
private RegisteredBean getRegisteredBean(Class<?> bean) {
|
||||
this.beanFactory.registerBeanDefinition("test", new RootBeanDefinition(bean));
|
||||
this.processor.postProcessBeanFactory(this.beanFactory);
|
||||
return RegisteredBean.of(this.beanFactory, "test");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Nullable
|
||||
private BeanFactoryInitializationAotContribution getContribution(Class<?>... types) {
|
||||
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
|
||||
@@ -458,8 +458,8 @@ class ConfigurationClassPostProcessorAotContributionTests {
|
||||
.containsExactly(entry(key.getName(), value.getName()));
|
||||
}
|
||||
|
||||
static class CustomPropertySourcesFactory extends DefaultPropertySourceFactory {
|
||||
|
||||
static class CustomPropertySourcesFactory extends DefaultPropertySourceFactory {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+48
-30
@@ -29,7 +29,9 @@ import org.springframework.beans.factory.support.AbstractBeanFactory;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.GenericBeanDefinition;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -39,8 +41,9 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
* {@link FactoryBean FactoryBeans} defined in the configuration.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Juergen Hoeller
|
||||
*/
|
||||
class ConfigurationWithFactoryBeanBeanEarlyDeductionTests {
|
||||
class ConfigurationWithFactoryBeanEarlyDeductionTests {
|
||||
|
||||
@Test
|
||||
void preFreezeDirect() {
|
||||
@@ -82,6 +85,16 @@ class ConfigurationWithFactoryBeanBeanEarlyDeductionTests {
|
||||
assertPostFreeze(AttributeClassConfiguration.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void preFreezeTargetType() {
|
||||
assertPreFreeze(TargetTypeConfiguration.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void postFreezeTargetType() {
|
||||
assertPostFreeze(TargetTypeConfiguration.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void preFreezeUnresolvedGenericFactoryBean() {
|
||||
// Covers the case where a @Configuration is picked up via component scanning
|
||||
@@ -105,14 +118,13 @@ class ConfigurationWithFactoryBeanBeanEarlyDeductionTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void assertPostFreeze(Class<?> configurationClass) {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
|
||||
configurationClass);
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(configurationClass);
|
||||
assertContainsMyBeanName(context);
|
||||
}
|
||||
|
||||
private void assertPreFreeze(Class<?> configurationClass,
|
||||
BeanFactoryPostProcessor... postProcessors) {
|
||||
private void assertPreFreeze(Class<?> configurationClass, BeanFactoryPostProcessor... postProcessors) {
|
||||
NameCollectingBeanFactoryPostProcessor postProcessor = new NameCollectingBeanFactoryPostProcessor();
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
try (context) {
|
||||
@@ -132,41 +144,38 @@ class ConfigurationWithFactoryBeanBeanEarlyDeductionTests {
|
||||
assertThat(names).containsExactly("myBean");
|
||||
}
|
||||
|
||||
private static class NameCollectingBeanFactoryPostProcessor
|
||||
implements BeanFactoryPostProcessor {
|
||||
|
||||
private static class NameCollectingBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
|
||||
|
||||
private String[] names;
|
||||
|
||||
@Override
|
||||
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
|
||||
throws BeansException {
|
||||
this.names = beanFactory.getBeanNamesForType(MyBean.class, true, false);
|
||||
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
|
||||
ResolvableType typeToMatch = ResolvableType.forClassWithGenerics(MyBean.class, String.class);
|
||||
this.names = beanFactory.getBeanNamesForType(typeToMatch, true, false);
|
||||
}
|
||||
|
||||
public String[] getNames() {
|
||||
return this.names;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class DirectConfiguration {
|
||||
|
||||
@Bean
|
||||
MyBean myBean() {
|
||||
return new MyBean();
|
||||
MyBean<String> myBean() {
|
||||
return new MyBean<>();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class GenericMethodConfiguration {
|
||||
|
||||
@Bean
|
||||
FactoryBean<MyBean> myBean() {
|
||||
return new TestFactoryBean<>(new MyBean());
|
||||
FactoryBean<MyBean<String>> myBean() {
|
||||
return new TestFactoryBean<>(new MyBean<>());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@@ -176,13 +185,11 @@ class ConfigurationWithFactoryBeanBeanEarlyDeductionTests {
|
||||
MyFactoryBean myBean() {
|
||||
return new MyFactoryBean();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(AttributeClassRegistrar.class)
|
||||
static class AttributeClassConfiguration {
|
||||
|
||||
}
|
||||
|
||||
static class AttributeClassRegistrar implements ImportBeanDefinitionRegistrar {
|
||||
@@ -191,16 +198,32 @@ class ConfigurationWithFactoryBeanBeanEarlyDeductionTests {
|
||||
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
|
||||
BeanDefinition definition = BeanDefinitionBuilder.genericBeanDefinition(
|
||||
RawWithAbstractObjectTypeFactoryBean.class).getBeanDefinition();
|
||||
definition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, MyBean.class);
|
||||
definition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE,
|
||||
ResolvableType.forClassWithGenerics(MyBean.class, String.class));
|
||||
registry.registerBeanDefinition("myBean", definition);
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(TargetTypeRegistrar.class)
|
||||
static class TargetTypeConfiguration {
|
||||
}
|
||||
|
||||
static class TargetTypeRegistrar implements ImportBeanDefinitionRegistrar {
|
||||
|
||||
@Override
|
||||
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
|
||||
RootBeanDefinition definition = new RootBeanDefinition(RawWithAbstractObjectTypeFactoryBean.class);
|
||||
definition.setTargetType(ResolvableType.forClassWithGenerics(FactoryBean.class,
|
||||
ResolvableType.forClassWithGenerics(MyBean.class, String.class)));
|
||||
registry.registerBeanDefinition("myBean", definition);
|
||||
}
|
||||
}
|
||||
|
||||
abstract static class AbstractMyBean {
|
||||
}
|
||||
|
||||
static class MyBean extends AbstractMyBean {
|
||||
static class MyBean<T> extends AbstractMyBean {
|
||||
}
|
||||
|
||||
static class TestFactoryBean<T> implements FactoryBean<T> {
|
||||
@@ -220,31 +243,26 @@ class ConfigurationWithFactoryBeanBeanEarlyDeductionTests {
|
||||
public Class<?> getObjectType() {
|
||||
return this.instance.getClass();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class MyFactoryBean extends TestFactoryBean<MyBean> {
|
||||
static class MyFactoryBean extends TestFactoryBean<MyBean<String>> {
|
||||
|
||||
public MyFactoryBean() {
|
||||
super(new MyBean());
|
||||
super(new MyBean<>());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class RawWithAbstractObjectTypeFactoryBean implements FactoryBean<Object> {
|
||||
|
||||
private final Object object = new MyBean();
|
||||
|
||||
@Override
|
||||
public Object getObject() {
|
||||
return object;
|
||||
public Object getObject() throws Exception {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getObjectType() {
|
||||
return MyBean.class;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+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.
|
||||
@@ -64,9 +64,9 @@ class KotlinApplicationListenerMethodAdapterTests {
|
||||
private class StaticApplicationListenerMethodAdapter(method: Method, private val targetBean: Any) :
|
||||
ApplicationListenerMethodAdapter("unused", targetBean.javaClass, method) {
|
||||
public override fun getTargetBean(): Any {
|
||||
return targetBean
|
||||
return targetBean
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("RedundantSuspendModifier", "UNUSED_PARAMETER")
|
||||
private class SampleEvents {
|
||||
|
||||
+9
-9
@@ -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.
|
||||
@@ -124,7 +124,7 @@ class InstrumentedMethodTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void classGetConstructorShouldMatchInstrospectPublicConstructorsHint() {
|
||||
void classGetConstructorShouldMatchIntrospectPublicConstructorsHint() {
|
||||
hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS);
|
||||
assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTOR, this.stringGetConstructor);
|
||||
}
|
||||
@@ -148,7 +148,7 @@ class InstrumentedMethodTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void classGetConstructorShouldMatchInstrospectConstructorHint() {
|
||||
void classGetConstructorShouldMatchIntrospectConstructorHint() {
|
||||
hints.reflection().registerType(String.class,typeHint ->
|
||||
typeHint.withConstructor(Collections.emptyList(), ExecutableMode.INTROSPECT));
|
||||
assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTOR, this.stringGetConstructor);
|
||||
@@ -210,7 +210,7 @@ class InstrumentedMethodTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void classGetDeclaredConstructorShouldMatchInstrospectConstructorHint() {
|
||||
void classGetDeclaredConstructorShouldMatchIntrospectConstructorHint() {
|
||||
hints.reflection().registerType(String.class, typeHint ->
|
||||
typeHint.withConstructor(TypeReference.listOf(byte[].class, byte.class), ExecutableMode.INTROSPECT));
|
||||
assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTOR, this.stringGetDeclaredConstructor);
|
||||
@@ -354,13 +354,13 @@ class InstrumentedMethodTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void classGetMethodsShouldMatchInstrospectDeclaredMethodsHint() {
|
||||
void classGetMethodsShouldMatchIntrospectDeclaredMethodsHint() {
|
||||
hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_DECLARED_METHODS);
|
||||
assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHODS, this.stringGetMethods);
|
||||
}
|
||||
|
||||
@Test
|
||||
void classGetMethodsShouldMatchInstrospectPublicMethodsHint() {
|
||||
void classGetMethodsShouldMatchIntrospectPublicMethodsHint() {
|
||||
hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_PUBLIC_METHODS);
|
||||
assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHODS, this.stringGetMethods);
|
||||
}
|
||||
@@ -396,7 +396,7 @@ class InstrumentedMethodTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void classGetMethodShouldMatchInstrospectPublicMethodsHint() {
|
||||
void classGetMethodShouldMatchIntrospectPublicMethodsHint() {
|
||||
hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_PUBLIC_METHODS);
|
||||
assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetToStringMethod);
|
||||
}
|
||||
@@ -434,13 +434,13 @@ class InstrumentedMethodTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void classGetMethodShouldNotMatchInstrospectPublicMethodsHintWhenPrivate() {
|
||||
void classGetMethodShouldNotMatchIntrospectPublicMethodsHintWhenPrivate() {
|
||||
hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_PUBLIC_METHODS);
|
||||
assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetScaleMethod);
|
||||
}
|
||||
|
||||
@Test
|
||||
void classGetMethodShouldMatchInstrospectDeclaredMethodsHintWhenPrivate() {
|
||||
void classGetMethodShouldMatchIntrospectDeclaredMethodsHintWhenPrivate() {
|
||||
hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_DECLARED_METHODS);
|
||||
assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetScaleMethod);
|
||||
}
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ public class BindingReflectionHintsRegistrar {
|
||||
* @param hints the hints instance to use
|
||||
* @param types the types to register
|
||||
*/
|
||||
public void registerReflectionHints(ReflectionHints hints, @Nullable Type... types) {
|
||||
public void registerReflectionHints(ReflectionHints hints, Type... types) {
|
||||
Set<Type> seen = new LinkedHashSet<>();
|
||||
for (Type type : types) {
|
||||
registerReflectionHints(hints, seen, type);
|
||||
|
||||
@@ -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.
|
||||
@@ -39,8 +39,7 @@ import org.springframework.core.annotation.AliasFor;
|
||||
* @see ReflectiveRuntimeHintsRegistrar
|
||||
* @see RegisterReflectionForBinding @RegisterReflectionForBinding
|
||||
*/
|
||||
@Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.CONSTRUCTOR,
|
||||
ElementType.FIELD, ElementType.METHOD })
|
||||
@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface Reflective {
|
||||
|
||||
+2
-3
@@ -36,7 +36,7 @@ import org.springframework.core.annotation.AliasFor;
|
||||
*
|
||||
* <pre class="code">
|
||||
* @Configuration
|
||||
* @RegisterReflectionForBinding({ Foo.class, Bar.class })
|
||||
* @RegisterReflectionForBinding({Foo.class, Bar.class})
|
||||
* public class MyConfig {
|
||||
* // ...
|
||||
* }</pre>
|
||||
@@ -77,8 +77,7 @@ public @interface RegisterReflectionForBinding {
|
||||
|
||||
/**
|
||||
* Classes for which reflection hints should be registered.
|
||||
* <p>At least one class must be specified either via {@link #value} or
|
||||
* {@link #classes}.
|
||||
* <p>At least one class must be specified either via {@link #value} or {@code classes}.
|
||||
* @see #value()
|
||||
*/
|
||||
@AliasFor("value")
|
||||
|
||||
+5
-4
@@ -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.
|
||||
@@ -48,8 +48,8 @@ class SpringFactoriesLoaderRuntimeHints implements RuntimeHintsRegistrar {
|
||||
|
||||
@Override
|
||||
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
|
||||
ClassLoader classLoaderToUse = (classLoader != null ? classLoader
|
||||
: SpringFactoriesLoaderRuntimeHints.class.getClassLoader());
|
||||
ClassLoader classLoaderToUse = (classLoader != null ? classLoader :
|
||||
SpringFactoriesLoaderRuntimeHints.class.getClassLoader());
|
||||
for (String resourceLocation : RESOURCE_LOCATIONS) {
|
||||
registerHints(hints, classLoaderToUse, resourceLocation);
|
||||
}
|
||||
@@ -65,6 +65,7 @@ class SpringFactoriesLoaderRuntimeHints implements RuntimeHintsRegistrar {
|
||||
|
||||
private void registerHints(RuntimeHints hints, ClassLoader classLoader,
|
||||
String factoryClassName, List<String> implementationClassNames) {
|
||||
|
||||
Class<?> factoryClass = resolveClassName(classLoader, factoryClassName);
|
||||
if (factoryClass == null) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
@@ -102,6 +103,7 @@ class SpringFactoriesLoaderRuntimeHints implements RuntimeHintsRegistrar {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class ExtendedSpringFactoriesLoader extends SpringFactoriesLoader {
|
||||
|
||||
ExtendedSpringFactoriesLoader(@Nullable ClassLoader classLoader, Map<String, List<String>> factories) {
|
||||
@@ -111,7 +113,6 @@ class SpringFactoriesLoaderRuntimeHints implements RuntimeHintsRegistrar {
|
||||
static Map<String, List<String>> accessLoadFactoriesResource(ClassLoader classLoader, String resourceLocation) {
|
||||
return SpringFactoriesLoader.loadFactoriesResource(classLoader, resourceLocation);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ public class ReflectUtils {
|
||||
Throwable throwable = null;
|
||||
try {
|
||||
classLoaderDefineClass = ClassLoader.class.getDeclaredMethod("defineClass",
|
||||
String.class, byte[].class, Integer.TYPE, Integer.TYPE, ProtectionDomain.class);
|
||||
String.class, byte[].class, Integer.TYPE, Integer.TYPE, ProtectionDomain.class);
|
||||
}
|
||||
catch (Throwable t) {
|
||||
classLoaderDefineClass = null;
|
||||
@@ -544,7 +544,15 @@ public class ReflectUtils {
|
||||
|
||||
// No defineClass variant available at all?
|
||||
if (c == null) {
|
||||
throw new CodeGenerationException(t);
|
||||
throw new CodeGenerationException(t) {
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "No compatible defineClass mechanism detected: " +
|
||||
"JVM should be started with --add-opens=java.base/java.lang=ALL-UNNAMED " +
|
||||
"for ClassLoader.defineClass to be accessible. On the module path, " +
|
||||
"you may not be able to define this CGLIB-generated class at all.";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Force static initializers to run.
|
||||
|
||||
@@ -57,7 +57,7 @@ public class MethodProxy {
|
||||
proxy.createInfo = new CreateInfo(c1, c2);
|
||||
|
||||
// SPRING PATCH BEGIN
|
||||
if (!c1.isInterface() && c1 != Object.class && !Factory.class.isAssignableFrom(c2)) {
|
||||
if (c1 != Object.class && c1.isAssignableFrom(c2.getSuperclass()) && !Factory.class.isAssignableFrom(c2)) {
|
||||
// Try early initialization for overridden methods on specifically purposed subclasses
|
||||
try {
|
||||
proxy.init();
|
||||
|
||||
+5
-2
@@ -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.
|
||||
@@ -418,7 +418,10 @@ final class TypeMappedAnnotations implements MergedAnnotations {
|
||||
|
||||
Annotation[] repeatedAnnotations = repeatableContainers.findRepeatedAnnotations(annotation);
|
||||
if (repeatedAnnotations != null) {
|
||||
return doWithAnnotations(type, aggregateIndex, source, repeatedAnnotations);
|
||||
MergedAnnotation<A> result = doWithAnnotations(type, aggregateIndex, source, repeatedAnnotations);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(
|
||||
annotation.annotationType(), repeatableContainers, annotationFilter);
|
||||
|
||||
+2
-2
@@ -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.
|
||||
@@ -48,7 +48,7 @@ public class PropertiesPropertySource extends MapPropertySource {
|
||||
@Override
|
||||
public String[] getPropertyNames() {
|
||||
synchronized (this.source) {
|
||||
return super.getPropertyNames();
|
||||
return ((Map<?, ?>) this.source).keySet().stream().filter(k -> k instanceof String).toArray(String[]::new);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,16 +23,22 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link Resource} implementation for a given {@link InputStream}.
|
||||
* {@link Resource} implementation for a given {@link InputStream} or a given
|
||||
* {@link InputStreamSource} (which can be supplied as a lambda expression)
|
||||
* for a lazy {@link InputStream} on demand.
|
||||
*
|
||||
* <p>Should only be used if no other specific {@code Resource} implementation
|
||||
* is applicable. In particular, prefer {@link ByteArrayResource} or any of the
|
||||
* file-based {@code Resource} implementations where possible.
|
||||
* file-based {@code Resource} implementations if possible. If you need to obtain
|
||||
* a custom stream multiple times, use a custom {@link AbstractResource} subclass
|
||||
* with a corresponding {@code getInputStream()} implementation.
|
||||
*
|
||||
* <p>In contrast to other {@code Resource} implementations, this is a descriptor
|
||||
* for an <i>already opened</i> resource - therefore returning {@code true} from
|
||||
* {@link #isOpen()}. Do not use an {@code InputStreamResource} if you need to
|
||||
* keep the resource descriptor somewhere, or if you need to read from a stream
|
||||
* multiple times.
|
||||
* {@link #isOpen()}. Do not use an {@code InputStreamResource} if you need to keep
|
||||
* the resource descriptor somewhere, or if you need to read from a stream multiple
|
||||
* times. This also applies when constructed with an {@code InputStreamSource}
|
||||
* which lazily obtains the stream but only allows for single access as well.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
@@ -44,30 +50,62 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class InputStreamResource extends AbstractResource {
|
||||
|
||||
private final InputStream inputStream;
|
||||
private final InputStreamSource inputStreamSource;
|
||||
|
||||
private final String description;
|
||||
|
||||
private final Object equality;
|
||||
|
||||
private boolean read = false;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new InputStreamResource.
|
||||
* Create a new {@code InputStreamResource} with a lazy {@code InputStream}
|
||||
* for single use.
|
||||
* @param inputStreamSource an on-demand source for a single-use InputStream
|
||||
* @since 6.1.7
|
||||
*/
|
||||
public InputStreamResource(InputStreamSource inputStreamSource) {
|
||||
this(inputStreamSource, "resource loaded from InputStreamSource");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@code InputStreamResource} with a lazy {@code InputStream}
|
||||
* for single use.
|
||||
* @param inputStreamSource an on-demand source for a single-use InputStream
|
||||
* @param description where the InputStream comes from
|
||||
* @since 6.1.7
|
||||
*/
|
||||
public InputStreamResource(InputStreamSource inputStreamSource, @Nullable String description) {
|
||||
Assert.notNull(inputStreamSource, "InputStreamSource must not be null");
|
||||
this.inputStreamSource = inputStreamSource;
|
||||
this.description = (description != null ? description : "");
|
||||
this.equality = inputStreamSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@code InputStreamResource} for an existing {@code InputStream}.
|
||||
* <p>Consider retrieving the InputStream on demand if possible, reducing its
|
||||
* lifetime and reliably opening it and closing it through regular
|
||||
* {@link InputStreamSource#getInputStream()} usage.
|
||||
* @param inputStream the InputStream to use
|
||||
* @see #InputStreamResource(InputStreamSource)
|
||||
*/
|
||||
public InputStreamResource(InputStream inputStream) {
|
||||
this(inputStream, "resource loaded through InputStream");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new InputStreamResource.
|
||||
* Create a new {@code InputStreamResource} for an existing {@code InputStream}.
|
||||
* @param inputStream the InputStream to use
|
||||
* @param description where the InputStream comes from
|
||||
* @see #InputStreamResource(InputStreamSource, String)
|
||||
*/
|
||||
public InputStreamResource(InputStream inputStream, @Nullable String description) {
|
||||
Assert.notNull(inputStream, "InputStream must not be null");
|
||||
this.inputStream = inputStream;
|
||||
this.inputStreamSource = () -> inputStream;
|
||||
this.description = (description != null ? description : "");
|
||||
this.equality = inputStream;
|
||||
}
|
||||
|
||||
|
||||
@@ -98,7 +136,7 @@ public class InputStreamResource extends AbstractResource {
|
||||
"do not use InputStreamResource if a stream needs to be read multiple times");
|
||||
}
|
||||
this.read = true;
|
||||
return this.inputStream;
|
||||
return this.inputStreamSource.getInputStream();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,7 +155,7 @@ public class InputStreamResource extends AbstractResource {
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
return (this == other || (other instanceof InputStreamResource that &&
|
||||
this.inputStream.equals(that.inputStream)));
|
||||
this.equality.equals(that.equality)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,7 +163,7 @@ public class InputStreamResource extends AbstractResource {
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.inputStream.hashCode();
|
||||
return this.equality.hashCode();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -38,11 +38,12 @@ import java.io.InputStream;
|
||||
* @see InputStreamResource
|
||||
* @see ByteArrayResource
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface InputStreamSource {
|
||||
|
||||
/**
|
||||
* Return an {@link InputStream} for the content of an underlying resource.
|
||||
* <p>It is expected that each call creates a <i>fresh</i> stream.
|
||||
* <p>It is usually expected that every such call creates a <i>fresh</i> stream.
|
||||
* <p>This requirement is particularly important when you consider an API such
|
||||
* as JavaMail, which needs to be able to read the stream multiple times when
|
||||
* creating mail attachments. For such a use case, it is <i>required</i>
|
||||
@@ -51,6 +52,7 @@ public interface InputStreamSource {
|
||||
* @throws java.io.FileNotFoundException if the underlying resource does not exist
|
||||
* @throws IOException if the content stream could not be opened
|
||||
* @see Resource#isReadable()
|
||||
* @see Resource#isOpen()
|
||||
*/
|
||||
InputStream getInputStream() throws IOException;
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -49,6 +49,7 @@ public class UrlResource extends AbstractFileResolvingResource {
|
||||
|
||||
private static final String AUTHORIZATION = "Authorization";
|
||||
|
||||
|
||||
/**
|
||||
* Original URI, if available; used for URI and File access.
|
||||
*/
|
||||
@@ -310,7 +311,8 @@ public class UrlResource extends AbstractFileResolvingResource {
|
||||
/**
|
||||
* This delegate creates a {@code java.net.URL}, applying the given path
|
||||
* relative to the path of the underlying URL of this resource descriptor.
|
||||
* A leading slash will get dropped; a "#" symbol will get encoded.
|
||||
* <p>A leading slash will get dropped; a "#" symbol will get encoded.
|
||||
* Note that this method effectively cleans the combined path as of 6.1.
|
||||
* @since 5.2
|
||||
* @see #createRelative(String)
|
||||
* @see ResourceUtils#toRelativeURL(URL, String)
|
||||
|
||||
+11
@@ -423,6 +423,17 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
}
|
||||
}
|
||||
else {
|
||||
String urlString = url.toString();
|
||||
String cleanedPath = StringUtils.cleanPath(urlString);
|
||||
if (!cleanedPath.equals(urlString)) {
|
||||
// Prefer cleaned URL, aligned with UrlResource#createRelative(String)
|
||||
try {
|
||||
return new UrlResource(ResourceUtils.toURI(cleanedPath));
|
||||
}
|
||||
catch (URISyntaxException | MalformedURLException ex) {
|
||||
// Fallback to regular URL construction below...
|
||||
}
|
||||
}
|
||||
return new UrlResource(url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -59,7 +59,7 @@ public class MultiValueMapAdapter<K, V> implements MultiValueMap<K, V>, Serializ
|
||||
@Nullable
|
||||
public V getFirst(K key) {
|
||||
List<V> values = this.targetMap.get(key);
|
||||
return (values != null && !values.isEmpty() ? values.get(0) : null);
|
||||
return (!CollectionUtils.isEmpty(values) ? values.get(0) : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -95,7 +95,7 @@ public class MultiValueMapAdapter<K, V> implements MultiValueMap<K, V>, Serializ
|
||||
public Map<K, V> toSingleValueMap() {
|
||||
Map<K, V> singleValueMap = CollectionUtils.newLinkedHashMap(this.targetMap.size());
|
||||
this.targetMap.forEach((key, values) -> {
|
||||
if (values != null && !values.isEmpty()) {
|
||||
if (!CollectionUtils.isEmpty(values)) {
|
||||
singleValueMap.put(key, values.get(0));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
@@ -34,7 +34,7 @@ import org.springframework.lang.Nullable;
|
||||
* <p>Consider using Spring's Resource abstraction in the core package
|
||||
* for handling all kinds of file resources in a uniform manner.
|
||||
* {@link org.springframework.core.io.ResourceLoader}'s {@code getResource()}
|
||||
* method can resolve any location to an {@link org.springframework.core.io.Resource}
|
||||
* method can resolve any location to a {@link org.springframework.core.io.Resource}
|
||||
* object, which in turn allows one to obtain a {@code java.io.File} in the
|
||||
* file system through its {@code getFile()} method.
|
||||
*
|
||||
@@ -101,6 +101,7 @@ public abstract class ResourceUtils {
|
||||
* @return whether the location qualifies as a URL
|
||||
* @see #CLASSPATH_URL_PREFIX
|
||||
* @see java.net.URL
|
||||
* @see #toURL(String)
|
||||
*/
|
||||
public static boolean isUrl(@Nullable String resourceLocation) {
|
||||
if (resourceLocation == null) {
|
||||
@@ -126,6 +127,7 @@ public abstract class ResourceUtils {
|
||||
* "classpath:" pseudo URL, a "file:" URL, or a plain file path
|
||||
* @return a corresponding URL object
|
||||
* @throws FileNotFoundException if the resource cannot be resolved to a URL
|
||||
* @see #toURL(String)
|
||||
*/
|
||||
public static URL getURL(String resourceLocation) throws FileNotFoundException {
|
||||
Assert.notNull(resourceLocation, "Resource location must not be null");
|
||||
@@ -166,6 +168,7 @@ public abstract class ResourceUtils {
|
||||
* @return a corresponding File object
|
||||
* @throws FileNotFoundException if the resource cannot be resolved to
|
||||
* a file in the file system
|
||||
* @see #getFile(URL)
|
||||
*/
|
||||
public static File getFile(String resourceLocation) throws FileNotFoundException {
|
||||
Assert.notNull(resourceLocation, "Resource location must not be null");
|
||||
@@ -197,6 +200,7 @@ public abstract class ResourceUtils {
|
||||
* @return a corresponding File object
|
||||
* @throws FileNotFoundException if the URL cannot be resolved to
|
||||
* a file in the file system
|
||||
* @see #getFile(URL, String)
|
||||
*/
|
||||
public static File getFile(URL resourceUrl) throws FileNotFoundException {
|
||||
return getFile(resourceUrl, "URL");
|
||||
@@ -237,6 +241,7 @@ public abstract class ResourceUtils {
|
||||
* @throws FileNotFoundException if the URL cannot be resolved to
|
||||
* a file in the file system
|
||||
* @since 2.5
|
||||
* @see #getFile(URI, String)
|
||||
*/
|
||||
public static File getFile(URI resourceUri) throws FileNotFoundException {
|
||||
return getFile(resourceUri, "URI");
|
||||
@@ -268,6 +273,7 @@ public abstract class ResourceUtils {
|
||||
* i.e. has protocol "file", "vfsfile" or "vfs".
|
||||
* @param url the URL to check
|
||||
* @return whether the URL has been identified as a file system URL
|
||||
* @see #isJarURL(URL)
|
||||
*/
|
||||
public static boolean isFileURL(URL url) {
|
||||
String protocol = url.getProtocol();
|
||||
@@ -281,6 +287,7 @@ public abstract class ResourceUtils {
|
||||
* "vfszip", or "wsjar".
|
||||
* @param url the URL to check
|
||||
* @return whether the URL has been identified as a JAR URL
|
||||
* @see #isJarFileURL(URL)
|
||||
*/
|
||||
public static boolean isJarURL(URL url) {
|
||||
String protocol = url.getProtocol();
|
||||
@@ -295,6 +302,7 @@ public abstract class ResourceUtils {
|
||||
* @param url the URL to check
|
||||
* @return whether the URL has been identified as a JAR file URL
|
||||
* @since 4.1
|
||||
* @see #extractJarFileURL(URL)
|
||||
*/
|
||||
public static boolean isJarFileURL(URL url) {
|
||||
return (URL_PROTOCOL_FILE.equals(url.getProtocol()) &&
|
||||
@@ -307,6 +315,7 @@ public abstract class ResourceUtils {
|
||||
* @param jarUrl the original URL
|
||||
* @return the URL for the actual jar file
|
||||
* @throws MalformedURLException if no valid jar file URL could be extracted
|
||||
* @see #extractArchiveURL(URL)
|
||||
*/
|
||||
public static URL extractJarFileURL(URL jarUrl) throws MalformedURLException {
|
||||
String urlFile = jarUrl.getFile();
|
||||
@@ -368,6 +377,7 @@ public abstract class ResourceUtils {
|
||||
* @return the URI instance
|
||||
* @throws URISyntaxException if the URL wasn't a valid URI
|
||||
* @see java.net.URL#toURI()
|
||||
* @see #toURI(String)
|
||||
*/
|
||||
public static URI toURI(URL url) throws URISyntaxException {
|
||||
return toURI(url.toString());
|
||||
@@ -379,18 +389,21 @@ public abstract class ResourceUtils {
|
||||
* @param location the location String to convert into a URI instance
|
||||
* @return the URI instance
|
||||
* @throws URISyntaxException if the location wasn't a valid URI
|
||||
* @see #toURI(URL)
|
||||
*/
|
||||
public static URI toURI(String location) throws URISyntaxException {
|
||||
return new URI(StringUtils.replace(location, " ", "%20"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a URL instance for the given location String,
|
||||
* Create a clean URL instance for the given location String,
|
||||
* going through URI construction and then URL conversion.
|
||||
* @param location the location String to convert into a URL instance
|
||||
* @return the URL instance
|
||||
* @throws MalformedURLException if the location wasn't a valid URL
|
||||
* @since 6.0
|
||||
* @see java.net.URI#toURL()
|
||||
* @see #toURI(String)
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // on JDK 20
|
||||
public static URL toURL(String location) throws MalformedURLException {
|
||||
@@ -406,13 +419,15 @@ public abstract class ResourceUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a URL instance for the given root URL and relative path,
|
||||
* Create a clean URL instance for the given root URL and relative path,
|
||||
* going through URI construction and then URL conversion.
|
||||
* @param root the root URL to start from
|
||||
* @param relativePath the relative path to apply
|
||||
* @return the relative URL instance
|
||||
* @throws MalformedURLException if the end result is not a valid URL
|
||||
* @since 6.0
|
||||
* @see #toURL(String)
|
||||
* @see StringUtils#applyRelativePath
|
||||
*/
|
||||
public static URL toRelativeURL(URL root, String relativePath) throws MalformedURLException {
|
||||
// # can appear in filenames, java.net.URL should not treat it as a fragment
|
||||
@@ -426,6 +441,7 @@ public abstract class ResourceUtils {
|
||||
* given connection, preferring {@code false} but leaving the flag at
|
||||
* its JVM default value for jar resources (typically {@code true}).
|
||||
* @param con the URLConnection to set the flag on
|
||||
* @see URLConnection#setUseCaches
|
||||
*/
|
||||
public static void useCachesIfNecessary(URLConnection con) {
|
||||
if (!(con instanceof JarURLConnection)) {
|
||||
|
||||
@@ -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.
|
||||
@@ -177,18 +177,13 @@ public abstract class StreamUtils {
|
||||
long bytesToCopy = end - start + 1;
|
||||
byte[] buffer = new byte[(int) Math.min(StreamUtils.BUFFER_SIZE, bytesToCopy)];
|
||||
while (bytesToCopy > 0) {
|
||||
int bytesRead = in.read(buffer);
|
||||
int bytesRead = (bytesToCopy < buffer.length ? in.read(buffer, 0, (int) bytesToCopy) :
|
||||
in.read(buffer));
|
||||
if (bytesRead == -1) {
|
||||
break;
|
||||
}
|
||||
else if (bytesRead <= bytesToCopy) {
|
||||
out.write(buffer, 0, bytesRead);
|
||||
bytesToCopy -= bytesRead;
|
||||
}
|
||||
else {
|
||||
out.write(buffer, 0, (int) bytesToCopy);
|
||||
bytesToCopy = 0;
|
||||
}
|
||||
out.write(buffer, 0, bytesRead);
|
||||
bytesToCopy -= bytesRead;
|
||||
}
|
||||
return (end - start + 1 - bytesToCopy);
|
||||
}
|
||||
@@ -202,7 +197,9 @@ public abstract class StreamUtils {
|
||||
* @since 4.3
|
||||
*/
|
||||
public static int drain(@Nullable InputStream in) throws IOException {
|
||||
Assert.notNull(in, "No InputStream specified");
|
||||
if (in == null) {
|
||||
return 0;
|
||||
}
|
||||
return (int) in.transferTo(OutputStream.nullOutputStream());
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ public abstract class StringUtils {
|
||||
* @see #hasText(CharSequence)
|
||||
*/
|
||||
public static boolean hasLength(@Nullable CharSequence str) {
|
||||
return (str != null && str.length() > 0);
|
||||
return (str != null && !str.isEmpty()); // as of JDK 15
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -853,7 +853,7 @@ public abstract class StringUtils {
|
||||
if (!localeValue.contains("_") && !localeValue.contains(" ")) {
|
||||
validateLocalePart(localeValue);
|
||||
Locale resolved = Locale.forLanguageTag(localeValue);
|
||||
if (resolved.getLanguage().length() > 0) {
|
||||
if (!resolved.getLanguage().isEmpty()) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
@@ -1182,7 +1182,7 @@ public abstract class StringUtils {
|
||||
if (trimTokens) {
|
||||
token = token.trim();
|
||||
}
|
||||
if (!ignoreEmptyTokens || token.length() > 0) {
|
||||
if (!ignoreEmptyTokens || !token.isEmpty()) {
|
||||
tokens.add(token);
|
||||
}
|
||||
}
|
||||
@@ -1244,7 +1244,7 @@ public abstract class StringUtils {
|
||||
result.add(deleteAny(str.substring(pos, delPos), charsToDelete));
|
||||
pos = delPos + delimiter.length();
|
||||
}
|
||||
if (str.length() > 0 && pos <= str.length()) {
|
||||
if (!str.isEmpty() && pos <= str.length()) {
|
||||
// Add rest of String, but not in case of empty input.
|
||||
result.add(deleteAny(str.substring(pos), charsToDelete));
|
||||
}
|
||||
|
||||
+145
-112
@@ -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.
|
||||
@@ -24,6 +24,7 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -35,6 +36,8 @@ import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.springframework.core.annotation.MergedAnnotations.SearchStrategy.INHERITED_ANNOTATIONS;
|
||||
import static org.springframework.core.annotation.MergedAnnotations.SearchStrategy.TYPE_HIERARCHY;
|
||||
|
||||
/**
|
||||
* Tests for {@link MergedAnnotations} and {@link RepeatableContainers} that
|
||||
@@ -49,184 +52,168 @@ class MergedAnnotationsRepeatableAnnotationTests {
|
||||
|
||||
@Test
|
||||
void inheritedAnnotationsWhenNonRepeatableThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
getAnnotations(null, NonRepeatable.class, SearchStrategy.INHERITED_ANNOTATIONS, getClass()))
|
||||
.satisfies(this::nonRepeatableRequirements);
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> getAnnotations(null, NonRepeatable.class, INHERITED_ANNOTATIONS, getClass()))
|
||||
.satisfies(this::nonRepeatableRequirements);
|
||||
}
|
||||
|
||||
@Test
|
||||
void inheritedAnnotationsWhenContainerMissingValueAttributeThrowsException() {
|
||||
assertThatAnnotationConfigurationException().isThrownBy(() ->
|
||||
getAnnotations(ContainerMissingValueAttribute.class, InvalidRepeatable.class,
|
||||
SearchStrategy.INHERITED_ANNOTATIONS, getClass()))
|
||||
.satisfies(this::missingValueAttributeRequirements);
|
||||
assertThatAnnotationConfigurationException()
|
||||
.isThrownBy(() -> getAnnotations(ContainerMissingValueAttribute.class, InvalidRepeatable.class,
|
||||
INHERITED_ANNOTATIONS, getClass()))
|
||||
.satisfies(this::missingValueAttributeRequirements);
|
||||
}
|
||||
|
||||
@Test
|
||||
void inheritedAnnotationsWhenWhenNonArrayValueAttributeThrowsException() {
|
||||
assertThatAnnotationConfigurationException().isThrownBy(() ->
|
||||
getAnnotations(ContainerWithNonArrayValueAttribute.class, InvalidRepeatable.class,
|
||||
SearchStrategy.INHERITED_ANNOTATIONS, getClass()))
|
||||
.satisfies(this::nonArrayValueAttributeRequirements);
|
||||
assertThatAnnotationConfigurationException()
|
||||
.isThrownBy(() -> getAnnotations(ContainerWithNonArrayValueAttribute.class, InvalidRepeatable.class,
|
||||
INHERITED_ANNOTATIONS, getClass()))
|
||||
.satisfies(this::nonArrayValueAttributeRequirements);
|
||||
}
|
||||
|
||||
@Test
|
||||
void inheritedAnnotationsWhenWrongComponentTypeThrowsException() {
|
||||
assertThatAnnotationConfigurationException().isThrownBy(() ->
|
||||
getAnnotations(ContainerWithArrayValueAttributeButWrongComponentType.class,
|
||||
InvalidRepeatable.class, SearchStrategy.INHERITED_ANNOTATIONS, getClass()))
|
||||
.satisfies(this::wrongComponentTypeRequirements);
|
||||
assertThatAnnotationConfigurationException()
|
||||
.isThrownBy(() -> getAnnotations(ContainerWithArrayValueAttributeButWrongComponentType.class,
|
||||
InvalidRepeatable.class, INHERITED_ANNOTATIONS, getClass()))
|
||||
.satisfies(this::wrongComponentTypeRequirements);
|
||||
}
|
||||
|
||||
@Test
|
||||
void inheritedAnnotationsWhenOnClassReturnsAnnotations() {
|
||||
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class,
|
||||
SearchStrategy.INHERITED_ANNOTATIONS, RepeatableClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B",
|
||||
"C");
|
||||
INHERITED_ANNOTATIONS, RepeatableClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", "C");
|
||||
}
|
||||
|
||||
@Test
|
||||
void inheritedAnnotationsWhenWhenOnSuperclassReturnsAnnotations() {
|
||||
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class,
|
||||
SearchStrategy.INHERITED_ANNOTATIONS, SubRepeatableClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B",
|
||||
"C");
|
||||
INHERITED_ANNOTATIONS, SubRepeatableClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", "C");
|
||||
}
|
||||
|
||||
@Test
|
||||
void inheritedAnnotationsWhenComposedOnClassReturnsAnnotations() {
|
||||
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class,
|
||||
SearchStrategy.INHERITED_ANNOTATIONS, ComposedRepeatableClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B",
|
||||
"C");
|
||||
INHERITED_ANNOTATIONS, ComposedRepeatableClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", "C");
|
||||
}
|
||||
|
||||
@Test
|
||||
void inheritedAnnotationsWhenComposedMixedWithContainerOnClassReturnsAnnotations() {
|
||||
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class,
|
||||
SearchStrategy.INHERITED_ANNOTATIONS,
|
||||
ComposedRepeatableMixedWithContainerClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B",
|
||||
"C");
|
||||
INHERITED_ANNOTATIONS, ComposedRepeatableMixedWithContainerClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", "C");
|
||||
}
|
||||
|
||||
@Test
|
||||
void inheritedAnnotationsWhenComposedContainerForRepeatableOnClassReturnsAnnotations() {
|
||||
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class,
|
||||
SearchStrategy.INHERITED_ANNOTATIONS, ComposedContainerClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B",
|
||||
"C");
|
||||
INHERITED_ANNOTATIONS, ComposedContainerClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", "C");
|
||||
}
|
||||
|
||||
@Test
|
||||
void inheritedAnnotationsWhenNoninheritedComposedRepeatableOnClassReturnsAnnotations() {
|
||||
Set<Noninherited> annotations = getAnnotations(null, Noninherited.class,
|
||||
SearchStrategy.INHERITED_ANNOTATIONS, NoninheritedRepeatableClass.class);
|
||||
assertThat(annotations.stream().map(Noninherited::value)).containsExactly("A",
|
||||
"B", "C");
|
||||
INHERITED_ANNOTATIONS, NoninheritedRepeatableClass.class);
|
||||
assertThat(annotations.stream().map(Noninherited::value)).containsExactly("A", "B", "C");
|
||||
}
|
||||
|
||||
@Test
|
||||
void inheritedAnnotationsWhenNoninheritedComposedRepeatableOnSuperclassReturnsAnnotations() {
|
||||
Set<Noninherited> annotations = getAnnotations(null, Noninherited.class,
|
||||
SearchStrategy.INHERITED_ANNOTATIONS,
|
||||
SubNoninheritedRepeatableClass.class);
|
||||
INHERITED_ANNOTATIONS, SubNoninheritedRepeatableClass.class);
|
||||
assertThat(annotations).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void typeHierarchyWhenNonRepeatableThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
getAnnotations(null, NonRepeatable.class, SearchStrategy.TYPE_HIERARCHY, getClass()))
|
||||
.satisfies(this::nonRepeatableRequirements);
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> getAnnotations(null, NonRepeatable.class, TYPE_HIERARCHY, getClass()))
|
||||
.satisfies(this::nonRepeatableRequirements);
|
||||
}
|
||||
|
||||
@Test
|
||||
void typeHierarchyWhenContainerMissingValueAttributeThrowsException() {
|
||||
assertThatAnnotationConfigurationException().isThrownBy(() ->
|
||||
getAnnotations(ContainerMissingValueAttribute.class, InvalidRepeatable.class,
|
||||
SearchStrategy.TYPE_HIERARCHY, getClass()))
|
||||
.satisfies(this::missingValueAttributeRequirements);
|
||||
assertThatAnnotationConfigurationException()
|
||||
.isThrownBy(() -> getAnnotations(ContainerMissingValueAttribute.class, InvalidRepeatable.class,
|
||||
TYPE_HIERARCHY, getClass()))
|
||||
.satisfies(this::missingValueAttributeRequirements);
|
||||
}
|
||||
|
||||
@Test
|
||||
void typeHierarchyWhenWhenNonArrayValueAttributeThrowsException() {
|
||||
assertThatAnnotationConfigurationException().isThrownBy(() ->
|
||||
getAnnotations(ContainerWithNonArrayValueAttribute.class, InvalidRepeatable.class,
|
||||
SearchStrategy.TYPE_HIERARCHY, getClass()))
|
||||
.satisfies(this::nonArrayValueAttributeRequirements);
|
||||
assertThatAnnotationConfigurationException()
|
||||
.isThrownBy(() -> getAnnotations(ContainerWithNonArrayValueAttribute.class, InvalidRepeatable.class,
|
||||
TYPE_HIERARCHY, getClass()))
|
||||
.satisfies(this::nonArrayValueAttributeRequirements);
|
||||
}
|
||||
|
||||
@Test
|
||||
void typeHierarchyWhenWrongComponentTypeThrowsException() {
|
||||
assertThatAnnotationConfigurationException().isThrownBy(() ->
|
||||
getAnnotations(ContainerWithArrayValueAttributeButWrongComponentType.class,
|
||||
InvalidRepeatable.class, SearchStrategy.TYPE_HIERARCHY, getClass()))
|
||||
.satisfies(this::wrongComponentTypeRequirements);
|
||||
assertThatAnnotationConfigurationException()
|
||||
.isThrownBy(() -> getAnnotations(ContainerWithArrayValueAttributeButWrongComponentType.class,
|
||||
InvalidRepeatable.class, TYPE_HIERARCHY, getClass()))
|
||||
.satisfies(this::wrongComponentTypeRequirements);
|
||||
}
|
||||
|
||||
@Test
|
||||
void typeHierarchyWhenOnClassReturnsAnnotations() {
|
||||
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class,
|
||||
SearchStrategy.TYPE_HIERARCHY, RepeatableClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B",
|
||||
"C");
|
||||
TYPE_HIERARCHY, RepeatableClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", "C");
|
||||
}
|
||||
|
||||
@Test
|
||||
void typeHierarchyWhenWhenOnSuperclassReturnsAnnotations() {
|
||||
void typeHierarchyWhenOnSuperclassReturnsAnnotations() {
|
||||
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class,
|
||||
SearchStrategy.TYPE_HIERARCHY, SubRepeatableClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B",
|
||||
"C");
|
||||
TYPE_HIERARCHY, SubRepeatableClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", "C");
|
||||
}
|
||||
|
||||
@Test
|
||||
void typeHierarchyWhenComposedOnClassReturnsAnnotations() {
|
||||
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class,
|
||||
SearchStrategy.TYPE_HIERARCHY, ComposedRepeatableClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B",
|
||||
"C");
|
||||
TYPE_HIERARCHY, ComposedRepeatableClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", "C");
|
||||
}
|
||||
|
||||
@Test
|
||||
void typeHierarchyWhenComposedMixedWithContainerOnClassReturnsAnnotations() {
|
||||
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class,
|
||||
SearchStrategy.TYPE_HIERARCHY,
|
||||
ComposedRepeatableMixedWithContainerClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B",
|
||||
"C");
|
||||
TYPE_HIERARCHY, ComposedRepeatableMixedWithContainerClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", "C");
|
||||
}
|
||||
|
||||
@Test
|
||||
void typeHierarchyWhenComposedContainerForRepeatableOnClassReturnsAnnotations() {
|
||||
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class,
|
||||
SearchStrategy.TYPE_HIERARCHY, ComposedContainerClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B",
|
||||
"C");
|
||||
TYPE_HIERARCHY, ComposedContainerClass.class);
|
||||
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", "C");
|
||||
}
|
||||
|
||||
@Test
|
||||
void typeHierarchyAnnotationsWhenNoninheritedComposedRepeatableOnClassReturnsAnnotations() {
|
||||
Set<Noninherited> annotations = getAnnotations(null, Noninherited.class,
|
||||
SearchStrategy.TYPE_HIERARCHY, NoninheritedRepeatableClass.class);
|
||||
assertThat(annotations.stream().map(Noninherited::value)).containsExactly("A",
|
||||
"B", "C");
|
||||
TYPE_HIERARCHY, NoninheritedRepeatableClass.class);
|
||||
assertThat(annotations.stream().map(Noninherited::value)).containsExactly("A", "B", "C");
|
||||
}
|
||||
|
||||
@Test
|
||||
void typeHierarchyAnnotationsWhenNoninheritedComposedRepeatableOnSuperclassReturnsAnnotations() {
|
||||
Set<Noninherited> annotations = getAnnotations(null, Noninherited.class,
|
||||
SearchStrategy.TYPE_HIERARCHY, SubNoninheritedRepeatableClass.class);
|
||||
assertThat(annotations.stream().map(Noninherited::value)).containsExactly("A",
|
||||
"B", "C");
|
||||
TYPE_HIERARCHY, SubNoninheritedRepeatableClass.class);
|
||||
assertThat(annotations.stream().map(Noninherited::value)).containsExactly("A", "B", "C");
|
||||
}
|
||||
|
||||
@Test
|
||||
void typeHierarchyAnnotationsWithLocalComposedAnnotationWhoseRepeatableMetaAnnotationsAreFiltered() {
|
||||
Class<WithRepeatedMetaAnnotationsClass> element = WithRepeatedMetaAnnotationsClass.class;
|
||||
SearchStrategy searchStrategy = SearchStrategy.TYPE_HIERARCHY;
|
||||
SearchStrategy searchStrategy = TYPE_HIERARCHY;
|
||||
AnnotationFilter annotationFilter = PeteRepeat.class.getName()::equals;
|
||||
|
||||
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class, searchStrategy, element, annotationFilter);
|
||||
@@ -240,6 +227,44 @@ class MergedAnnotationsRepeatableAnnotationTests {
|
||||
assertThat(annotationTypes).containsExactly(WithRepeatedMetaAnnotations.class, Noninherited.class, Noninherited.class);
|
||||
}
|
||||
|
||||
@Test // gh-32731
|
||||
void searchFindsRepeatableContainerAnnotationAndRepeatedAnnotations() {
|
||||
Class<?> clazz = StandardRepeatablesWithContainerWithMultipleAttributesTestCase.class;
|
||||
|
||||
// NO RepeatableContainers
|
||||
MergedAnnotations mergedAnnotations = MergedAnnotations.from(clazz, TYPE_HIERARCHY, RepeatableContainers.none());
|
||||
ContainerWithMultipleAttributes container = mergedAnnotations
|
||||
.get(ContainerWithMultipleAttributes.class)
|
||||
.synthesize(MergedAnnotation::isPresent).orElse(null);
|
||||
assertThat(container).as("container").isNotNull();
|
||||
assertThat(container.name()).isEqualTo("enigma");
|
||||
RepeatableWithContainerWithMultipleAttributes[] repeatedAnnotations = container.value();
|
||||
assertThat(Arrays.stream(repeatedAnnotations).map(RepeatableWithContainerWithMultipleAttributes::value))
|
||||
.containsExactly("A", "B");
|
||||
Set<RepeatableWithContainerWithMultipleAttributes> set =
|
||||
mergedAnnotations.stream(RepeatableWithContainerWithMultipleAttributes.class)
|
||||
.collect(MergedAnnotationCollectors.toAnnotationSet());
|
||||
// Only finds the locally declared repeated annotation.
|
||||
assertThat(set.stream().map(RepeatableWithContainerWithMultipleAttributes::value))
|
||||
.containsExactly("C");
|
||||
|
||||
// Standard RepeatableContainers
|
||||
mergedAnnotations = MergedAnnotations.from(clazz, TYPE_HIERARCHY, RepeatableContainers.standardRepeatables());
|
||||
container = mergedAnnotations
|
||||
.get(ContainerWithMultipleAttributes.class)
|
||||
.synthesize(MergedAnnotation::isPresent).orElse(null);
|
||||
assertThat(container).as("container").isNotNull();
|
||||
assertThat(container.name()).isEqualTo("enigma");
|
||||
repeatedAnnotations = container.value();
|
||||
assertThat(Arrays.stream(repeatedAnnotations).map(RepeatableWithContainerWithMultipleAttributes::value))
|
||||
.containsExactly("A", "B");
|
||||
set = mergedAnnotations.stream(RepeatableWithContainerWithMultipleAttributes.class)
|
||||
.collect(MergedAnnotationCollectors.toAnnotationSet());
|
||||
// Finds the locally declared repeated annotation plus the 2 in the container.
|
||||
assertThat(set.stream().map(RepeatableWithContainerWithMultipleAttributes::value))
|
||||
.containsExactly("A", "B", "C");
|
||||
}
|
||||
|
||||
private <A extends Annotation> Set<A> getAnnotations(Class<? extends Annotation> container,
|
||||
Class<A> repeatable, SearchStrategy searchStrategy, AnnotatedElement element) {
|
||||
|
||||
@@ -255,32 +280,37 @@ class MergedAnnotationsRepeatableAnnotationTests {
|
||||
}
|
||||
|
||||
private void nonRepeatableRequirements(Exception ex) {
|
||||
assertThat(ex.getMessage()).startsWith(
|
||||
"Annotation type must be a repeatable annotation").contains(
|
||||
"failed to resolve container type for",
|
||||
NonRepeatable.class.getName());
|
||||
assertThat(ex)
|
||||
.hasMessageStartingWith("Annotation type must be a repeatable annotation")
|
||||
.hasMessageContaining("failed to resolve container type for", NonRepeatable.class.getName());
|
||||
}
|
||||
|
||||
private void missingValueAttributeRequirements(Exception ex) {
|
||||
assertThat(ex.getMessage()).startsWith(
|
||||
"Invalid declaration of container type").contains(
|
||||
assertThat(ex)
|
||||
.hasMessageStartingWith("Invalid declaration of container type")
|
||||
.hasMessageContaining(
|
||||
ContainerMissingValueAttribute.class.getName(),
|
||||
"for repeatable annotation", InvalidRepeatable.class.getName());
|
||||
assertThat(ex).hasCauseInstanceOf(NoSuchMethodException.class);
|
||||
"for repeatable annotation",
|
||||
InvalidRepeatable.class.getName())
|
||||
.hasCauseInstanceOf(NoSuchMethodException.class);
|
||||
}
|
||||
|
||||
private void nonArrayValueAttributeRequirements(Exception ex) {
|
||||
assertThat(ex.getMessage()).startsWith("Container type").contains(
|
||||
ContainerWithNonArrayValueAttribute.class.getName(),
|
||||
"must declare a 'value' attribute for an array of type",
|
||||
InvalidRepeatable.class.getName());
|
||||
assertThat(ex)
|
||||
.hasMessageStartingWith("Container type")
|
||||
.hasMessageContaining(
|
||||
ContainerWithNonArrayValueAttribute.class.getName(),
|
||||
"must declare a 'value' attribute for an array of type",
|
||||
InvalidRepeatable.class.getName());
|
||||
}
|
||||
|
||||
private void wrongComponentTypeRequirements(Exception ex) {
|
||||
assertThat(ex.getMessage()).startsWith("Container type").contains(
|
||||
ContainerWithArrayValueAttributeButWrongComponentType.class.getName(),
|
||||
"must declare a 'value' attribute for an array of type",
|
||||
InvalidRepeatable.class.getName());
|
||||
assertThat(ex)
|
||||
.hasMessageStartingWith("Container type")
|
||||
.hasMessageContaining(
|
||||
ContainerWithArrayValueAttributeButWrongComponentType.class.getName(),
|
||||
"must declare a 'value' attribute for an array of type",
|
||||
InvalidRepeatable.class.getName());
|
||||
}
|
||||
|
||||
private static ThrowableTypeAssert<AnnotationConfigurationException> assertThatAnnotationConfigurationException() {
|
||||
@@ -289,33 +319,28 @@ class MergedAnnotationsRepeatableAnnotationTests {
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface NonRepeatable {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface ContainerMissingValueAttribute {
|
||||
|
||||
// InvalidRepeatable[] value();
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface ContainerWithNonArrayValueAttribute {
|
||||
|
||||
InvalidRepeatable value();
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface ContainerWithArrayValueAttributeButWrongComponentType {
|
||||
|
||||
String[] value();
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface InvalidRepeatable {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@@ -323,7 +348,6 @@ class MergedAnnotationsRepeatableAnnotationTests {
|
||||
@interface PeteRepeats {
|
||||
|
||||
PeteRepeat[] value();
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@@ -332,7 +356,6 @@ class MergedAnnotationsRepeatableAnnotationTests {
|
||||
@interface PeteRepeat {
|
||||
|
||||
String value();
|
||||
|
||||
}
|
||||
|
||||
@PeteRepeat("shadowed")
|
||||
@@ -343,7 +366,6 @@ class MergedAnnotationsRepeatableAnnotationTests {
|
||||
|
||||
@AliasFor(annotation = PeteRepeat.class)
|
||||
String value();
|
||||
|
||||
}
|
||||
|
||||
@PeteRepeat("shadowed")
|
||||
@@ -354,7 +376,6 @@ class MergedAnnotationsRepeatableAnnotationTests {
|
||||
|
||||
@AliasFor(annotation = PeteRepeat.class)
|
||||
String value();
|
||||
|
||||
}
|
||||
|
||||
@PeteRepeats({ @PeteRepeat("B"), @PeteRepeat("C") })
|
||||
@@ -362,37 +383,31 @@ class MergedAnnotationsRepeatableAnnotationTests {
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
@interface ComposedContainer {
|
||||
|
||||
}
|
||||
|
||||
@PeteRepeat("A")
|
||||
@PeteRepeats({ @PeteRepeat("B"), @PeteRepeat("C") })
|
||||
static class RepeatableClass {
|
||||
|
||||
}
|
||||
|
||||
static class SubRepeatableClass extends RepeatableClass {
|
||||
|
||||
}
|
||||
|
||||
@ForPetesSake("B")
|
||||
@ForTheLoveOfFoo("C")
|
||||
@PeteRepeat("A")
|
||||
static class ComposedRepeatableClass {
|
||||
|
||||
}
|
||||
|
||||
@ForPetesSake("C")
|
||||
@PeteRepeats(@PeteRepeat("A"))
|
||||
@PeteRepeat("B")
|
||||
static class ComposedRepeatableMixedWithContainerClass {
|
||||
|
||||
}
|
||||
|
||||
@PeteRepeat("A")
|
||||
@ComposedContainer
|
||||
static class ComposedContainerClass {
|
||||
|
||||
}
|
||||
|
||||
@Target(ElementType.TYPE)
|
||||
@@ -400,7 +415,6 @@ class MergedAnnotationsRepeatableAnnotationTests {
|
||||
@interface Noninheriteds {
|
||||
|
||||
Noninherited[] value();
|
||||
|
||||
}
|
||||
|
||||
@Target(ElementType.TYPE)
|
||||
@@ -413,7 +427,6 @@ class MergedAnnotationsRepeatableAnnotationTests {
|
||||
|
||||
@AliasFor("value")
|
||||
String name() default "";
|
||||
|
||||
}
|
||||
|
||||
@Noninherited(name = "shadowed")
|
||||
@@ -423,17 +436,14 @@ class MergedAnnotationsRepeatableAnnotationTests {
|
||||
|
||||
@AliasFor(annotation = Noninherited.class)
|
||||
String name() default "";
|
||||
|
||||
}
|
||||
|
||||
@ComposedNoninherited(name = "C")
|
||||
@Noninheriteds({ @Noninherited(value = "A"), @Noninherited(name = "B") })
|
||||
static class NoninheritedRepeatableClass {
|
||||
|
||||
}
|
||||
|
||||
static class SubNoninheritedRepeatableClass extends NoninheritedRepeatableClass {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@@ -449,4 +459,27 @@ class MergedAnnotationsRepeatableAnnotationTests {
|
||||
static class WithRepeatedMetaAnnotationsClass {
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface ContainerWithMultipleAttributes {
|
||||
|
||||
RepeatableWithContainerWithMultipleAttributes[] value();
|
||||
|
||||
String name() default "";
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Repeatable(ContainerWithMultipleAttributes.class)
|
||||
@interface RepeatableWithContainerWithMultipleAttributes {
|
||||
|
||||
String value() default "";
|
||||
}
|
||||
|
||||
@ContainerWithMultipleAttributes(name = "enigma", value = {
|
||||
@RepeatableWithContainerWithMultipleAttributes("A"),
|
||||
@RepeatableWithContainerWithMultipleAttributes("B")
|
||||
})
|
||||
@RepeatableWithContainerWithMultipleAttributes("C")
|
||||
static class StandardRepeatablesWithContainerWithMultipleAttributesTestCase {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+11
-2
@@ -16,7 +16,9 @@
|
||||
|
||||
package org.springframework.core.env;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -300,6 +302,12 @@ class StandardEnvironmentTests {
|
||||
assertThat(systemProperties.get(DISALLOWED_PROPERTY_NAME)).isEqualTo(DISALLOWED_PROPERTY_VALUE);
|
||||
assertThat(systemProperties.get(STRING_PROPERTY_NAME)).isEqualTo(NON_STRING_PROPERTY_VALUE);
|
||||
assertThat(systemProperties.get(NON_STRING_PROPERTY_NAME)).isEqualTo(STRING_PROPERTY_VALUE);
|
||||
|
||||
PropertiesPropertySource systemPropertySource = (PropertiesPropertySource)
|
||||
environment.getPropertySources().get(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME);
|
||||
Set<String> expectedKeys = new HashSet<>(System.getProperties().stringPropertyNames());
|
||||
expectedKeys.add(STRING_PROPERTY_NAME); // filtered out by stringPropertyNames due to non-String value
|
||||
assertThat(Set.of(systemPropertySource.getPropertyNames())).isEqualTo(expectedKeys);
|
||||
}
|
||||
finally {
|
||||
System.clearProperty(ALLOWED_PROPERTY_NAME);
|
||||
@@ -316,6 +324,7 @@ class StandardEnvironmentTests {
|
||||
assertThat(System.getenv()).isSameAs(systemEnvironment);
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
class GetActiveProfiles {
|
||||
|
||||
@@ -365,6 +374,7 @@ class StandardEnvironmentTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
class AcceptsProfilesTests {
|
||||
|
||||
@@ -447,9 +457,9 @@ class StandardEnvironmentTests {
|
||||
environment.addActiveProfile("p2");
|
||||
assertThat(environment.acceptsProfiles(Profiles.of("p1 & p2"))).isTrue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
class MatchesProfilesTests {
|
||||
|
||||
@@ -549,7 +559,6 @@ class StandardEnvironmentTests {
|
||||
assertThat(environment.matchesProfiles("p2 & (foo | p1)")).isTrue();
|
||||
assertThat(environment.matchesProfiles("foo", "(p2 & p1)")).isTrue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import okhttp3.mockwebserver.Dispatcher;
|
||||
@@ -189,14 +190,21 @@ class ResourceTests {
|
||||
String content = FileCopyUtils.copyToString(new InputStreamReader(resource1.getInputStream()));
|
||||
assertThat(content).isEqualTo(testString);
|
||||
assertThat(new InputStreamResource(is)).isEqualTo(resource1);
|
||||
assertThat(new InputStreamResource(() -> is)).isNotEqualTo(resource1);
|
||||
assertThatIllegalStateException().isThrownBy(resource1::getInputStream);
|
||||
|
||||
Resource resource2 = new InputStreamResource(new ByteArrayInputStream(testBytes));
|
||||
assertThat(resource2.getContentAsByteArray()).containsExactly(testBytes);
|
||||
assertThatIllegalStateException().isThrownBy(resource2::getContentAsByteArray);
|
||||
|
||||
Resource resource3 = new InputStreamResource(new ByteArrayInputStream(testBytes));
|
||||
AtomicBoolean obtained = new AtomicBoolean();
|
||||
Resource resource3 = new InputStreamResource(() -> {
|
||||
obtained.set(true);
|
||||
return new ByteArrayInputStream(testBytes);
|
||||
});
|
||||
assertThat(obtained).isFalse();
|
||||
assertThat(resource3.getContentAsString(StandardCharsets.US_ASCII)).isEqualTo(testString);
|
||||
assertThat(obtained).isTrue();
|
||||
assertThatIllegalStateException().isThrownBy(() -> resource3.getContentAsString(StandardCharsets.US_ASCII));
|
||||
}
|
||||
|
||||
@@ -206,6 +214,10 @@ class ResourceTests {
|
||||
Resource resource = new InputStreamResource(is);
|
||||
assertThat(resource.exists()).isTrue();
|
||||
assertThat(resource.isOpen()).isTrue();
|
||||
|
||||
resource = new InputStreamResource(() -> is);
|
||||
assertThat(resource.exists()).isTrue();
|
||||
assertThat(resource.isOpen()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -213,6 +225,9 @@ class ResourceTests {
|
||||
InputStream is = new ByteArrayInputStream("testString".getBytes());
|
||||
Resource resource = new InputStreamResource(is, "my description");
|
||||
assertThat(resource.getDescription()).contains("my description");
|
||||
|
||||
resource = new InputStreamResource(() -> is, "my description");
|
||||
assertThat(resource.getDescription()).contains("my description");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -38,6 +38,7 @@ import static org.mockito.Mockito.never;
|
||||
* Tests for {@link StreamUtils}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Juergen Hoeller
|
||||
*/
|
||||
class StreamUtilsTests {
|
||||
|
||||
@@ -45,6 +46,7 @@ class StreamUtilsTests {
|
||||
|
||||
private String string = "";
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
new Random().nextBytes(bytes);
|
||||
@@ -53,6 +55,7 @@ class StreamUtilsTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void copyToByteArray() throws Exception {
|
||||
InputStream inputStream = new ByteArrayInputStream(bytes);
|
||||
@@ -91,11 +94,30 @@ class StreamUtilsTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void copyRange() throws Exception {
|
||||
void copyRangeWithinBuffer() throws Exception {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
StreamUtils.copyRange(new ByteArrayInputStream(bytes), out, 0, 100);
|
||||
byte[] range = Arrays.copyOfRange(bytes, 0, 101);
|
||||
assertThat(out.toByteArray()).isEqualTo(range);
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(bytes);
|
||||
StreamUtils.copyRange(in, out, 0, 100);
|
||||
assertThat(in.available()).isEqualTo(bytes.length - 101);
|
||||
assertThat(out.toByteArray()).isEqualTo(Arrays.copyOfRange(bytes, 0, 101));
|
||||
}
|
||||
|
||||
@Test
|
||||
void copyRangeBeyondBuffer() throws Exception {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(bytes);
|
||||
StreamUtils.copyRange(in, out, 0, 8200);
|
||||
assertThat(in.available()).isEqualTo(1);
|
||||
assertThat(out.toByteArray()).isEqualTo(Arrays.copyOfRange(bytes, 0, 8201));
|
||||
}
|
||||
|
||||
@Test
|
||||
void copyRangeBeyondAvailable() throws Exception {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(bytes);
|
||||
StreamUtils.copyRange(in, out, 0, 8300);
|
||||
assertThat(in.available()).isEqualTo(0);
|
||||
assertThat(out.toByteArray()).isEqualTo(Arrays.copyOfRange(bytes, 0, 8202));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -127,4 +149,5 @@ class StreamUtilsTests {
|
||||
ordered.verify(source).write(bytes, 1, 2);
|
||||
ordered.verify(source, never()).close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+18
-12
@@ -440,24 +440,29 @@ public abstract class ReflectionHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Package up the arguments so that they correctly match what is expected in requiredParameterTypes.
|
||||
* <p>For example, if requiredParameterTypes is {@code (int, String[])} because the second parameter
|
||||
* was declared {@code String...}, then if arguments is {@code [1,"a","b"]} then it must be
|
||||
* repackaged as {@code [1,new String[]{"a","b"}]} in order to match the expected types.
|
||||
* Package up the supplied {@code args} so that they correctly match what is
|
||||
* expected in {@code requiredParameterTypes}.
|
||||
* <p>For example, if {@code requiredParameterTypes} is {@code (int, String[])}
|
||||
* because the second parameter was declared as {@code String...}, then if
|
||||
* {@code args} is {@code [1, "a", "b"]} it must be repackaged as
|
||||
* {@code [1, new String[] {"a", "b"}]} in order to match the expected types.
|
||||
* @param requiredParameterTypes the types of the parameters for the invocation
|
||||
* @param args the arguments to be setup ready for the invocation
|
||||
* @return a repackaged array of arguments where any varargs setup has been done
|
||||
* @param args the arguments to be set up for the invocation
|
||||
* @return a repackaged array of arguments where any varargs setup has performed
|
||||
*/
|
||||
public static Object[] setupArgumentsForVarargsInvocation(Class<?>[] requiredParameterTypes, Object... args) {
|
||||
// Check if array already built for final argument
|
||||
Assert.notEmpty(requiredParameterTypes, "Required parameter types array must not be empty");
|
||||
|
||||
int parameterCount = requiredParameterTypes.length;
|
||||
Class<?> lastRequiredParameterType = requiredParameterTypes[parameterCount - 1];
|
||||
Assert.isTrue(lastRequiredParameterType.isArray(),
|
||||
"The last required parameter type must be an array to support varargs invocation");
|
||||
|
||||
int argumentCount = args.length;
|
||||
Object lastArgument = (argumentCount > 0 ? args[argumentCount - 1] : null);
|
||||
|
||||
// Check if repackaging is needed...
|
||||
if (parameterCount != args.length ||
|
||||
requiredParameterTypes[parameterCount - 1] !=
|
||||
(args[argumentCount - 1] != null ? args[argumentCount - 1].getClass() : null)) {
|
||||
|
||||
if (parameterCount != argumentCount || !lastRequiredParameterType.isInstance(lastArgument)) {
|
||||
// Create an array for the leading arguments plus the varargs array argument.
|
||||
Object[] newArgs = new Object[parameterCount];
|
||||
// Copy all leading arguments to the new array, omitting the varargs array argument.
|
||||
@@ -469,7 +474,7 @@ public abstract class ReflectionHelper {
|
||||
if (argumentCount >= parameterCount) {
|
||||
varargsArraySize = argumentCount - (parameterCount - 1);
|
||||
}
|
||||
Class<?> componentType = requiredParameterTypes[parameterCount - 1].componentType();
|
||||
Class<?> componentType = lastRequiredParameterType.componentType();
|
||||
Object varargsArray = Array.newInstance(componentType, varargsArraySize);
|
||||
for (int i = 0; i < varargsArraySize; i++) {
|
||||
Array.set(varargsArray, i, args[parameterCount - 1 + i]);
|
||||
@@ -478,6 +483,7 @@ public abstract class ReflectionHelper {
|
||||
newArgs[newArgs.length - 1] = varargsArray;
|
||||
return newArgs;
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
|
||||
+25
-10
@@ -4221,16 +4221,27 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||
assertThat(tc.s).isEqualTo("aaabbbccc");
|
||||
tc.reset();
|
||||
|
||||
// TODO Fails related to conversion service converting a String[] to satisfy Object...
|
||||
// expression = parser.parseExpression("sixteen(stringArray)");
|
||||
// assertCantCompile(expression);
|
||||
// expression.getValue(tc);
|
||||
// assertEquals("aaabbbccc", tc.s);
|
||||
// assertCanCompile(expression);
|
||||
// tc.reset();
|
||||
// expression.getValue(tc);
|
||||
// assertEquals("aaabbbccc", tc.s);
|
||||
// tc.reset();
|
||||
expression = parser.parseExpression("sixteen(seventeen)");
|
||||
assertCantCompile(expression);
|
||||
expression.getValue(tc);
|
||||
assertThat(tc.s).isEqualTo("aaabbbccc");
|
||||
assertCanCompile(expression);
|
||||
tc.reset();
|
||||
// see TODO below
|
||||
// expression.getValue(tc);
|
||||
// assertThat(tc.s).isEqualTo("aaabbbccc");
|
||||
// tc.reset();
|
||||
|
||||
// TODO Determine why the String[] is passed as the first element of the Object... varargs array instead of the entire varargs array.
|
||||
// expression = parser.parseExpression("sixteen(stringArray)");
|
||||
// assertCantCompile(expression);
|
||||
// expression.getValue(tc);
|
||||
// assertThat(tc.s).isEqualTo("aaabbbccc");
|
||||
// assertCanCompile(expression);
|
||||
// tc.reset();
|
||||
// expression.getValue(tc);
|
||||
// assertThat(tc.s).isEqualTo("aaabbbccc");
|
||||
// tc.reset();
|
||||
|
||||
// varargs int
|
||||
expression = parser.parseExpression("twelve(1,2,3)");
|
||||
@@ -6089,6 +6100,10 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String[] seventeen() {
|
||||
return new String[] { "aaa", "bbb", "ccc" };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+67
-8
@@ -40,6 +40,8 @@ import org.springframework.expression.spel.support.ReflectionHelper.ArgumentsMat
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.InstanceOfAssertFactories.array;
|
||||
import static org.springframework.expression.spel.support.ReflectionHelper.ArgumentsMatchKind.CLOSE;
|
||||
import static org.springframework.expression.spel.support.ReflectionHelper.ArgumentsMatchKind.EXACT;
|
||||
import static org.springframework.expression.spel.support.ReflectionHelper.ArgumentsMatchKind.REQUIRES_CONVERSION;
|
||||
@@ -251,15 +253,72 @@ class ReflectionHelperTests extends AbstractExpressionTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void setupArgumentsForVarargsInvocation() {
|
||||
Object[] newArray = ReflectionHelper.setupArgumentsForVarargsInvocation(
|
||||
new Class<?>[] {String[].class}, "a", "b", "c");
|
||||
void setupArgumentsForVarargsInvocationPreconditions() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ReflectionHelper.setupArgumentsForVarargsInvocation(new Class[] {}, "a"))
|
||||
.withMessage("Required parameter types array must not be empty");
|
||||
|
||||
assertThat(newArray).hasSize(1);
|
||||
Object firstParam = newArray[0];
|
||||
assertThat(firstParam.getClass().componentType()).isEqualTo(String.class);
|
||||
Object[] firstParamArray = (Object[]) firstParam;
|
||||
assertThat(firstParamArray).containsExactly("a", "b", "c");
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ReflectionHelper.setupArgumentsForVarargsInvocation(
|
||||
new Class<?>[] { Integer.class, Integer.class }, 123))
|
||||
.withMessage("The last required parameter type must be an array to support varargs invocation");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setupArgumentsForVarargsInvocation() {
|
||||
Object[] newArray;
|
||||
|
||||
newArray = ReflectionHelper.setupArgumentsForVarargsInvocation(new Class<?>[] { String[].class }, "a", "b", "c");
|
||||
assertThat(newArray)
|
||||
.singleElement()
|
||||
.asInstanceOf(array(String[].class))
|
||||
.containsExactly("a", "b", "c");
|
||||
|
||||
newArray = ReflectionHelper.setupArgumentsForVarargsInvocation(new Class<?>[] { Object[].class }, "a", "b", "c");
|
||||
assertThat(newArray)
|
||||
.singleElement()
|
||||
.asInstanceOf(array(Object[].class))
|
||||
.containsExactly("a", "b", "c");
|
||||
|
||||
newArray = ReflectionHelper.setupArgumentsForVarargsInvocation(
|
||||
new Class<?>[] { Integer.class, Integer.class, String[].class }, 123, 456, "a", "b", "c");
|
||||
assertThat(newArray).satisfiesExactly(
|
||||
one -> assertThat(one).isEqualTo(123),
|
||||
two -> assertThat(two).isEqualTo(456),
|
||||
three -> assertThat(three).asInstanceOf(array(String[].class)).containsExactly("a", "b", "c"));
|
||||
|
||||
newArray = ReflectionHelper.setupArgumentsForVarargsInvocation(new Class<?>[] { String[].class });
|
||||
assertThat(newArray)
|
||||
.singleElement()
|
||||
.asInstanceOf(array(String[].class))
|
||||
.isEmpty();
|
||||
|
||||
newArray = ReflectionHelper.setupArgumentsForVarargsInvocation(
|
||||
new Class<?>[] { String[].class }, new Object[] { new String[] { "a", "b", "c" } });
|
||||
assertThat(newArray)
|
||||
.singleElement()
|
||||
.asInstanceOf(array(String[].class))
|
||||
.containsExactly("a", "b", "c");
|
||||
|
||||
newArray = ReflectionHelper.setupArgumentsForVarargsInvocation(
|
||||
new Class<?>[] { Object[].class }, new Object[] { new String[] { "a", "b", "c" } });
|
||||
assertThat(newArray)
|
||||
.singleElement()
|
||||
.asInstanceOf(array(Object[].class))
|
||||
.containsExactly("a", "b", "c");
|
||||
|
||||
newArray = ReflectionHelper.setupArgumentsForVarargsInvocation(new Class<?>[] { String[].class }, "a");
|
||||
assertThat(newArray)
|
||||
.singleElement()
|
||||
.asInstanceOf(array(String[].class))
|
||||
.containsExactly("a");
|
||||
|
||||
newArray = ReflectionHelper.setupArgumentsForVarargsInvocation(new Class<?>[] { String[].class }, new Object[] { null });
|
||||
assertThat(newArray)
|
||||
.singleElement()
|
||||
.asInstanceOf(array(String[].class))
|
||||
.singleElement()
|
||||
.isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+2
-2
@@ -433,12 +433,12 @@ public class TableMetaDataContext {
|
||||
|
||||
private final boolean quoting;
|
||||
|
||||
public QuoteHandler(@Nullable String identifierQuoteString) {
|
||||
QuoteHandler(@Nullable String identifierQuoteString) {
|
||||
this.identifierQuoteString = identifierQuoteString;
|
||||
this.quoting = StringUtils.hasText(identifierQuoteString);
|
||||
}
|
||||
|
||||
public void appendTo(StringBuilder stringBuilder, @Nullable String item) {
|
||||
void appendTo(StringBuilder stringBuilder, @Nullable String item) {
|
||||
if (this.quoting) {
|
||||
stringBuilder.append(this.identifierQuoteString)
|
||||
.append(item).append(this.identifierQuoteString);
|
||||
|
||||
+4
-4
@@ -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.
|
||||
@@ -24,9 +24,9 @@ import org.springframework.aot.hint.RuntimeHintsRegistrar;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* {@link RuntimeHintsRegistrar} implementation that registers reflection hints for
|
||||
* {@code EmbeddedDataSourceProxy#shutdown} in order to allow it to be used as a bean
|
||||
* destroy method.
|
||||
* {@link RuntimeHintsRegistrar} implementation that registers reflection hints
|
||||
* for {@code EmbeddedDataSourceProxy#shutdown} in order to allow it to be used
|
||||
* as a bean destroy method.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 6.0
|
||||
|
||||
+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.
|
||||
@@ -32,7 +32,7 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* The default implementation of Spring's {@link SqlRowSet} interface, wrapping a
|
||||
* The common implementation of Spring's {@link SqlRowSet} interface, wrapping a
|
||||
* {@link java.sql.ResultSet}, catching any {@link SQLException SQLExceptions} and
|
||||
* translating them to a corresponding Spring {@link InvalidResultSetAccessException}.
|
||||
*
|
||||
|
||||
+6
-5
@@ -39,9 +39,9 @@ import static org.mockito.Mockito.mock;
|
||||
*/
|
||||
class ResultSetWrappingRowSetTests {
|
||||
|
||||
private ResultSet resultSet = mock();
|
||||
private final ResultSet resultSet = mock();
|
||||
|
||||
private ResultSetWrappingSqlRowSet rowSet = new ResultSetWrappingSqlRowSet(resultSet);
|
||||
private final ResultSetWrappingSqlRowSet rowSet = new ResultSetWrappingSqlRowSet(resultSet);
|
||||
|
||||
|
||||
@Test
|
||||
@@ -198,6 +198,7 @@ class ResultSetWrappingRowSetTests {
|
||||
doTest(rset, rowset, "test", true);
|
||||
}
|
||||
|
||||
|
||||
private void doTest(Method rsetMethod, Method rowsetMethod, Object arg, Object ret) throws Exception {
|
||||
if (arg instanceof String) {
|
||||
given(resultSet.findColumn((String) arg)).willReturn(1);
|
||||
@@ -207,9 +208,9 @@ class ResultSetWrappingRowSetTests {
|
||||
given(rsetMethod.invoke(resultSet, arg)).willReturn(ret).willThrow(new SQLException("test"));
|
||||
}
|
||||
rowsetMethod.invoke(rowSet, arg);
|
||||
assertThatExceptionOfType(InvocationTargetException.class).isThrownBy(() ->
|
||||
rowsetMethod.invoke(rowSet, arg)).
|
||||
satisfies(ex -> assertThat(ex.getTargetException()).isExactlyInstanceOf(InvalidResultSetAccessException.class));
|
||||
assertThatExceptionOfType(InvocationTargetException.class)
|
||||
.isThrownBy(() -> rowsetMethod.invoke(rowSet, arg))
|
||||
.satisfies(ex -> assertThat(ex.getTargetException()).isExactlyInstanceOf(InvalidResultSetAccessException.class));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -189,11 +189,6 @@ public class JmsMessagingTemplate extends AbstractMessagingTemplate<Destination>
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void convertAndSend(Object payload) throws MessagingException {
|
||||
convertAndSend(payload, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void convertAndSend(Object payload, @Nullable MessagePostProcessor postProcessor) throws MessagingException {
|
||||
Destination defaultDestination = getDefaultDestination();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2017 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.
|
||||
@@ -290,7 +290,7 @@ public interface JmsOperations {
|
||||
* <p>This method should be used carefully, since it will block the thread
|
||||
* until the message becomes available or until the timeout value is exceeded.
|
||||
* <p>This will only work with a default destination specified!
|
||||
* @return the message produced for the consumer or {@code null} if the timeout expires.
|
||||
* @return the message produced for the consumer, or {@code null} if the timeout expires
|
||||
* @throws JmsException checked JMSException converted to unchecked
|
||||
*/
|
||||
@Nullable
|
||||
@@ -303,7 +303,7 @@ public interface JmsOperations {
|
||||
* <p>This method should be used carefully, since it will block the thread
|
||||
* until the message becomes available or until the timeout value is exceeded.
|
||||
* @param destination the destination to receive a message from
|
||||
* @return the message produced for the consumer or {@code null} if the timeout expires.
|
||||
* @return the message produced for the consumer, or {@code null} if the timeout expires
|
||||
* @throws JmsException checked JMSException converted to unchecked
|
||||
*/
|
||||
@Nullable
|
||||
@@ -317,7 +317,7 @@ public interface JmsOperations {
|
||||
* until the message becomes available or until the timeout value is exceeded.
|
||||
* @param destinationName the name of the destination to send this message to
|
||||
* (to be resolved to an actual destination by a DestinationResolver)
|
||||
* @return the message produced for the consumer or {@code null} if the timeout expires.
|
||||
* @return the message produced for the consumer, or {@code null} if the timeout expires
|
||||
* @throws JmsException checked JMSException converted to unchecked
|
||||
*/
|
||||
@Nullable
|
||||
@@ -332,7 +332,7 @@ public interface JmsOperations {
|
||||
* <p>This will only work with a default destination specified!
|
||||
* @param messageSelector the JMS message selector expression (or {@code null} if none).
|
||||
* See the JMS specification for a detailed definition of selector expressions.
|
||||
* @return the message produced for the consumer or {@code null} if the timeout expires.
|
||||
* @return the message produced for the consumer, or {@code null} if the timeout expires
|
||||
* @throws JmsException checked JMSException converted to unchecked
|
||||
*/
|
||||
@Nullable
|
||||
@@ -347,7 +347,7 @@ public interface JmsOperations {
|
||||
* @param destination the destination to receive a message from
|
||||
* @param messageSelector the JMS message selector expression (or {@code null} if none).
|
||||
* See the JMS specification for a detailed definition of selector expressions.
|
||||
* @return the message produced for the consumer or {@code null} if the timeout expires.
|
||||
* @return the message produced for the consumer, or {@code null} if the timeout expires
|
||||
* @throws JmsException checked JMSException converted to unchecked
|
||||
*/
|
||||
@Nullable
|
||||
@@ -363,7 +363,7 @@ public interface JmsOperations {
|
||||
* (to be resolved to an actual destination by a DestinationResolver)
|
||||
* @param messageSelector the JMS message selector expression (or {@code null} if none).
|
||||
* See the JMS specification for a detailed definition of selector expressions.
|
||||
* @return the message produced for the consumer or {@code null} if the timeout expires.
|
||||
* @return the message produced for the consumer, or {@code null} if the timeout expires
|
||||
* @throws JmsException checked JMSException converted to unchecked
|
||||
*/
|
||||
@Nullable
|
||||
|
||||
+1
@@ -344,6 +344,7 @@ public abstract class AbstractPollingMessageListenerContainer extends AbstractMe
|
||||
}
|
||||
catch (Throwable throwable) {
|
||||
observation.error(throwable);
|
||||
throw throwable;
|
||||
}
|
||||
// Rethrow JMSException to indicate an infrastructure problem
|
||||
// that may have to trigger recovery...
|
||||
|
||||
+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.
|
||||
@@ -168,8 +168,7 @@ public abstract class AbstractMessageSendingTemplate<D> implements MessageSendin
|
||||
|
||||
Map<String, Object> headersToUse = processHeadersToSend(headers);
|
||||
if (headersToUse != null) {
|
||||
messageHeaders = (headersToUse instanceof MessageHeaders _messageHeaders ?
|
||||
_messageHeaders : new MessageHeaders(headersToUse));
|
||||
messageHeaders = (headersToUse instanceof MessageHeaders mh ? mh : new MessageHeaders(headersToUse));
|
||||
}
|
||||
|
||||
MessageConverter converter = getMessageConverter();
|
||||
|
||||
+5
-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.
|
||||
@@ -83,7 +83,10 @@ public class MessageMappingReflectiveProcessor implements ReflectiveProcessor {
|
||||
for (Parameter parameter : method.getParameters()) {
|
||||
MethodParameter methodParameter = MethodParameter.forParameter(parameter);
|
||||
if (Message.class.isAssignableFrom(methodParameter.getParameterType())) {
|
||||
this.bindingRegistrar.registerReflectionHints(hints, getMessageType(methodParameter));
|
||||
Type messageType = getMessageType(methodParameter);
|
||||
if (messageType != null) {
|
||||
this.bindingRegistrar.registerReflectionHints(hints, messageType);
|
||||
}
|
||||
}
|
||||
else if (couldBePayload(methodParameter)) {
|
||||
this.bindingRegistrar.registerReflectionHints(hints, methodParameter.getGenericParameterType());
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user