mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
127 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e354390837 | |||
| c53132ecdf | |||
| 03c3ec1577 | |||
| c88bfc54c9 | |||
| a96558c965 | |||
| 1612b7c5db | |||
| 01acb80501 | |||
| f4438ce9e3 | |||
| 5033b9d3c5 | |||
| d733023a29 | |||
| df67c1cf2d | |||
| 05814f7a42 | |||
| ba2bb08589 | |||
| 141df5291d | |||
| dcfe33f427 | |||
| 3a61460f91 | |||
| ee284f2ee6 | |||
| 2591cab561 | |||
| 8456cd1e74 | |||
| 7699b4af9c | |||
| 6c5de48059 | |||
| 9f678ce698 | |||
| 467a484df6 | |||
| 83efe8cff4 | |||
| 2c83144946 | |||
| c165dd5e0e | |||
| 449b85f446 | |||
| c2a66e723f | |||
| 2ee34a5632 | |||
| 1bc82d241a | |||
| 80e7ee321e | |||
| ecd3dd8883 | |||
| 332953c9a4 | |||
| 1cdd56bf02 | |||
| 3041071269 | |||
| 2da821389c | |||
| d484e4f3ff | |||
| 74dc61b8c4 | |||
| 717358b56b | |||
| 836634c47f | |||
| a6f6ecfe6c | |||
| e3da26ebbd | |||
| e1c008f5a3 | |||
| cb849a7071 | |||
| 3e37279db6 | |||
| b3264ec2a8 | |||
| df860fd3cd | |||
| 636523a2f5 | |||
| a19b51b7e0 | |||
| fbdece6759 | |||
| 64d42fefda | |||
| 447cfa18e9 | |||
| ec3d9d6253 | |||
| 5cd2cb38e1 | |||
| e9fb5eb38a | |||
| 5a858915ea | |||
| d85a020e4e | |||
| 0cc79ba366 | |||
| 1e29911292 | |||
| 931686a5ee | |||
| bf715ac23e | |||
| b213344d25 | |||
| 0a48984fab | |||
| cbdd107799 | |||
| d17601e01c | |||
| 5b387615c6 | |||
| 0e3e34bee0 | |||
| 4745c7cf3c | |||
| 275fb52ad6 | |||
| 7f9aa39748 | |||
| c788554b1d | |||
| 9e8c64011d | |||
| ebb8e34570 | |||
| 9ba954c330 | |||
| ba52164373 | |||
| 977582fced | |||
| 64721b3bc0 | |||
| 02f0f92a72 | |||
| db9e938ec4 | |||
| 3a4315bf16 | |||
| 33fe8d29c1 | |||
| b741632e99 | |||
| cd208797e2 | |||
| 764336f0f2 | |||
| 4903fee939 | |||
| 6978f0a398 | |||
| f62519bb55 | |||
| 0e2af5d113 | |||
| ed7c3d737c | |||
| 01b24f2644 | |||
| 4a4cf8a787 | |||
| 55181fa1c9 | |||
| 300ae841ce | |||
| c248f94e5a | |||
| 19d5ec6781 | |||
| c0b71f8999 | |||
| 5d214c2624 | |||
| 6d710d482a | |||
| ed28390d24 | |||
| e2085063f6 | |||
| edda4731e1 | |||
| 9fa2d7d190 | |||
| c30427fd4e | |||
| 1d908f1847 | |||
| 37b076be51 | |||
| a9453a5959 | |||
| 3781ba223e | |||
| f11a1e6f82 | |||
| ffc785471b | |||
| 6e2fbfe108 | |||
| 968e037503 | |||
| f0a9f649c1 | |||
| 600d6c6fc0 | |||
| 7a55ce48a9 | |||
| 5df9fd4eff | |||
| da13a24604 | |||
| 67e88f3c20 | |||
| 24e66b63d1 | |||
| f3832c7262 | |||
| 16e99f289c | |||
| 642e554c52 | |||
| 4f6304707d | |||
| 3c112703d9 | |||
| 8c44a61033 | |||
| 5e338ef1b8 | |||
| 445da24631 | |||
| 6d97791f5e |
@@ -21,7 +21,7 @@ jobs:
|
||||
toolchain: false
|
||||
- version: 21
|
||||
toolchain: true
|
||||
- version: 23
|
||||
- version: 25
|
||||
toolchain: true
|
||||
exclude:
|
||||
- os:
|
||||
|
||||
+2
-2
@@ -86,7 +86,7 @@ configure([rootProject] + javaProjects) { project ->
|
||||
ext.javadocLinks = [
|
||||
"https://docs.oracle.com/en/java/javase/17/docs/api/",
|
||||
"https://jakarta.ee/specifications/platform/9/apidocs/",
|
||||
"https://docs.jboss.org/hibernate/orm/5.6/javadocs/",
|
||||
"https://docs.hibernate.org/orm/5.6/javadocs/",
|
||||
"https://www.quartz-scheduler.org/api/2.3.0/",
|
||||
"https://fasterxml.github.io/jackson-core/javadoc/2.14/",
|
||||
"https://fasterxml.github.io/jackson-databind/javadoc/2.14/",
|
||||
@@ -97,7 +97,7 @@ configure([rootProject] + javaProjects) { project ->
|
||||
// TODO Uncomment link to JUnit 5 docs once we execute Gradle with Java 18+.
|
||||
// See https://github.com/spring-projects/spring-framework/issues/27497
|
||||
//
|
||||
// "https://junit.org/junit5/docs/5.13.3/api/",
|
||||
// "https://junit.org/junit5/docs/5.14.0/api/",
|
||||
"https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/",
|
||||
//"https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/",
|
||||
"https://r2dbc.io/spec/1.0.0.RELEASE/api/",
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
[[beans-classpath-scanning]]
|
||||
= Classpath Scanning and Managed Components
|
||||
|
||||
Most examples in this chapter use XML to specify the configuration metadata that produces
|
||||
each `BeanDefinition` within the Spring container. The previous section
|
||||
(xref:core/beans/annotation-config.adoc[Annotation-based Container Configuration]) demonstrates how to provide a lot of the configuration
|
||||
metadata through source-level annotations. Even in those examples, however, the "base"
|
||||
bean definitions are explicitly defined in the XML file, while the annotations drive only
|
||||
the dependency injection. This section describes an option for implicitly detecting the
|
||||
candidate components by scanning the classpath. Candidate components are classes that
|
||||
match against a filter criteria and have a corresponding bean definition registered with
|
||||
the container. This removes the need to use XML to perform bean registration. Instead, you
|
||||
can use annotations (for example, `@Component`), AspectJ type expressions, or your own
|
||||
Most examples in this chapter use XML to specify the configuration metadata that
|
||||
produces each `BeanDefinition` within the Spring container. The previous section
|
||||
(xref:core/beans/annotation-config.adoc[Annotation-based Container Configuration])
|
||||
demonstrates how to provide a lot of the configuration metadata through source-level
|
||||
annotations. Even in those examples, however, the "base" bean definitions are explicitly
|
||||
defined in the XML file, while the annotations drive only the dependency injection.
|
||||
|
||||
This section describes an option for implicitly detecting the candidate components by
|
||||
scanning the classpath. Candidate components are classes that match against a filter
|
||||
criteria and have a corresponding bean definition registered with the container.
|
||||
This removes the need to use XML to perform bean registration. Instead, you can use
|
||||
annotations (for example, `@Component`), AspectJ type expressions, or your own
|
||||
custom filter criteria to select which classes have bean definitions registered with
|
||||
the container.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
You can define beans using Java rather than using XML files. Take a look at the
|
||||
`@Configuration`, `@Bean`, `@Import`, and `@DependsOn` annotations for examples of how to
|
||||
use these features.
|
||||
`@Configuration`, `@Bean`, `@Import`, and `@DependsOn` annotations for examples
|
||||
of how to use these features.
|
||||
====
|
||||
|
||||
|
||||
@@ -830,10 +832,10 @@ definitions, there is no notion of bean definition inheritance, and inheritance
|
||||
hierarchies at the class level are irrelevant for metadata purposes.
|
||||
|
||||
For details on web-specific scopes such as "`request`" or "`session`" in a Spring context,
|
||||
see xref:core/beans/factory-scopes.adoc#beans-factory-scopes-other[Request, Session, Application, and WebSocket Scopes]. As with the pre-built annotations for those scopes,
|
||||
you may also compose your own scoping annotations by using Spring's meta-annotation
|
||||
approach: for example, a custom annotation meta-annotated with `@Scope("prototype")`,
|
||||
possibly also declaring a custom scoped-proxy mode.
|
||||
see xref:core/beans/factory-scopes.adoc#beans-factory-scopes-other[Request, Session, Application, and WebSocket Scopes].
|
||||
As with the pre-built annotations for those scopes, you may also compose your own scoping
|
||||
annotations by using Spring's meta-annotation approach: for example, a custom annotation
|
||||
meta-annotated with `@Scope("prototype")`, possibly also declaring a custom scoped-proxy mode.
|
||||
|
||||
NOTE: To provide a custom strategy for scope resolution rather than relying on the
|
||||
annotation-based approach, you can implement the
|
||||
@@ -875,7 +877,8 @@ Kotlin::
|
||||
----
|
||||
|
||||
When using certain non-singleton scopes, it may be necessary to generate proxies for the
|
||||
scoped objects. The reasoning is described in xref:core/beans/factory-scopes.adoc#beans-factory-scopes-other-injection[Scoped Beans as Dependencies].
|
||||
scoped objects. The reasoning is described in
|
||||
xref:core/beans/factory-scopes.adoc#beans-factory-scopes-other-injection[Scoped Beans as Dependencies].
|
||||
For this purpose, a scoped-proxy attribute is available on the component-scan
|
||||
element. The three possible values are: `no`, `interfaces`, and `targetClass`. For example,
|
||||
the following configuration results in standard JDK dynamic proxies:
|
||||
|
||||
+14
-18
@@ -85,11 +85,11 @@ element. The following example shows how to use it:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
<bean id="theTargetBean" class="..."/>
|
||||
<bean id="collaborator" class="..." />
|
||||
|
||||
<bean id="theClientBean" class="...">
|
||||
<bean id="client" class="...">
|
||||
<property name="targetName">
|
||||
<idref bean="theTargetBean"/>
|
||||
<idref bean="collaborator" />
|
||||
</property>
|
||||
</bean>
|
||||
----
|
||||
@@ -99,28 +99,24 @@ following snippet:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
<bean id="theTargetBean" class="..." />
|
||||
<bean id="collaborator" class="..." />
|
||||
|
||||
<bean id="theClientBean" class="...">
|
||||
<property name="targetName" ref="theTargetBean"/>
|
||||
<bean id="client" class="...">
|
||||
<property name="targetName" value="collaborator" />
|
||||
</bean>
|
||||
----
|
||||
|
||||
The first form is preferable to the second, because using the `idref` tag lets the
|
||||
container validate at deployment time that the referenced, named bean actually
|
||||
exists. In the second variation, no validation is performed on the value that is passed
|
||||
to the `targetName` property of the `client` bean. Typos are only discovered (with most
|
||||
container validate at deployment time that the referenced, named bean actually exists. In
|
||||
the second variation, no validation is performed on the value that is passed to the
|
||||
`targetName` property of the `client` bean. Typos are therefore only discovered (with most
|
||||
likely fatal results) when the `client` bean is actually instantiated. If the `client`
|
||||
bean is a xref:core/beans/factory-scopes.adoc[prototype] bean, this typo and the resulting exception
|
||||
may only be discovered long after the container is deployed.
|
||||
bean is a xref:core/beans/factory-scopes.adoc[prototype] bean, this typo and the resulting
|
||||
exception may only be discovered long after the container is deployed.
|
||||
|
||||
NOTE: The `local` attribute on the `idref` element is no longer supported in the 4.0 beans
|
||||
XSD, since it does not provide value over a regular `bean` reference any more. Change
|
||||
your existing `idref local` references to `idref bean` when upgrading to the 4.0 schema.
|
||||
|
||||
A common place (at least in versions earlier than Spring 2.0) where the `<idref/>` element
|
||||
brings value is in the configuration of xref:core/aop-api/pfb.adoc#aop-pfb-1[AOP interceptors] in a
|
||||
`ProxyFactoryBean` bean definition. Using `<idref/>` elements when you specify the
|
||||
NOTE: A common place (at least in versions earlier than Spring 2.0) where the `<idref/>`
|
||||
element brings value is in the configuration of xref:core/aop-api/pfb.adoc#aop-pfb-1[AOP interceptors]
|
||||
in a `ProxyFactoryBean` bean definition. Using `<idref/>` elements when you specify the
|
||||
interceptor names prevents you from misspelling an interceptor ID.
|
||||
|
||||
|
||||
|
||||
+11
@@ -77,6 +77,17 @@ exactly one candidate bean exists.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
As stated in the documentation for Mockito, there are times when using `Mockito.when()` is
|
||||
inappropriate for stubbing a spy – for example, if calling a real method on a spy results
|
||||
in undesired side effects.
|
||||
|
||||
To avoid such undesired side effects, consider using
|
||||
`Mockito.doReturn(...).when(spy)...`, `Mockito.doThrow(...).when(spy)...`,
|
||||
`Mockito.doNothing().when(spy)...`, and similar methods.
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Only _singleton_ beans can be overridden. Any attempt to override a non-singleton bean
|
||||
will result in an exception.
|
||||
|
||||
|
||||
@@ -261,7 +261,7 @@ Kotlin::
|
||||
|
||||
This improves on the design of our xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-usage[HtmlUnit test]
|
||||
by leveraging the Page Object Pattern. As we mentioned in
|
||||
xref:testing/mockmvc/htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-why[Why WebDriver and MockMvc?], we can use the Page Object Pattern
|
||||
xref:testing/mockmvc/htmlunit/webdriver.adoc#mockmvc-server-htmlunit-webdriver-why[Why WebDriver and MockMvc?], we can use the Page Object Pattern
|
||||
with HtmlUnit, but it is much easier with WebDriver. Consider the following
|
||||
`CreateMessagePage` implementation:
|
||||
|
||||
|
||||
+23
-28
@@ -2,10 +2,10 @@
|
||||
= Application Events
|
||||
|
||||
The TestContext framework provides support for recording
|
||||
xref:core/beans/context-introduction.adoc#context-functionality-events[application events] published in the
|
||||
`ApplicationContext` so that assertions can be performed against those events within
|
||||
tests. All events published during the execution of a single test are made available via
|
||||
the `ApplicationEvents` API which allows you to process the events as a
|
||||
xref:core/beans/context-introduction.adoc#context-functionality-events[application events]
|
||||
published in the `ApplicationContext` so that assertions can be performed against those
|
||||
events within tests. All events published during the execution of a single test are made
|
||||
available via the `ApplicationEvents` API which allows you to process the events as a
|
||||
`java.util.Stream`.
|
||||
|
||||
To use `ApplicationEvents` in your tests, do the following.
|
||||
@@ -16,16 +16,23 @@ To use `ApplicationEvents` in your tests, do the following.
|
||||
that `ApplicationEventsTestExecutionListener` is registered by default and only needs
|
||||
to be manually registered if you have custom configuration via
|
||||
`@TestExecutionListeners` that does not include the default listeners.
|
||||
* Annotate a field of type `ApplicationEvents` with `@Autowired` and use that instance of
|
||||
`ApplicationEvents` in your test and lifecycle methods (such as `@BeforeEach` and
|
||||
`@AfterEach` methods in JUnit Jupiter).
|
||||
** When using the xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[SpringExtension for JUnit Jupiter], you may declare a method
|
||||
parameter of type `ApplicationEvents` in a test or lifecycle method as an alternative
|
||||
to an `@Autowired` field in the test class.
|
||||
* When using the
|
||||
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[SpringExtension for JUnit Jupiter],
|
||||
declare a method parameter of type `ApplicationEvents` in a `@Test`, `@BeforeEach`, or
|
||||
`@AfterEach` method.
|
||||
** Since `ApplicationEvents` is scoped to the lifecycle of the current test method, this
|
||||
is the recommended approach.
|
||||
* Alternatively, you can annotate a field of type `ApplicationEvents` with `@Autowired`
|
||||
and use that instance of `ApplicationEvents` in your test and lifecycle methods.
|
||||
|
||||
NOTE: `ApplicationEvents` is registered with the `ApplicationContext` as a _resolvable
|
||||
dependency_ which is scoped to the lifecycle of the current test method. Consequently,
|
||||
`ApplicationEvents` cannot be accessed outside the lifecycle of a test method and cannot be
|
||||
`@Autowired` into the constructor of a test class.
|
||||
|
||||
The following test class uses the `SpringExtension` for JUnit Jupiter and
|
||||
{assertj-docs}[AssertJ] to assert the types of application events
|
||||
published while invoking a method in a Spring-managed component:
|
||||
{assertj-docs}[AssertJ] to assert the types of application events published while
|
||||
invoking a method in a Spring-managed component:
|
||||
|
||||
// Don't use "quotes" in the "subs" section because of the asterisks in /* ... */
|
||||
[tabs]
|
||||
@@ -38,16 +45,10 @@ Java::
|
||||
@RecordApplicationEvents // <1>
|
||||
class OrderServiceTests {
|
||||
|
||||
@Autowired
|
||||
OrderService orderService;
|
||||
|
||||
@Autowired
|
||||
ApplicationEvents events; // <2>
|
||||
|
||||
@Test
|
||||
void submitOrder() {
|
||||
void submitOrder(@Autowired OrderService service, ApplicationEvents events) { // <2>
|
||||
// Invoke method in OrderService that publishes an event
|
||||
orderService.submitOrder(new Order(/* ... */));
|
||||
service.submitOrder(new Order(/* ... */));
|
||||
// Verify that an OrderSubmitted event was published
|
||||
long numEvents = events.stream(OrderSubmitted.class).count(); // <3>
|
||||
assertThat(numEvents).isEqualTo(1);
|
||||
@@ -66,16 +67,10 @@ Kotlin::
|
||||
@RecordApplicationEvents // <1>
|
||||
class OrderServiceTests {
|
||||
|
||||
@Autowired
|
||||
lateinit var orderService: OrderService
|
||||
|
||||
@Autowired
|
||||
lateinit var events: ApplicationEvents // <2>
|
||||
|
||||
@Test
|
||||
fun submitOrder() {
|
||||
fun submitOrder(@Autowired service: OrderService, events: ApplicationEvents) { // <2>
|
||||
// Invoke method in OrderService that publishes an event
|
||||
orderService.submitOrder(Order(/* ... */))
|
||||
service.submitOrder(Order(/* ... */))
|
||||
// Verify that an OrderSubmitted event was published
|
||||
val numEvents = events.stream(OrderSubmitted::class).count() // <3>
|
||||
assertThat(numEvents).isEqualTo(1)
|
||||
|
||||
@@ -294,7 +294,28 @@ allPartsEvents.windowUntil(PartEvent::isLast)
|
||||
----
|
||||
======
|
||||
|
||||
Note that the body contents of the `PartEvent` objects must be completely consumed, relayed, or released to avoid memory leaks.
|
||||
NOTE: The body contents of the `PartEvent` objects must be completely consumed, relayed, or released to avoid memory leaks.
|
||||
|
||||
The following shows how to bind request parameters, including an optional `DataBinder` customization:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java]
|
||||
----
|
||||
Pet pet = request.bind(Pet.class, dataBinder -> dataBinder.setAllowedFields("name"));
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin]
|
||||
----
|
||||
val pet = request.bind(Pet::class.java, {dataBinder -> dataBinder.setAllowedFields("name")})
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
|
||||
[[webflux-fn-response]]
|
||||
=== ServerResponse
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
= WebClient
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
Spring WebFlux includes a client to perform HTTP requests with. `WebClient` has a
|
||||
functional, fluent API based on Reactor, see xref:web-reactive.adoc#webflux-reactive-libraries[Reactive Libraries],
|
||||
Spring WebFlux includes a client to perform HTTP requests. `WebClient` has a
|
||||
functional, fluent API based on Reactor (see xref:web/webflux-reactive-libraries.adoc[Reactive Libraries])
|
||||
which enables declarative composition of asynchronous logic without the need to deal with
|
||||
threads or concurrency. It is fully non-blocking, it supports streaming, and relies on
|
||||
threads or concurrency. It is fully non-blocking, supports streaming, and relies on
|
||||
the same xref:web/webflux/reactive-spring.adoc#webflux-codecs[codecs] that are also used to encode and
|
||||
decode request and response content on the server side.
|
||||
|
||||
`WebClient` needs an HTTP client library to perform requests with. There is built-in
|
||||
`WebClient` needs an HTTP client library to perform requests. There is built-in
|
||||
support for the following:
|
||||
|
||||
* {reactor-github-org}/reactor-netty[Reactor Netty]
|
||||
* {java-api}/java.net.http/java/net/http/HttpClient.html[JDK HttpClient]
|
||||
* https://github.com/jetty-project/jetty-reactive-httpclient[Jetty Reactive HttpClient]
|
||||
* https://hc.apache.org/index.html[Apache HttpComponents]
|
||||
* Others can be plugged via `ClientHttpConnector`.
|
||||
* Others can be plugged in via `ClientHttpConnector`.
|
||||
|
||||
@@ -310,7 +310,7 @@ Java::
|
||||
Flux<String> source = ... ;
|
||||
Mono<Void> output = session.send(source.map(session::textMessage)); <2>
|
||||
|
||||
return Mono.zip(input, output).then(); <3>
|
||||
return input.and(output); <3>
|
||||
}
|
||||
}
|
||||
----
|
||||
@@ -338,7 +338,7 @@ Kotlin::
|
||||
val source: Flux<String> = ...
|
||||
val output = session.send(source.map(session::textMessage)) // <2>
|
||||
|
||||
return Mono.zip(input, output).then() // <3>
|
||||
return input.and(output) // <3>
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
@@ -112,10 +112,11 @@ You can map requests by using glob patterns and wildcards:
|
||||
| `+{name}+`
|
||||
| Matches a path segment and captures it as a variable named "name"
|
||||
| `+"/projects/{project}/versions"+` matches `+"/projects/spring/versions"+` and captures `+project=spring+`
|
||||
`+"/projects/{project}/versions"+` does not match `+"/projects/spring/framework/versions"+` as it captures a single path segment.
|
||||
|
||||
| `+{name:[a-z]+}+`
|
||||
| Matches the regexp `+"[a-z]+"+` as a path variable named "name"
|
||||
| `+"/projects/{project:[a-z]+}/versions"+` matches `+"/projects/spring/versions"+` but not `+"/projects/spring1/versions"+`
|
||||
| `{name:[a-z]+}`
|
||||
| Matches the regexp `[a-z]+` as a path variable named "name"
|
||||
| `/projects/{project:[a-z]+}/versions` matches `/projects/spring/versions` but not `/projects/spring1/versions`
|
||||
|
||||
| `+{*path}+`
|
||||
| Matches zero or more path segments until the end of the path and captures it as a variable named "path"
|
||||
@@ -234,10 +235,13 @@ Kotlin::
|
||||
======
|
||||
--
|
||||
|
||||
URI path patterns can also have embedded `${...}` placeholders that are resolved on startup
|
||||
by using `PropertySourcesPlaceholderConfigurer` against local, system, environment, and
|
||||
other property sources. You can use this, for example, to parameterize a base URL based on
|
||||
some external configuration.
|
||||
URI path patterns can also have:
|
||||
|
||||
- Embedded `${...}` placeholders that are resolved on startup via
|
||||
`PropertySourcesPlaceholderConfigurer` against local, system, environment, and
|
||||
other property sources. This is useful, for example, to parameterize a base URL based on
|
||||
external configuration.
|
||||
- SpEL expressions `#{...}`.
|
||||
|
||||
NOTE: Spring WebFlux uses `PathPattern` and the `PathPatternParser` for URI path matching support.
|
||||
Both classes are located in `spring-web` and are expressly designed for use with HTTP URL
|
||||
|
||||
@@ -15,27 +15,27 @@ See xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] for more de
|
||||
[[webmvc-webclient]]
|
||||
== `WebClient`
|
||||
|
||||
`WebClient` is a reactive client to perform HTTP requests with a fluent API.
|
||||
`WebClient` is a reactive client for making HTTP requests with a fluent API.
|
||||
|
||||
See xref:web/webflux-webclient.adoc[WebClient] for more details.
|
||||
See xref:web/webflux-webclient.adoc[`WebClient`] for more details.
|
||||
|
||||
|
||||
[[webmvc-resttemplate]]
|
||||
== `RestTemplate`
|
||||
|
||||
`RestTemplate` is a synchronous client to perform HTTP requests. It is the original
|
||||
`RestTemplate` is a synchronous client for making HTTP requests. It is the original
|
||||
Spring REST client and exposes a simple, template-method API over underlying HTTP client
|
||||
libraries.
|
||||
|
||||
See xref:integration/rest-clients.adoc[REST Endpoints] for details.
|
||||
See xref:integration/rest-clients.adoc#rest-resttemplate[`RestTemplate`] for details.
|
||||
|
||||
|
||||
[[webmvc-http-interface]]
|
||||
== HTTP Interface
|
||||
|
||||
The Spring Frameworks lets you define an HTTP service as a Java interface with HTTP
|
||||
The Spring Framework lets you define an HTTP service as a Java interface with HTTP
|
||||
exchange methods. You can then generate a proxy that implements this interface and
|
||||
performs the exchanges. This helps to simplify HTTP remote access and provides additional
|
||||
flexibility for to choose an API style such as synchronous or reactive.
|
||||
flexibility for choosing an API style such as synchronous or reactive.
|
||||
|
||||
See xref:integration/rest-clients.adoc#rest-http-interface[REST Endpoints] for details.
|
||||
See xref:integration/rest-clients.adoc#rest-http-interface[HTTP Interface] for details.
|
||||
|
||||
@@ -184,6 +184,26 @@ val map = request.params()
|
||||
----
|
||||
======
|
||||
|
||||
The following shows how to bind request parameters, including an optional `DataBinder` customization:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java]
|
||||
----
|
||||
Pet pet = request.bind(Pet.class, dataBinder -> dataBinder.setAllowedFields("name"));
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin]
|
||||
----
|
||||
val pet = request.bind(Pet::class.java, {dataBinder -> dataBinder.setAllowedFields("name")})
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
[[webmvc-fn-response]]
|
||||
=== ServerResponse
|
||||
|
||||
|
||||
@@ -217,10 +217,13 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
URI path patterns can also have embedded `${...}` placeholders that are resolved on startup
|
||||
by using `PropertySourcesPlaceholderConfigurer` against local, system, environment, and
|
||||
other property sources. You can use this, for example, to parameterize a base URL based on
|
||||
some external configuration.
|
||||
URI path patterns can also have:
|
||||
|
||||
- Embedded `${...}` placeholders that are resolved on startup via
|
||||
`PropertySourcesPlaceholderConfigurer` against local, system, environment, and
|
||||
other property sources. This is useful, for example, to parameterize a base URL based on
|
||||
external configuration.
|
||||
- SpEL expression `#{...}`.
|
||||
|
||||
|
||||
[[mvc-ann-requestmapping-pattern-comparison]]
|
||||
|
||||
@@ -7,31 +7,31 @@ javaPlatform {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.18.4"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.14.9"))
|
||||
api(platform("io.netty:netty-bom:4.1.123.Final"))
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.18.4.1"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.14.12"))
|
||||
api(platform("io.netty:netty-bom:4.1.128.Final"))
|
||||
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
|
||||
api(platform("io.projectreactor:reactor-bom:2024.0.8"))
|
||||
api(platform("io.projectreactor:reactor-bom:2024.0.11"))
|
||||
api(platform("io.rsocket:rsocket-bom:1.1.5"))
|
||||
api(platform("org.apache.groovy:groovy-bom:4.0.27"))
|
||||
api(platform("org.apache.groovy:groovy-bom:4.0.28"))
|
||||
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
|
||||
api(platform("org.assertj:assertj-bom:3.27.3"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.0.23"))
|
||||
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.23"))
|
||||
api(platform("org.assertj:assertj-bom:3.27.6"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.0.28"))
|
||||
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.28"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3"))
|
||||
api(platform("org.junit:junit-bom:5.13.3"))
|
||||
api(platform("org.mockito:mockito-bom:5.18.0"))
|
||||
api(platform("org.junit:junit-bom:5.14.0"))
|
||||
api(platform("org.mockito:mockito-bom:5.20.0"))
|
||||
|
||||
constraints {
|
||||
api("com.fasterxml:aalto-xml:1.3.2")
|
||||
api("com.fasterxml:aalto-xml:1.3.4")
|
||||
api("com.fasterxml.woodstox:woodstox-core:6.7.0")
|
||||
api("com.github.ben-manes.caffeine:caffeine:3.2.1")
|
||||
api("com.github.ben-manes.caffeine:caffeine:3.2.2")
|
||||
api("com.github.librepdf:openpdf:1.3.43")
|
||||
api("com.google.code.findbugs:findbugs:3.0.1")
|
||||
api("com.google.code.findbugs:jsr305:3.0.2")
|
||||
api("com.google.code.gson:gson:2.13.1")
|
||||
api("com.google.protobuf:protobuf-java-util:4.30.2")
|
||||
api("com.google.code.gson:gson:2.13.2")
|
||||
api("com.google.protobuf:protobuf-java-util:4.32.1")
|
||||
api("com.h2database:h2:2.3.232")
|
||||
api("com.jayway.jsonpath:json-path:2.9.0")
|
||||
api("com.oracle.database.jdbc:ojdbc11:21.9.0.0")
|
||||
@@ -53,11 +53,11 @@ dependencies {
|
||||
api("io.r2dbc:r2dbc-h2:1.0.0.RELEASE")
|
||||
api("io.r2dbc:r2dbc-spi-test:1.0.0.RELEASE")
|
||||
api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE")
|
||||
api("io.reactivex.rxjava3:rxjava:3.1.10")
|
||||
api("io.reactivex.rxjava3:rxjava:3.1.12")
|
||||
api("io.smallrye.reactive:mutiny:1.10.0")
|
||||
api("io.undertow:undertow-core:2.3.18.Final")
|
||||
api("io.undertow:undertow-servlet:2.3.18.Final")
|
||||
api("io.undertow:undertow-websockets-jsr:2.3.18.Final")
|
||||
api("io.undertow:undertow-core:2.3.20.Final")
|
||||
api("io.undertow:undertow-servlet:2.3.20.Final")
|
||||
api("io.undertow:undertow-websockets-jsr:2.3.20.Final")
|
||||
api("io.vavr:vavr:0.10.4")
|
||||
api("jakarta.activation:jakarta.activation-api:2.0.1")
|
||||
api("jakarta.annotation:jakarta.annotation-api:2.0.0")
|
||||
@@ -90,17 +90,17 @@ dependencies {
|
||||
api("junit:junit:4.13.2")
|
||||
api("net.sf.jopt-simple:jopt-simple:5.0.4")
|
||||
api("org.apache-extras.beanshell:bsh:2.0b6")
|
||||
api("org.apache.activemq:activemq-broker:5.17.6")
|
||||
api("org.apache.activemq:activemq-kahadb-store:5.17.6")
|
||||
api("org.apache.activemq:activemq-stomp:5.17.6")
|
||||
api("org.apache.activemq:artemis-jakarta-client:2.31.2")
|
||||
api("org.apache.activemq:artemis-junit-5:2.31.2")
|
||||
api("org.apache.activemq:activemq-broker:5.17.7")
|
||||
api("org.apache.activemq:activemq-kahadb-store:5.17.7")
|
||||
api("org.apache.activemq:activemq-stomp:5.17.7")
|
||||
api("org.apache.activemq:artemis-jakarta-client:2.42.0")
|
||||
api("org.apache.activemq:artemis-junit-5:2.42.0")
|
||||
api("org.apache.commons:commons-pool2:2.9.0")
|
||||
api("org.apache.derby:derby:10.16.1.1")
|
||||
api("org.apache.derby:derbyclient:10.16.1.1")
|
||||
api("org.apache.derby:derbytools:10.16.1.1")
|
||||
api("org.apache.httpcomponents.client5:httpclient5:5.5")
|
||||
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.4")
|
||||
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.5")
|
||||
api("org.apache.poi:poi-ooxml:5.2.5")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-core:10.1.28")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.28")
|
||||
@@ -113,9 +113,9 @@ dependencies {
|
||||
api("org.bouncycastle:bcpkix-jdk18on:1.72")
|
||||
api("org.codehaus.jettison:jettison:1.5.4")
|
||||
api("org.crac:crac:1.4.0")
|
||||
api("org.dom4j:dom4j:2.1.4")
|
||||
api("org.easymock:easymock:5.5.0")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.9")
|
||||
api("org.dom4j:dom4j:2.2.0")
|
||||
api("org.easymock:easymock:5.6.0")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.12")
|
||||
api("org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.4")
|
||||
api("org.eclipse:yasson:2.0.4")
|
||||
api("org.ehcache:ehcache:3.10.8")
|
||||
@@ -129,24 +129,24 @@ dependencies {
|
||||
api("org.hibernate:hibernate-core-jakarta:5.6.15.Final")
|
||||
api("org.hibernate:hibernate-validator:7.0.5.Final")
|
||||
api("org.hsqldb:hsqldb:2.7.4")
|
||||
api("org.htmlunit:htmlunit:4.13.0")
|
||||
api("org.htmlunit:htmlunit:4.17.0")
|
||||
api("org.javamoney:moneta:1.4.4")
|
||||
api("org.jruby:jruby:9.4.12.0")
|
||||
api("org.jruby:jruby:9.4.13.0")
|
||||
api("org.junit.support:testng-engine:1.0.5")
|
||||
api("org.mozilla:rhino:1.7.15")
|
||||
api("org.ogce:xpp3:1.1.6")
|
||||
api("org.python:jython-standalone:2.7.4")
|
||||
api("org.quartz-scheduler:quartz:2.3.2")
|
||||
api("org.seleniumhq.selenium:htmlunit3-driver:4.33.0")
|
||||
api("org.seleniumhq.selenium:selenium-java:4.34.0")
|
||||
api("org.seleniumhq.selenium:htmlunit3-driver:4.36.1")
|
||||
api("org.seleniumhq.selenium:selenium-java:4.36.0")
|
||||
api("org.skyscreamer:jsonassert:1.5.3")
|
||||
api("org.slf4j:slf4j-api:2.0.17")
|
||||
api("org.testng:testng:7.11.0")
|
||||
api("org.webjars:underscorejs:1.8.3")
|
||||
api("org.webjars:webjars-locator-core:0.59")
|
||||
api("org.webjars:webjars-locator-lite:1.1.0")
|
||||
api("org.xmlunit:xmlunit-assertj:2.10.3")
|
||||
api("org.xmlunit:xmlunit-matchers:2.10.3")
|
||||
api("org.yaml:snakeyaml:2.4")
|
||||
api("org.xmlunit:xmlunit-assertj:2.10.4")
|
||||
api("org.xmlunit:xmlunit-matchers:2.10.4")
|
||||
api("org.yaml:snakeyaml:2.5")
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
version=6.2.9
|
||||
version=6.2.12
|
||||
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
|
||||
@@ -121,12 +121,12 @@ public abstract class AopConfigUtils {
|
||||
Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
|
||||
|
||||
if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
|
||||
BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
|
||||
if (!cls.getName().equals(apcDefinition.getBeanClassName())) {
|
||||
int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName());
|
||||
BeanDefinition beanDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
|
||||
if (!cls.getName().equals(beanDefinition.getBeanClassName())) {
|
||||
int currentPriority = findPriorityForClass(beanDefinition.getBeanClassName());
|
||||
int requiredPriority = findPriorityForClass(cls);
|
||||
if (currentPriority < requiredPriority) {
|
||||
apcDefinition.setBeanClassName(cls.getName());
|
||||
beanDefinition.setBeanClassName(cls.getName());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -134,8 +134,8 @@ public abstract class AopConfigUtils {
|
||||
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition(cls);
|
||||
beanDefinition.setSource(source);
|
||||
beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE);
|
||||
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE);
|
||||
registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition);
|
||||
return beanDefinition;
|
||||
}
|
||||
|
||||
+1
@@ -117,6 +117,7 @@ public abstract class AbstractAdvisingBeanPostProcessor extends ProxyProcessorSu
|
||||
}
|
||||
proxyFactory.addAdvisor(this.advisor);
|
||||
customizeProxyFactory(proxyFactory);
|
||||
proxyFactory.setPreFiltered(true);
|
||||
|
||||
// Use original ClassLoader if bean class not locally loaded in overriding class loader
|
||||
ClassLoader classLoader = getProxyClassLoader();
|
||||
|
||||
+3
-3
@@ -24,9 +24,9 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Extension of {@link AbstractAutoProxyCreator} which implements {@link BeanFactoryAware},
|
||||
* adds exposure of the original target class for each proxied bean
|
||||
* ({@link AutoProxyUtils#ORIGINAL_TARGET_CLASS_ATTRIBUTE}),
|
||||
* Extension of {@link AbstractAdvisingBeanPostProcessor} which implements
|
||||
* {@link BeanFactoryAware}, adds exposure of the original target class for each
|
||||
* proxied bean ({@link AutoProxyUtils#ORIGINAL_TARGET_CLASS_ATTRIBUTE}),
|
||||
* and participates in an externally enforced target-class mode for any given bean
|
||||
* ({@link AutoProxyUtils#PRESERVE_TARGET_CLASS_ATTRIBUTE}).
|
||||
* This post-processor is therefore aligned with {@link AbstractAutoProxyCreator}.
|
||||
|
||||
@@ -81,13 +81,19 @@ public abstract class ScopedProxyUtils {
|
||||
// Copy autowire settings from original bean definition.
|
||||
proxyDefinition.setAutowireCandidate(targetDefinition.isAutowireCandidate());
|
||||
proxyDefinition.setPrimary(targetDefinition.isPrimary());
|
||||
proxyDefinition.setFallback(targetDefinition.isFallback());
|
||||
if (targetDefinition instanceof AbstractBeanDefinition abd) {
|
||||
proxyDefinition.setDefaultCandidate(abd.isDefaultCandidate());
|
||||
proxyDefinition.copyQualifiersFrom(abd);
|
||||
}
|
||||
|
||||
// The target bean should be ignored in favor of the scoped proxy.
|
||||
targetDefinition.setAutowireCandidate(false);
|
||||
targetDefinition.setPrimary(false);
|
||||
targetDefinition.setFallback(false);
|
||||
if (targetDefinition instanceof AbstractBeanDefinition abd) {
|
||||
abd.setDefaultCandidate(false);
|
||||
}
|
||||
|
||||
// Register the target bean as separate bean in the factory.
|
||||
registry.registerBeanDefinition(targetBeanName, targetDefinition);
|
||||
|
||||
+4
-3
@@ -37,7 +37,6 @@ import static org.mockito.Mockito.verify;
|
||||
* Tests for {@link AsyncExecutionInterceptor}.
|
||||
*
|
||||
* @author Bao Ngo
|
||||
* @since 7.0
|
||||
*/
|
||||
class AsyncExecutionInterceptorTests {
|
||||
|
||||
@@ -62,11 +61,13 @@ class AsyncExecutionInterceptorTests {
|
||||
O run();
|
||||
}
|
||||
|
||||
|
||||
static class FutureRunner implements GenericRunner<Future<Void>> {
|
||||
|
||||
@Override
|
||||
public Future<Void> run() {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
});
|
||||
return CompletableFuture.runAsync(() -> {});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,6 +18,15 @@ package org.springframework.aop.scope;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.BeanDefinitionHolder;
|
||||
import org.springframework.beans.factory.support.AbstractBeanDefinition;
|
||||
import org.springframework.beans.factory.support.AutowireCandidateQualifier;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.GenericBeanDefinition;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.beans.factory.support.SimpleBeanDefinitionRegistry;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
@@ -25,6 +34,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
||||
* Tests for {@link ScopedProxyUtils}.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @author Juergen Hoeller
|
||||
* @since 5.1.10
|
||||
*/
|
||||
class ScopedProxyUtilsTests {
|
||||
@@ -53,15 +63,79 @@ class ScopedProxyUtilsTests {
|
||||
@Test
|
||||
void getOriginalBeanNameForNullTargetBean() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ScopedProxyUtils.getOriginalBeanName(null))
|
||||
.withMessage("bean name 'null' does not refer to the target of a scoped proxy");
|
||||
.isThrownBy(() -> ScopedProxyUtils.getOriginalBeanName(null))
|
||||
.withMessage("bean name 'null' does not refer to the target of a scoped proxy");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOriginalBeanNameForNonScopedTarget() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ScopedProxyUtils.getOriginalBeanName("myBean"))
|
||||
.withMessage("bean name 'myBean' does not refer to the target of a scoped proxy");
|
||||
.isThrownBy(() -> ScopedProxyUtils.getOriginalBeanName("myBean"))
|
||||
.withMessage("bean name 'myBean' does not refer to the target of a scoped proxy");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createScopedProxyTargetAppliesAutowireSettingsToProxyBeanDefinition() {
|
||||
AbstractBeanDefinition targetDefinition = new GenericBeanDefinition();
|
||||
// Opposite of defaults
|
||||
targetDefinition.setAutowireCandidate(false);
|
||||
targetDefinition.setDefaultCandidate(false);
|
||||
targetDefinition.setPrimary(true);
|
||||
targetDefinition.setFallback(true);
|
||||
|
||||
BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry();
|
||||
BeanDefinitionHolder proxyHolder = ScopedProxyUtils.createScopedProxy(
|
||||
new BeanDefinitionHolder(targetDefinition, "myBean"), registry, false);
|
||||
AbstractBeanDefinition proxyBeanDefinition = (AbstractBeanDefinition) proxyHolder.getBeanDefinition();
|
||||
|
||||
assertThat(proxyBeanDefinition.isAutowireCandidate()).isFalse();
|
||||
assertThat(proxyBeanDefinition.isDefaultCandidate()).isFalse();
|
||||
assertThat(proxyBeanDefinition.isPrimary()).isTrue();
|
||||
assertThat(proxyBeanDefinition.isFallback()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createScopedProxyTargetAppliesBeanAttributesToProxyBeanDefinition() {
|
||||
GenericBeanDefinition targetDefinition = new GenericBeanDefinition();
|
||||
// Opposite of defaults
|
||||
targetDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
targetDefinition.setSource("theSource");
|
||||
targetDefinition.addQualifier(new AutowireCandidateQualifier("myQualifier"));
|
||||
|
||||
BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry();
|
||||
BeanDefinitionHolder proxyHolder = ScopedProxyUtils.createScopedProxy(
|
||||
new BeanDefinitionHolder(targetDefinition, "myBean"), registry, false);
|
||||
BeanDefinition proxyBeanDefinition = proxyHolder.getBeanDefinition();
|
||||
|
||||
assertThat(proxyBeanDefinition.getRole()).isEqualTo(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
assertThat(proxyBeanDefinition).isInstanceOf(RootBeanDefinition.class);
|
||||
assertThat(proxyBeanDefinition.getPropertyValues()).hasSize(2);
|
||||
assertThat(proxyBeanDefinition.getPropertyValues().get("proxyTargetClass")).isEqualTo(false);
|
||||
assertThat(proxyBeanDefinition.getPropertyValues().get("targetBeanName")).isEqualTo(
|
||||
ScopedProxyUtils.getTargetBeanName("myBean"));
|
||||
|
||||
RootBeanDefinition rootBeanDefinition = (RootBeanDefinition) proxyBeanDefinition;
|
||||
assertThat(rootBeanDefinition.getQualifiers()).hasSize(1);
|
||||
assertThat(rootBeanDefinition.hasQualifier("myQualifier")).isTrue();
|
||||
assertThat(rootBeanDefinition.getSource()).isEqualTo("theSource");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createScopedProxyTargetCleansAutowireSettingsInTargetDefinition() {
|
||||
AbstractBeanDefinition targetDefinition = new GenericBeanDefinition();
|
||||
targetDefinition.setAutowireCandidate(true);
|
||||
targetDefinition.setDefaultCandidate(true);
|
||||
targetDefinition.setPrimary(true);
|
||||
targetDefinition.setFallback(true);
|
||||
|
||||
BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry();
|
||||
ScopedProxyUtils.createScopedProxy(
|
||||
new BeanDefinitionHolder(targetDefinition, "myBean"), registry, false);
|
||||
|
||||
assertThat(targetDefinition.isAutowireCandidate()).isFalse();
|
||||
assertThat(targetDefinition.isDefaultCandidate()).isFalse();
|
||||
assertThat(targetDefinition.isPrimary()).isFalse();
|
||||
assertThat(targetDefinition.isFallback()).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+28
@@ -19,6 +19,7 @@ package org.springframework.aop.framework
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.LocalDateTime
|
||||
|
||||
/**
|
||||
* Tests for Kotlin support in [CglibAopProxy].
|
||||
@@ -48,6 +49,13 @@ class CglibAopProxyKotlinTests {
|
||||
assertThatThrownBy { proxy.checkedException() }.isInstanceOf(CheckedException::class.java)
|
||||
}
|
||||
|
||||
@Test // gh-35487
|
||||
fun jvmDefault() {
|
||||
val proxyFactory = ProxyFactory()
|
||||
proxyFactory.setTarget(AddressRepo())
|
||||
proxyFactory.proxy
|
||||
}
|
||||
|
||||
|
||||
open class MyKotlinBean {
|
||||
|
||||
@@ -63,4 +71,24 @@ class CglibAopProxyKotlinTests {
|
||||
}
|
||||
|
||||
class CheckedException() : Exception()
|
||||
|
||||
open class AddressRepo(): CrudRepo<Address, Int>
|
||||
|
||||
interface CrudRepo<E : Any, ID : Any> {
|
||||
fun save(e: E): E {
|
||||
return e
|
||||
}
|
||||
fun delete(id: ID): Long {
|
||||
return 0L
|
||||
}
|
||||
}
|
||||
|
||||
data class Address(
|
||||
val id: Int = 0,
|
||||
val street: String,
|
||||
val version: Int = 0,
|
||||
val createdAt: LocalDateTime? = null,
|
||||
val updatedAt: LocalDateTime? = null,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
+3
@@ -294,9 +294,12 @@ public class InstanceSupplierCodeGenerator {
|
||||
|
||||
this.generationContext.getRuntimeHints().reflection().registerMethod(factoryMethod, ExecutableMode.INVOKE);
|
||||
GeneratedMethod getInstanceMethod = generateGetInstanceSupplierMethod(method -> {
|
||||
CodeWarnings codeWarnings = new CodeWarnings();
|
||||
Class<?> suppliedType = ClassUtils.resolvePrimitiveIfNecessary(factoryMethod.getReturnType());
|
||||
codeWarnings.detectDeprecation(suppliedType, factoryMethod);
|
||||
method.addJavadoc("Get the bean instance supplier for '$L'.", beanName);
|
||||
method.addModifiers(PRIVATE_STATIC);
|
||||
codeWarnings.suppress(method);
|
||||
method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, suppliedType));
|
||||
method.addStatement(generateInstanceSupplierForFactoryMethod(
|
||||
factoryMethod, suppliedType, targetClass, factoryMethod.getName()));
|
||||
|
||||
+13
@@ -152,6 +152,18 @@ public interface ConfigurableListableBeanFactory
|
||||
*/
|
||||
boolean isConfigurationFrozen();
|
||||
|
||||
/**
|
||||
* Mark current thread as main bootstrap thread for singleton instantiation,
|
||||
* with lenient bootstrap locking applying for background threads.
|
||||
* <p>Any such marker is to be removed at the end of the managed bootstrap in
|
||||
* {@link #preInstantiateSingletons()}.
|
||||
* @since 6.2.12
|
||||
* @see #setBootstrapExecutor
|
||||
* @see #preInstantiateSingletons()
|
||||
*/
|
||||
default void prepareSingletonBootstrap() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that all non-lazy-init singletons are instantiated, also considering
|
||||
* {@link org.springframework.beans.factory.FactoryBean FactoryBeans}.
|
||||
@@ -159,6 +171,7 @@ public interface ConfigurableListableBeanFactory
|
||||
* @throws BeansException if one of the singleton beans could not be created.
|
||||
* Note: This may have left the factory with some beans already initialized!
|
||||
* Call {@link #destroySingletons()} for full cleanup in this case.
|
||||
* @see #prepareSingletonBootstrap()
|
||||
* @see #destroySingletons()
|
||||
*/
|
||||
void preInstantiateSingletons() throws BeansException;
|
||||
|
||||
+1
-2
@@ -520,8 +520,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
|
||||
* to check whether the bean with the given name matches the specified type. Allow
|
||||
* additional constraints to be applied to ensure that beans are not created early.
|
||||
* @param name the name of the bean to query
|
||||
* @param typeToMatch the type to match against (as a
|
||||
* {@code ResolvableType})
|
||||
* @param typeToMatch the type to match against (as a {@code ResolvableType})
|
||||
* @return {@code true} if the bean type matches, {@code false} if it
|
||||
* doesn't match or cannot be determined yet
|
||||
* @throws NoSuchBeanDefinitionException if there is no bean with the given name
|
||||
|
||||
+52
-22
@@ -58,6 +58,7 @@ import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.BeanNotOfRequiredTypeException;
|
||||
import org.springframework.beans.factory.CannotLoadBeanClassException;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.InjectionPoint;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
|
||||
@@ -196,8 +197,8 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
/** Map from bean name to merged BeanDefinitionHolder. */
|
||||
private final Map<String, BeanDefinitionHolder> mergedBeanDefinitionHolders = new ConcurrentHashMap<>(256);
|
||||
|
||||
/** Set of bean definition names with a primary marker. */
|
||||
private final Set<String> primaryBeanNames = ConcurrentHashMap.newKeySet(16);
|
||||
/** Map of bean definition names with a primary marker plus corresponding type. */
|
||||
private final Map<String, Class<?>> primaryBeanNamesWithType = new ConcurrentHashMap<>(16);
|
||||
|
||||
/** Map of singleton and non-singleton bean names, keyed by dependency type. */
|
||||
private final Map<Class<?>, String[]> allBeanNamesByType = new ConcurrentHashMap<>(64);
|
||||
@@ -1037,7 +1038,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
protected void cacheMergedBeanDefinition(RootBeanDefinition mbd, String beanName) {
|
||||
super.cacheMergedBeanDefinition(mbd, beanName);
|
||||
if (mbd.isPrimary()) {
|
||||
this.primaryBeanNames.add(beanName);
|
||||
this.primaryBeanNamesWithType.put(beanName, Void.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1101,6 +1102,11 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareSingletonBootstrap() {
|
||||
this.mainThreadPrefix = getThreadNamePrefix();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preInstantiateSingletons() throws BeansException {
|
||||
if (logger.isTraceEnabled()) {
|
||||
@@ -1112,11 +1118,12 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
|
||||
|
||||
// Trigger initialization of all non-lazy singleton beans...
|
||||
List<CompletableFuture<?>> futures = new ArrayList<>();
|
||||
|
||||
this.preInstantiationThread.set(PreInstantiation.MAIN);
|
||||
this.mainThreadPrefix = getThreadNamePrefix();
|
||||
if (this.mainThreadPrefix == null) {
|
||||
this.mainThreadPrefix = getThreadNamePrefix();
|
||||
}
|
||||
try {
|
||||
List<CompletableFuture<?>> futures = new ArrayList<>();
|
||||
for (String beanName : beanNames) {
|
||||
RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
|
||||
if (!mbd.isAbstract() && mbd.isSingleton()) {
|
||||
@@ -1126,21 +1133,20 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!futures.isEmpty()) {
|
||||
try {
|
||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0])).join();
|
||||
}
|
||||
catch (CompletionException ex) {
|
||||
ReflectionUtils.rethrowRuntimeException(ex.getCause());
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.mainThreadPrefix = null;
|
||||
this.preInstantiationThread.remove();
|
||||
}
|
||||
|
||||
if (!futures.isEmpty()) {
|
||||
try {
|
||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0])).join();
|
||||
}
|
||||
catch (CompletionException ex) {
|
||||
ReflectionUtils.rethrowRuntimeException(ex.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger post-initialization callback for all applicable beans...
|
||||
for (String beanName : beanNames) {
|
||||
Object singletonInstance = getSingleton(beanName, false);
|
||||
@@ -1313,7 +1319,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
|
||||
// Cache a primary marker for the given bean.
|
||||
if (beanDefinition.isPrimary()) {
|
||||
this.primaryBeanNames.add(beanName);
|
||||
this.primaryBeanNamesWithType.put(beanName, Void.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1405,7 +1411,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
destroySingleton(beanName);
|
||||
|
||||
// Remove a cached primary marker for the given bean.
|
||||
this.primaryBeanNames.remove(beanName);
|
||||
this.primaryBeanNamesWithType.remove(beanName);
|
||||
|
||||
// Notify all post-processors that the specified bean definition has been reset.
|
||||
for (MergedBeanDefinitionPostProcessor processor : getBeanPostProcessorCache().mergedDefinition) {
|
||||
@@ -1455,11 +1461,30 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addSingleton(String beanName, Object singletonObject) {
|
||||
super.addSingleton(beanName, singletonObject);
|
||||
|
||||
Predicate<Class<?>> filter = (beanType -> beanType != Object.class && beanType.isInstance(singletonObject));
|
||||
this.allBeanNamesByType.keySet().removeIf(filter);
|
||||
this.singletonBeanNamesByType.keySet().removeIf(filter);
|
||||
|
||||
if (this.primaryBeanNamesWithType.containsKey(beanName) && singletonObject.getClass() != NullBean.class) {
|
||||
Class<?> beanType = (singletonObject instanceof FactoryBean<?> fb ?
|
||||
getTypeForFactoryBean(fb) : singletonObject.getClass());
|
||||
if (beanType != null) {
|
||||
this.primaryBeanNamesWithType.put(beanName, beanType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerSingleton(String beanName, Object singletonObject) throws IllegalStateException {
|
||||
super.registerSingleton(beanName, singletonObject);
|
||||
|
||||
updateManualSingletonNames(set -> set.add(beanName), set -> !this.beanDefinitionMap.containsKey(beanName));
|
||||
clearByTypeCache();
|
||||
this.allBeanNamesByType.remove(Object.class);
|
||||
this.singletonBeanNamesByType.remove(Object.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -2083,8 +2108,9 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
boolean candidateLocal = containsBeanDefinition(candidateBeanName);
|
||||
boolean primaryLocal = containsBeanDefinition(primaryBeanName);
|
||||
if (candidateLocal == primaryLocal) {
|
||||
throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(),
|
||||
"more than one 'primary' bean found among candidates: " + candidates.keySet());
|
||||
String message = "more than one 'primary' bean found among candidates: " + candidates.keySet();
|
||||
logger.trace(message);
|
||||
throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(), message);
|
||||
}
|
||||
else if (candidateLocal) {
|
||||
primaryBeanName = candidateBeanName;
|
||||
@@ -2261,8 +2287,12 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
* not matching the given bean name.
|
||||
*/
|
||||
private boolean hasPrimaryConflict(String beanName, Class<?> dependencyType) {
|
||||
for (String candidate : this.primaryBeanNames) {
|
||||
if (isTypeMatch(candidate, dependencyType) && !candidate.equals(beanName)) {
|
||||
for (Map.Entry<String, Class<?>> candidate : this.primaryBeanNamesWithType.entrySet()) {
|
||||
String candidateName = candidate.getKey();
|
||||
Class<?> candidateType = candidate.getValue();
|
||||
if (!candidateName.equals(beanName) && (candidateType != Void.class ?
|
||||
dependencyType.isAssignableFrom(candidateType) : // cached singleton class for primary bean
|
||||
isTypeMatch(candidateName, dependencyType))) { // not instantiated yet or not a singleton
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+19
-3
@@ -23,6 +23,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
@@ -418,14 +419,29 @@ class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable {
|
||||
String destroyMethodName = beanDefinition.resolvedDestroyMethodName;
|
||||
if (destroyMethodName == null) {
|
||||
destroyMethodName = beanDefinition.getDestroyMethodName();
|
||||
boolean autoCloseable = (AutoCloseable.class.isAssignableFrom(target));
|
||||
boolean autoCloseable = AutoCloseable.class.isAssignableFrom(target);
|
||||
boolean executorService = ExecutorService.class.isAssignableFrom(target);
|
||||
if (AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName) ||
|
||||
(destroyMethodName == null && autoCloseable)) {
|
||||
(destroyMethodName == null && (autoCloseable || executorService))) {
|
||||
// Only perform destroy method inference in case of the bean
|
||||
// not explicitly implementing the DisposableBean interface
|
||||
destroyMethodName = null;
|
||||
if (!(DisposableBean.class.isAssignableFrom(target))) {
|
||||
if (autoCloseable) {
|
||||
if (executorService) {
|
||||
destroyMethodName = SHUTDOWN_METHOD_NAME;
|
||||
try {
|
||||
// On JDK 19+, avoid the ExecutorService-level AutoCloseable default implementation
|
||||
// which awaits task termination for 1 day, even for delayed tasks such as cron jobs.
|
||||
// Custom close() implementations in ExecutorService subclasses are still accepted.
|
||||
if (target.getMethod(CLOSE_METHOD_NAME).getDeclaringClass() != ExecutorService.class) {
|
||||
destroyMethodName = CLOSE_METHOD_NAME;
|
||||
}
|
||||
}
|
||||
catch (NoSuchMethodException ex) {
|
||||
// Ignore - stick with shutdown()
|
||||
}
|
||||
}
|
||||
else if (autoCloseable) {
|
||||
destroyMethodName = CLOSE_METHOD_NAME;
|
||||
}
|
||||
else {
|
||||
|
||||
+36
-31
@@ -128,43 +128,48 @@ public abstract class FactoryBeanRegistrySupport extends DefaultSingletonBeanReg
|
||||
locked = (lockFlag && this.singletonLock.tryLock());
|
||||
}
|
||||
try {
|
||||
Object object = this.factoryBeanObjectCache.get(beanName);
|
||||
if (object == null) {
|
||||
object = doGetObjectFromFactoryBean(factory, beanName);
|
||||
// Only post-process and store if not put there already during getObject() call above
|
||||
// (for example, because of circular reference processing triggered by custom getBean calls)
|
||||
Object alreadyThere = this.factoryBeanObjectCache.get(beanName);
|
||||
if (alreadyThere != null) {
|
||||
object = alreadyThere;
|
||||
}
|
||||
else {
|
||||
if (shouldPostProcess) {
|
||||
if (locked) {
|
||||
if (isSingletonCurrentlyInCreation(beanName)) {
|
||||
// Temporarily return non-post-processed object, not storing it yet
|
||||
return object;
|
||||
}
|
||||
beforeSingletonCreation(beanName);
|
||||
}
|
||||
try {
|
||||
object = postProcessObjectFromFactoryBean(object, beanName);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
throw new BeanCreationException(beanName,
|
||||
"Post-processing of FactoryBean's singleton object failed", ex);
|
||||
}
|
||||
finally {
|
||||
// Defensively synchronize against non-thread-safe FactoryBean.getObject() implementations,
|
||||
// potentially to be called from a background thread while the main thread currently calls
|
||||
// the same getObject() method within the singleton lock.
|
||||
synchronized (factory) {
|
||||
Object object = this.factoryBeanObjectCache.get(beanName);
|
||||
if (object == null) {
|
||||
object = doGetObjectFromFactoryBean(factory, beanName);
|
||||
// Only post-process and store if not put there already during getObject() call above
|
||||
// (for example, because of circular reference processing triggered by custom getBean calls)
|
||||
Object alreadyThere = this.factoryBeanObjectCache.get(beanName);
|
||||
if (alreadyThere != null) {
|
||||
object = alreadyThere;
|
||||
}
|
||||
else {
|
||||
if (shouldPostProcess) {
|
||||
if (locked) {
|
||||
afterSingletonCreation(beanName);
|
||||
if (isSingletonCurrentlyInCreation(beanName)) {
|
||||
// Temporarily return non-post-processed object, not storing it yet
|
||||
return object;
|
||||
}
|
||||
beforeSingletonCreation(beanName);
|
||||
}
|
||||
try {
|
||||
object = postProcessObjectFromFactoryBean(object, beanName);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
throw new BeanCreationException(beanName,
|
||||
"Post-processing of FactoryBean's singleton object failed", ex);
|
||||
}
|
||||
finally {
|
||||
if (locked) {
|
||||
afterSingletonCreation(beanName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (containsSingleton(beanName)) {
|
||||
this.factoryBeanObjectCache.put(beanName, object);
|
||||
if (containsSingleton(beanName)) {
|
||||
this.factoryBeanObjectCache.put(beanName, object);
|
||||
}
|
||||
}
|
||||
}
|
||||
return object;
|
||||
}
|
||||
return object;
|
||||
}
|
||||
finally {
|
||||
if (locked) {
|
||||
|
||||
+1
-1
@@ -103,7 +103,7 @@ public class PathEditor extends PropertyEditorSupport {
|
||||
if (resource == null) {
|
||||
setValue(null);
|
||||
}
|
||||
else if (nioPathCandidate && !resource.exists()) {
|
||||
else if (nioPathCandidate && (!resource.isFile() || !resource.exists())) {
|
||||
setValue(Paths.get(text).normalize());
|
||||
}
|
||||
else {
|
||||
|
||||
+33
-5
@@ -87,6 +87,7 @@ import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.core.testfixture.io.SerializationTestUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.util.StringValueResolver;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -1798,7 +1799,7 @@ class DefaultListableBeanFactoryTests {
|
||||
|
||||
assertThatExceptionOfType(NoUniqueBeanDefinitionException.class)
|
||||
.isThrownBy(() -> lbf.getBean(TestBean.class))
|
||||
.withMessageContaining("more than one 'primary'");
|
||||
.withMessageEndingWith("more than one 'primary' bean found among candidates: [bd1, bd2]");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -2122,7 +2123,7 @@ class DefaultListableBeanFactoryTests {
|
||||
|
||||
assertThatExceptionOfType(NoUniqueBeanDefinitionException.class)
|
||||
.isThrownBy(() -> lbf.getBean(ConstructorDependency.class, 42))
|
||||
.withMessageContaining("more than one 'primary'");
|
||||
.withMessageEndingWith("more than one 'primary' bean found among candidates: [bd1, bd2]");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -3202,6 +3203,33 @@ class DefaultListableBeanFactoryTests {
|
||||
assertThat(holder.getNonPublicEnum()).isEqualTo(NonPublicEnum.VALUE_1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void mostSpecificCacheEntryForTypeMatching() {
|
||||
RootBeanDefinition bd1 = new RootBeanDefinition();
|
||||
bd1.setFactoryBeanName("config");
|
||||
bd1.setFactoryMethodName("create");
|
||||
lbf.registerBeanDefinition("config", new RootBeanDefinition(BeanWithFactoryMethod.class));
|
||||
lbf.registerBeanDefinition("bd1", bd1);
|
||||
lbf.registerBeanDefinition("bd2", new RootBeanDefinition(NestedTestBean.class));
|
||||
lbf.freezeConfiguration();
|
||||
|
||||
String[] allBeanNames = lbf.getBeanNamesForType(Object.class);
|
||||
String[] nestedBeanNames = lbf.getBeanNamesForType(NestedTestBean.class);
|
||||
assertThat(lbf.getType("bd1")).isEqualTo(TestBean.class);
|
||||
assertThat(lbf.getBeanNamesForType(TestBean.class)).containsExactly("bd1");
|
||||
assertThat(lbf.getBeanNamesForType(DerivedTestBean.class)).isEmpty();
|
||||
lbf.getBean("bd1");
|
||||
assertThat(lbf.getType("bd1")).isEqualTo(DerivedTestBean.class);
|
||||
assertThat(lbf.getBeanNamesForType(TestBean.class)).containsExactly("bd1");
|
||||
assertThat(lbf.getBeanNamesForType(DerivedTestBean.class)).containsExactly("bd1");
|
||||
assertThat(lbf.getBeanNamesForType(NestedTestBean.class)).isSameAs(nestedBeanNames);
|
||||
assertThat(lbf.getBeanNamesForType(Object.class)).isSameAs(allBeanNames);
|
||||
|
||||
lbf.registerSingleton("bd3", new Object());
|
||||
assertThat(lbf.getBeanNamesForType(NestedTestBean.class)).isSameAs(nestedBeanNames);
|
||||
assertThat(lbf.getBeanNamesForType(Object.class)).containsExactly(StringUtils.addStringToArray(allBeanNames, "bd3"));
|
||||
}
|
||||
|
||||
|
||||
private int registerBeanDefinitions(Properties p) {
|
||||
return registerBeanDefinitions(p, null);
|
||||
@@ -3418,7 +3446,7 @@ class DefaultListableBeanFactoryTests {
|
||||
}
|
||||
|
||||
public TestBean create() {
|
||||
TestBean tb = new TestBean();
|
||||
DerivedTestBean tb = new DerivedTestBean();
|
||||
tb.setName(this.name);
|
||||
return tb;
|
||||
}
|
||||
@@ -3646,11 +3674,11 @@ class DefaultListableBeanFactoryTests {
|
||||
|
||||
private FactoryBean<?> factoryBean;
|
||||
|
||||
public final FactoryBean<?> getFactoryBean() {
|
||||
public FactoryBean<?> getFactoryBean() {
|
||||
return this.factoryBean;
|
||||
}
|
||||
|
||||
public final void setFactoryBean(final FactoryBean<?> factoryBean) {
|
||||
public void setFactoryBean(FactoryBean<?> factoryBean) {
|
||||
this.factoryBean = factoryBean;
|
||||
}
|
||||
}
|
||||
|
||||
+30
@@ -418,6 +418,16 @@ class InstanceSupplierCodeGeneratorTests {
|
||||
compileAndCheckWarnings(beanDefinition);
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateWhenTargetFactoryMethodIsProtectedAndReturnTypeIsDeprecated() {
|
||||
BeanDefinition beanDefinition = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(DeprecatedBean.class)
|
||||
.setFactoryMethodOnBean("deprecatedReturnTypeProtected", "config").getBeanDefinition();
|
||||
beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder
|
||||
.genericBeanDefinition(DeprecatedMemberConfiguration.class).getBeanDefinition());
|
||||
compileAndCheckWarnings(beanDefinition);
|
||||
}
|
||||
|
||||
private void compileAndCheckWarnings(BeanDefinition beanDefinition) {
|
||||
assertThatNoException().isThrownBy(() -> compile(TEST_COMPILER, beanDefinition,
|
||||
((instanceSupplier, compiled) -> {})));
|
||||
@@ -464,6 +474,26 @@ class InstanceSupplierCodeGeneratorTests {
|
||||
compileAndCheckWarnings(beanDefinition);
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateWhenTargetFactoryMethodReturnTypeIsDeprecatedForRemoval() {
|
||||
BeanDefinition beanDefinition = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(DeprecatedForRemovalBean.class)
|
||||
.setFactoryMethodOnBean("deprecatedReturnType", "config").getBeanDefinition();
|
||||
beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder
|
||||
.genericBeanDefinition(DeprecatedForRemovalMemberConfiguration.class).getBeanDefinition());
|
||||
compileAndCheckWarnings(beanDefinition);
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateWhenTargetFactoryMethodIsProtectedAndReturnTypeIsDeprecatedForRemoval() {
|
||||
BeanDefinition beanDefinition = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(DeprecatedForRemovalBean.class)
|
||||
.setFactoryMethodOnBean("deprecatedReturnTypeProtected", "config").getBeanDefinition();
|
||||
beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder
|
||||
.genericBeanDefinition(DeprecatedForRemovalMemberConfiguration.class).getBeanDefinition());
|
||||
compileAndCheckWarnings(beanDefinition);
|
||||
}
|
||||
|
||||
private void compileAndCheckWarnings(BeanDefinition beanDefinition) {
|
||||
assertThatNoException().isThrownBy(() -> compile(TEST_COMPILER, beanDefinition,
|
||||
((instanceSupplier, compiled) -> {})));
|
||||
|
||||
+46
-1
@@ -17,6 +17,7 @@
|
||||
package org.springframework.beans.factory.support;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -59,13 +60,38 @@ class RootBeanDefinitionTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveDestroyMethodWithMatchingCandidateReplacedInferredVaue() {
|
||||
void resolveDestroyMethodWithMatchingCandidateReplacedForCloseMethod() {
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanWithCloseMethod.class);
|
||||
beanDefinition.setDestroyMethodName(AbstractBeanDefinition.INFER_METHOD);
|
||||
beanDefinition.resolveDestroyMethodIfNecessary();
|
||||
assertThat(beanDefinition.getDestroyMethodNames()).containsExactly("close");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveDestroyMethodWithMatchingCandidateReplacedForShutdownMethod() {
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanWithShutdownMethod.class);
|
||||
beanDefinition.setDestroyMethodName(AbstractBeanDefinition.INFER_METHOD);
|
||||
beanDefinition.resolveDestroyMethodIfNecessary();
|
||||
assertThat(beanDefinition.getDestroyMethodNames()).containsExactly("shutdown");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveDestroyMethodWithMatchingCandidateReplacedForExecutorService() {
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanImplementingExecutorService.class);
|
||||
beanDefinition.setDestroyMethodName(AbstractBeanDefinition.INFER_METHOD);
|
||||
beanDefinition.resolveDestroyMethodIfNecessary();
|
||||
assertThat(beanDefinition.getDestroyMethodNames()).containsExactly("shutdown");
|
||||
// even on JDK 19+ where the ExecutorService interface declares a default AutoCloseable implementation
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveDestroyMethodWithMatchingCandidateReplacedForAutoCloseableExecutorService() {
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanImplementingExecutorServiceAndAutoCloseable.class);
|
||||
beanDefinition.setDestroyMethodName(AbstractBeanDefinition.INFER_METHOD);
|
||||
beanDefinition.resolveDestroyMethodIfNecessary();
|
||||
assertThat(beanDefinition.getDestroyMethodNames()).containsExactly("close");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveDestroyMethodWithNoCandidateSetDestroyMethodNameToNull() {
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanWithNoDestroyMethod.class);
|
||||
@@ -90,6 +116,25 @@ class RootBeanDefinitionTests {
|
||||
}
|
||||
|
||||
|
||||
static class BeanWithShutdownMethod {
|
||||
|
||||
public void shutdown() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
abstract static class BeanImplementingExecutorService implements ExecutorService {
|
||||
}
|
||||
|
||||
|
||||
abstract static class BeanImplementingExecutorServiceAndAutoCloseable implements ExecutorService, AutoCloseable {
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static class BeanWithNoDestroyMethod {
|
||||
}
|
||||
|
||||
|
||||
+10
@@ -33,4 +33,14 @@ public class DeprecatedForRemovalMemberConfiguration {
|
||||
return bean.toString();
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
public DeprecatedForRemovalBean deprecatedReturnType() {
|
||||
return new DeprecatedForRemovalBean();
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
DeprecatedForRemovalBean deprecatedReturnTypeProtected() {
|
||||
return new DeprecatedForRemovalBean();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
@@ -38,4 +38,9 @@ public class DeprecatedMemberConfiguration {
|
||||
return new DeprecatedBean();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
DeprecatedBean deprecatedReturnTypeProtected() {
|
||||
return new DeprecatedBean();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-2
@@ -37,12 +37,13 @@ import javax.lang.model.element.TypeElement;
|
||||
|
||||
/**
|
||||
* Annotation {@link Processor} that writes a {@link CandidateComponentsMetadata}
|
||||
* file for spring components.
|
||||
* file for Spring components.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Juergen Hoeller
|
||||
* @since 5.0
|
||||
* @deprecated as of 6.1, in favor of the AOT engine.
|
||||
* @deprecated as of 6.1, in favor of the AOT engine and the forthcoming
|
||||
* support for an AOT-generated Spring components index
|
||||
*/
|
||||
@Deprecated(since = "6.1", forRemoval = true)
|
||||
public class CandidateComponentsIndexer implements Processor {
|
||||
|
||||
+2
-2
@@ -25,8 +25,8 @@ import javax.lang.model.element.ElementKind;
|
||||
|
||||
/**
|
||||
* A {@link StereotypesProvider} that extracts a stereotype for each
|
||||
* {@code jakarta.*} or {@code javax.*} annotation <i>present</i> on a class or
|
||||
* interface.
|
||||
* {@code jakarta.*} or {@code javax.*} annotation <i>present</i>
|
||||
* on a class or interface.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 5.0
|
||||
|
||||
@@ -55,7 +55,7 @@ public interface MessageSource {
|
||||
* @see java.text.MessageFormat
|
||||
*/
|
||||
@Nullable
|
||||
String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);
|
||||
String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, @Nullable Locale locale);
|
||||
|
||||
/**
|
||||
* Try to resolve the message. Treat as an error if the message can't be found.
|
||||
@@ -71,7 +71,7 @@ public interface MessageSource {
|
||||
* @see #getMessage(MessageSourceResolvable, Locale)
|
||||
* @see java.text.MessageFormat
|
||||
*/
|
||||
String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;
|
||||
String getMessage(String code, @Nullable Object[] args, @Nullable Locale locale) throws NoSuchMessageException;
|
||||
|
||||
/**
|
||||
* Try to resolve the message using all the attributes contained within the
|
||||
@@ -91,6 +91,6 @@ public interface MessageSource {
|
||||
* @see MessageSourceResolvable#getDefaultMessage()
|
||||
* @see java.text.MessageFormat
|
||||
*/
|
||||
String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
|
||||
String getMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) throws NoSuchMessageException;
|
||||
|
||||
}
|
||||
|
||||
@@ -17,19 +17,18 @@
|
||||
package org.springframework.context.annotation;
|
||||
|
||||
/**
|
||||
* Enumeration used to determine whether JDK proxy-based or
|
||||
* Enumeration used to determine whether JDK/CGLIB proxy-based or
|
||||
* AspectJ weaving-based advice should be applied.
|
||||
*
|
||||
* @author Chris Beams
|
||||
* @since 3.1
|
||||
* @see org.springframework.scheduling.annotation.EnableAsync#mode()
|
||||
* @see org.springframework.scheduling.annotation.AsyncConfigurationSelector#selectImports
|
||||
* @see org.springframework.transaction.annotation.EnableTransactionManagement#mode()
|
||||
* @see org.springframework.scheduling.annotation.EnableAsync#mode()
|
||||
*/
|
||||
public enum AdviceMode {
|
||||
|
||||
/**
|
||||
* JDK proxy-based advice.
|
||||
* JDK/CGLIB proxy-based advice.
|
||||
*/
|
||||
PROXY,
|
||||
|
||||
|
||||
+6
-7
@@ -90,7 +90,6 @@ import org.springframework.util.ClassUtils;
|
||||
* @see ScannedGenericBeanDefinition
|
||||
* @see CandidateComponentsIndex
|
||||
*/
|
||||
@SuppressWarnings("removal") // components index
|
||||
public class ClassPathScanningCandidateComponentProvider implements EnvironmentCapable, ResourceLoaderAware {
|
||||
|
||||
static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
|
||||
@@ -452,9 +451,9 @@ public class ClassPathScanningCandidateComponentProvider implements EnvironmentC
|
||||
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
|
||||
Set<BeanDefinition> candidates = new LinkedHashSet<>();
|
||||
try {
|
||||
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
|
||||
String packageSearchPattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
|
||||
resolveBasePackage(basePackage) + '/' + this.resourcePattern;
|
||||
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
|
||||
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPattern);
|
||||
boolean traceEnabled = logger.isTraceEnabled();
|
||||
boolean debugEnabled = logger.isDebugEnabled();
|
||||
for (Resource resource : resources) {
|
||||
@@ -537,13 +536,13 @@ public class ClassPathScanningCandidateComponentProvider implements EnvironmentC
|
||||
* @return whether the class qualifies as a candidate component
|
||||
*/
|
||||
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
|
||||
for (TypeFilter tf : this.excludeFilters) {
|
||||
if (tf.match(metadataReader, getMetadataReaderFactory())) {
|
||||
for (TypeFilter filter : this.excludeFilters) {
|
||||
if (filter.match(metadataReader, getMetadataReaderFactory())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (TypeFilter tf : this.includeFilters) {
|
||||
if (tf.match(metadataReader, getMetadataReaderFactory())) {
|
||||
for (TypeFilter filter : this.includeFilters) {
|
||||
if (filter.match(metadataReader, getMetadataReaderFactory())) {
|
||||
return isConditionMatch(metadataReader);
|
||||
}
|
||||
}
|
||||
|
||||
+10
-2
@@ -157,9 +157,17 @@ class ConfigurationClassBeanDefinitionReader {
|
||||
|
||||
ScopeMetadata scopeMetadata = scopeMetadataResolver.resolveScopeMetadata(configBeanDef);
|
||||
configBeanDef.setScope(scopeMetadata.getScopeName());
|
||||
String configBeanName = this.importBeanNameGenerator.generateBeanName(configBeanDef, this.registry);
|
||||
AnnotationConfigUtils.processCommonDefinitionAnnotations(configBeanDef, metadata);
|
||||
|
||||
String configBeanName;
|
||||
try {
|
||||
configBeanName = this.importBeanNameGenerator.generateBeanName(configBeanDef, this.registry);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
throw new IllegalStateException("Failed to generate bean name for imported class '" +
|
||||
configClass.getMetadata().getClassName() + "'", ex);
|
||||
}
|
||||
|
||||
AnnotationConfigUtils.processCommonDefinitionAnnotations(configBeanDef, metadata);
|
||||
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(configBeanDef, configBeanName);
|
||||
definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
|
||||
this.registry.registerBeanDefinition(definitionHolder.getBeanName(), definitionHolder.getBeanDefinition());
|
||||
|
||||
+1
-1
@@ -51,9 +51,9 @@ import org.springframework.core.annotation.AliasFor;
|
||||
* ignored.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 6.2
|
||||
* @see Reflective @Reflective
|
||||
* @see RegisterReflection @RegisterReflection
|
||||
* @since 6.2
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
|
||||
+2
-2
@@ -63,13 +63,13 @@ public interface AotApplicationContextInitializer<C extends ConfigurableApplicat
|
||||
|
||||
private static <C extends ConfigurableApplicationContext> void initialize(
|
||||
C applicationContext, String... initializerClassNames) {
|
||||
|
||||
Log logger = LogFactory.getLog(AotApplicationContextInitializer.class);
|
||||
ClassLoader classLoader = applicationContext.getClassLoader();
|
||||
logger.debug("Initializing ApplicationContext with AOT");
|
||||
for (String initializerClassName : initializerClassNames) {
|
||||
logger.trace(LogMessage.format("Applying %s", initializerClassName));
|
||||
instantiateInitializer(initializerClassName, classLoader)
|
||||
.initialize(applicationContext);
|
||||
instantiateInitializer(initializerClassName, classLoader).initialize(applicationContext);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -101,10 +101,9 @@ public @interface EventListener {
|
||||
|
||||
/**
|
||||
* The event classes that this listener handles.
|
||||
* <p>If this attribute is specified with a single value, the
|
||||
* annotated method may optionally accept a single parameter.
|
||||
* However, if this attribute is specified with multiple values,
|
||||
* the annotated method must <em>not</em> declare any parameters.
|
||||
* <p>The annotated method may optionally accept a single parameter
|
||||
* of the given event class, or of a common base class or interface
|
||||
* for all given event classes.
|
||||
*/
|
||||
@AliasFor("value")
|
||||
Class<?>[] classes() default {};
|
||||
|
||||
+2
-4
@@ -45,9 +45,7 @@ import org.springframework.util.MultiValueMap;
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 5.0
|
||||
* @deprecated as of 6.1, in favor of the AOT engine.
|
||||
*/
|
||||
@Deprecated(since = "6.1", forRemoval = true)
|
||||
public class CandidateComponentsIndex {
|
||||
|
||||
private static final AntPathMatcher pathMatcher = new AntPathMatcher(".");
|
||||
@@ -83,7 +81,7 @@ public class CandidateComponentsIndex {
|
||||
public Set<String> getCandidateTypes(String basePackage, String stereotype) {
|
||||
List<Entry> candidates = this.index.get(stereotype);
|
||||
if (candidates != null) {
|
||||
return candidates.parallelStream()
|
||||
return candidates.stream()
|
||||
.filter(t -> t.match(basePackage))
|
||||
.map(t -> t.type)
|
||||
.collect(Collectors.toSet());
|
||||
@@ -94,7 +92,7 @@ public class CandidateComponentsIndex {
|
||||
|
||||
private static class Entry {
|
||||
|
||||
private final String type;
|
||||
final String type;
|
||||
|
||||
private final String packageName;
|
||||
|
||||
|
||||
-3
@@ -38,10 +38,7 @@ import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 5.0
|
||||
* @deprecated as of 6.1, in favor of the AOT engine.
|
||||
*/
|
||||
@Deprecated(since = "6.1", forRemoval = true)
|
||||
@SuppressWarnings("removal")
|
||||
public final class CandidateComponentsIndexLoader {
|
||||
|
||||
/**
|
||||
|
||||
+6
-3
@@ -936,6 +936,9 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
|
||||
// Mark current thread for singleton instantiation with applied bootstrap locking.
|
||||
beanFactory.prepareSingletonBootstrap();
|
||||
|
||||
// Initialize bootstrap executor for this context.
|
||||
if (beanFactory.containsBean(BOOTSTRAP_EXECUTOR_BEAN_NAME) &&
|
||||
beanFactory.isTypeMatch(BOOTSTRAP_EXECUTOR_BEAN_NAME, Executor.class)) {
|
||||
@@ -1502,17 +1505,17 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) {
|
||||
public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, @Nullable Locale locale) {
|
||||
return getMessageSource().getMessage(code, args, defaultMessage, locale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException {
|
||||
public String getMessage(String code, @Nullable Object[] args, @Nullable Locale locale) throws NoSuchMessageException {
|
||||
return getMessageSource().getMessage(code, args, locale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException {
|
||||
public String getMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) throws NoSuchMessageException {
|
||||
return getMessageSource().getMessage(resolvable, locale);
|
||||
}
|
||||
|
||||
|
||||
+18
-7
@@ -138,7 +138,7 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public final String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) {
|
||||
public final String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, @Nullable Locale locale) {
|
||||
String msg = getMessageInternal(code, args, locale);
|
||||
if (msg != null) {
|
||||
return msg;
|
||||
@@ -150,7 +150,7 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException {
|
||||
public final String getMessage(String code, @Nullable Object[] args, @Nullable Locale locale) throws NoSuchMessageException {
|
||||
String msg = getMessageInternal(code, args, locale);
|
||||
if (msg != null) {
|
||||
return msg;
|
||||
@@ -159,11 +159,16 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme
|
||||
if (fallback != null) {
|
||||
return fallback;
|
||||
}
|
||||
throw new NoSuchMessageException(code, locale);
|
||||
if (locale == null ) {
|
||||
throw new NoSuchMessageException(code);
|
||||
}
|
||||
else {
|
||||
throw new NoSuchMessageException(code, locale);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException {
|
||||
public final String getMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) throws NoSuchMessageException {
|
||||
String[] codes = resolvable.getCodes();
|
||||
if (codes != null) {
|
||||
for (String code : codes) {
|
||||
@@ -177,7 +182,13 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme
|
||||
if (defaultMessage != null) {
|
||||
return defaultMessage;
|
||||
}
|
||||
throw new NoSuchMessageException(!ObjectUtils.isEmpty(codes) ? codes[codes.length - 1] : "", locale);
|
||||
String code = !ObjectUtils.isEmpty(codes) ? codes[codes.length - 1] : "";
|
||||
if (locale == null ) {
|
||||
throw new NoSuchMessageException(code);
|
||||
}
|
||||
else {
|
||||
throw new NoSuchMessageException(code, locale);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -284,7 +295,7 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme
|
||||
* @see #getDefaultMessage(String)
|
||||
*/
|
||||
@Nullable
|
||||
protected String getDefaultMessage(MessageSourceResolvable resolvable, Locale locale) {
|
||||
protected String getDefaultMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) {
|
||||
String defaultMessage = resolvable.getDefaultMessage();
|
||||
String[] codes = resolvable.getCodes();
|
||||
if (defaultMessage != null) {
|
||||
@@ -331,7 +342,7 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme
|
||||
* @return an array of arguments with any MessageSourceResolvables resolved
|
||||
*/
|
||||
@Override
|
||||
protected Object[] resolveArguments(@Nullable Object[] args, Locale locale) {
|
||||
protected Object[] resolveArguments(@Nullable Object[] args, @Nullable Locale locale) {
|
||||
if (ObjectUtils.isEmpty(args)) {
|
||||
return super.resolveArguments(args, locale);
|
||||
}
|
||||
|
||||
+15
-5
@@ -55,7 +55,7 @@ public class DelegatingMessageSource extends MessageSourceSupport implements Hie
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) {
|
||||
public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, @Nullable Locale locale) {
|
||||
if (this.parentMessageSource != null) {
|
||||
return this.parentMessageSource.getMessage(code, args, defaultMessage, locale);
|
||||
}
|
||||
@@ -68,17 +68,22 @@ public class DelegatingMessageSource extends MessageSourceSupport implements Hie
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException {
|
||||
public String getMessage(String code, @Nullable Object[] args, @Nullable Locale locale) throws NoSuchMessageException {
|
||||
if (this.parentMessageSource != null) {
|
||||
return this.parentMessageSource.getMessage(code, args, locale);
|
||||
}
|
||||
else {
|
||||
throw new NoSuchMessageException(code, locale);
|
||||
if (locale == null) {
|
||||
throw new NoSuchMessageException(code);
|
||||
}
|
||||
else {
|
||||
throw new NoSuchMessageException(code, locale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException {
|
||||
public String getMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) throws NoSuchMessageException {
|
||||
if (this.parentMessageSource != null) {
|
||||
return this.parentMessageSource.getMessage(resolvable, locale);
|
||||
}
|
||||
@@ -88,7 +93,12 @@ public class DelegatingMessageSource extends MessageSourceSupport implements Hie
|
||||
}
|
||||
String[] codes = resolvable.getCodes();
|
||||
String code = (codes != null && codes.length > 0 ? codes[0] : "");
|
||||
throw new NoSuchMessageException(code, locale);
|
||||
if (locale == null) {
|
||||
throw new NoSuchMessageException(code);
|
||||
}
|
||||
else {
|
||||
throw new NoSuchMessageException(code, locale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -98,7 +98,7 @@ public abstract class MessageSourceSupport {
|
||||
* @return the rendered default message (with resolved arguments)
|
||||
* @see #formatMessage(String, Object[], java.util.Locale)
|
||||
*/
|
||||
protected String renderDefaultMessage(String defaultMessage, @Nullable Object[] args, Locale locale) {
|
||||
protected String renderDefaultMessage(String defaultMessage, @Nullable Object[] args, @Nullable Locale locale) {
|
||||
return formatMessage(defaultMessage, args, locale);
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ public abstract class MessageSourceSupport {
|
||||
* @param locale the Locale used for formatting
|
||||
* @return the formatted message (with resolved arguments)
|
||||
*/
|
||||
protected String formatMessage(String msg, @Nullable Object[] args, Locale locale) {
|
||||
protected String formatMessage(String msg, @Nullable Object[] args, @Nullable Locale locale) {
|
||||
if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {
|
||||
return msg;
|
||||
}
|
||||
@@ -146,7 +146,7 @@ public abstract class MessageSourceSupport {
|
||||
* @param locale the Locale to create a {@code MessageFormat} for
|
||||
* @return the {@code MessageFormat} instance
|
||||
*/
|
||||
protected MessageFormat createMessageFormat(String msg, Locale locale) {
|
||||
protected MessageFormat createMessageFormat(String msg, @Nullable Locale locale) {
|
||||
return new MessageFormat(msg, locale);
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ public abstract class MessageSourceSupport {
|
||||
* @param locale the Locale to resolve against
|
||||
* @return the resolved argument array
|
||||
*/
|
||||
protected Object[] resolveArguments(@Nullable Object[] args, Locale locale) {
|
||||
protected Object[] resolveArguments(@Nullable Object[] args, @Nullable Locale locale) {
|
||||
return (args != null ? args : new Object[0]);
|
||||
}
|
||||
|
||||
|
||||
-1
@@ -87,7 +87,6 @@ public class AsyncAnnotationBeanPostProcessor extends AbstractBeanFactoryAwareAd
|
||||
private Class<? extends Annotation> asyncAnnotationType;
|
||||
|
||||
|
||||
|
||||
public AsyncAnnotationBeanPostProcessor() {
|
||||
setBeforeExistingAdvisors(true);
|
||||
}
|
||||
|
||||
+1
-1
@@ -376,7 +376,7 @@ public class SimpleAsyncTaskScheduler extends SimpleAsyncTaskExecutor implements
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return this.triggerLifecycle.isRunning();
|
||||
return (this.triggerLifecycle.isRunning() || this.fixedDelayLifecycle.isRunning());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -50,7 +50,9 @@ public class Task {
|
||||
|
||||
|
||||
/**
|
||||
* Return the underlying task.
|
||||
* Return a {@link Runnable} that executes the underlying task.
|
||||
* <p>Note, this does not necessarily return the {@link Task#Task(Runnable) original runnable}
|
||||
* as it can be wrapped by the Framework for additional support.
|
||||
*/
|
||||
public Runnable getRunnable() {
|
||||
return this.runnable;
|
||||
|
||||
-2
@@ -69,8 +69,6 @@ import org.springframework.validation.method.ParameterValidationResult;
|
||||
* at the type level of the containing target class, applying to all public service methods
|
||||
* of that class. By default, JSR-303 will validate against its default group only.
|
||||
*
|
||||
* <p>This functionality requires a Bean Validation 1.1+ provider.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 3.1
|
||||
|
||||
-2
@@ -60,8 +60,6 @@ import org.springframework.validation.method.MethodValidationResult;
|
||||
* inline constraint annotations. Validation groups can be specified through {@code @Validated}
|
||||
* as well. By default, JSR-303 will validate against its default group only.
|
||||
*
|
||||
* <p>This functionality requires a Bean Validation 1.1+ provider.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @since 3.1
|
||||
* @see MethodValidationInterceptor
|
||||
|
||||
-1
@@ -56,7 +56,6 @@ public class SpringConstraintValidatorFactory implements ConstraintValidatorFact
|
||||
return this.beanFactory.createBean(key);
|
||||
}
|
||||
|
||||
// Bean Validation 1.1 releaseInstance method
|
||||
@Override
|
||||
public void releaseInstance(ConstraintValidator<?, ?> instance) {
|
||||
this.beanFactory.destroyBean(instance);
|
||||
|
||||
-2
@@ -55,8 +55,6 @@ import org.springframework.validation.SmartValidator;
|
||||
* {@link CustomValidatorBean} and {@link LocalValidatorFactoryBean},
|
||||
* and as the primary implementation of the {@link SmartValidator} interface.
|
||||
*
|
||||
* <p>This adapter is fully compatible with Bean Validation 1.1 as well as 2.0.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 3.0
|
||||
|
||||
+6
-6
@@ -60,12 +60,6 @@ class CacheOperationExpressionEvaluatorTests {
|
||||
private final AnnotationCacheOperationSource source = new AnnotationCacheOperationSource();
|
||||
|
||||
|
||||
private Collection<CacheOperation> getOps(String name) {
|
||||
Method method = ReflectionUtils.findMethod(AnnotatedClass.class, name, Object.class, Object.class);
|
||||
return this.source.getCacheOperations(method, AnnotatedClass.class);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testMultipleCachingSource() {
|
||||
Collection<CacheOperation> ops = getOps("multipleCaching");
|
||||
@@ -144,6 +138,12 @@ class CacheOperationExpressionEvaluatorTests {
|
||||
assertThat(value).isEqualTo(String.class.getName());
|
||||
}
|
||||
|
||||
|
||||
private Collection<CacheOperation> getOps(String name) {
|
||||
Method method = ReflectionUtils.findMethod(AnnotatedClass.class, name, Object.class, Object.class);
|
||||
return this.source.getCacheOperations(method, AnnotatedClass.class);
|
||||
}
|
||||
|
||||
private EvaluationContext createEvaluationContext(Object result) {
|
||||
return createEvaluationContext(result, null);
|
||||
}
|
||||
|
||||
Vendored
+4
@@ -104,6 +104,7 @@ class CachePutEvaluationTests {
|
||||
assertThat(this.cache.get(anotherValue + 100).get()).as("Wrong value for @CachePut key").isEqualTo(anotherValue);
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
static class Config implements CachingConfigurer {
|
||||
@@ -121,8 +122,10 @@ class CachePutEvaluationTests {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@CacheConfig("test")
|
||||
public static class SimpleService {
|
||||
|
||||
private AtomicLong counter = new AtomicLong();
|
||||
|
||||
/**
|
||||
@@ -144,4 +147,5 @@ class CachePutEvaluationTests {
|
||||
return this.counter.getAndIncrement();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+55
@@ -31,6 +31,7 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.beans.testfixture.beans.TestBean;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.weaving.LoadTimeWeaverAware;
|
||||
import org.springframework.core.SpringProperties;
|
||||
import org.springframework.core.testfixture.EnabledForTestGroups;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
@@ -68,6 +69,16 @@ class BackgroundBootstrapTests {
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(10)
|
||||
@EnabledForTestGroups(LONG_RUNNING)
|
||||
void bootstrapWithLoadTimeWeaverAware() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(LoadTimeWeaverAwareBeanConfig.class);
|
||||
ctx.getBean("testBean1", TestBean.class);
|
||||
ctx.getBean("testBean2", TestBean.class);
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(10)
|
||||
@EnabledForTestGroups(LONG_RUNNING)
|
||||
@@ -266,6 +277,50 @@ class BackgroundBootstrapTests {
|
||||
}
|
||||
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class LoadTimeWeaverAwareBeanConfig {
|
||||
|
||||
@Bean
|
||||
LoadTimeWeaverAware loadTimeWeaverAware(ObjectProvider<TestBean> testBean1) {
|
||||
Thread thread = new Thread(testBean1::getObject);
|
||||
thread.start();
|
||||
try {
|
||||
thread.join();
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
return (loadTimeWeaver -> {});
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestBean testBean1(TestBean testBean2) {
|
||||
return new TestBean(testBean2);
|
||||
}
|
||||
|
||||
@Bean @Lazy
|
||||
public FactoryBean<TestBean> testBean2() {
|
||||
try {
|
||||
Thread.sleep(2000);
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
TestBean testBean = new TestBean();
|
||||
return new FactoryBean<>() {
|
||||
@Override
|
||||
public TestBean getObject() {
|
||||
return testBean;
|
||||
}
|
||||
@Override
|
||||
public Class<?> getObjectType() {
|
||||
return testBean.getClass();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class StrictLockingBeanConfig {
|
||||
|
||||
|
||||
+15
-9
@@ -61,6 +61,7 @@ class ClassPathBeanDefinitionScannerTests {
|
||||
GenericApplicationContext context = new GenericApplicationContext();
|
||||
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context);
|
||||
int beanCount = scanner.scan(BASE_PACKAGE);
|
||||
|
||||
assertThat(beanCount).isGreaterThanOrEqualTo(12);
|
||||
assertThat(context.containsBean("serviceInvocationCounter")).isTrue();
|
||||
assertThat(context.containsBean("fooServiceImpl")).isTrue();
|
||||
@@ -73,8 +74,8 @@ class ClassPathBeanDefinitionScannerTests {
|
||||
assertThat(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue();
|
||||
assertThat(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)).isTrue();
|
||||
assertThat(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_FACTORY_BEAN_NAME)).isTrue();
|
||||
context.refresh();
|
||||
|
||||
context.refresh();
|
||||
FooServiceImpl fooService = context.getBean("fooServiceImpl", FooServiceImpl.class);
|
||||
assertThat(context.getDefaultListableBeanFactory().containsSingleton("myNamedComponent")).isTrue();
|
||||
assertThat(fooService.foo(123)).isEqualTo("bar");
|
||||
@@ -157,6 +158,7 @@ class ClassPathBeanDefinitionScannerTests {
|
||||
|
||||
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context);
|
||||
int beanCount = scanner.scan(BASE_PACKAGE);
|
||||
|
||||
assertThat(beanCount).isGreaterThanOrEqualTo(12);
|
||||
|
||||
ClassPathBeanDefinitionScanner scanner2 = new ClassPathBeanDefinitionScanner(context) {
|
||||
@@ -182,8 +184,8 @@ class ClassPathBeanDefinitionScannerTests {
|
||||
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context);
|
||||
scanner.setIncludeAnnotationConfig(false);
|
||||
int beanCount = scanner.scan(BASE_PACKAGE);
|
||||
assertThat(beanCount).isGreaterThanOrEqualTo(7);
|
||||
|
||||
assertThat(beanCount).isGreaterThanOrEqualTo(7);
|
||||
assertThat(context.containsBean("serviceInvocationCounter")).isTrue();
|
||||
assertThat(context.containsBean("fooServiceImpl")).isTrue();
|
||||
assertThat(context.containsBean("stubFooDao")).isTrue();
|
||||
@@ -482,12 +484,14 @@ class ClassPathBeanDefinitionScannerTests {
|
||||
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context);
|
||||
scanner.setBeanNameGenerator(new TestBeanNameGenerator());
|
||||
int beanCount = scanner.scan(BASE_PACKAGE);
|
||||
assertThat(beanCount).isGreaterThanOrEqualTo(12);
|
||||
context.refresh();
|
||||
|
||||
assertThat(beanCount).isGreaterThanOrEqualTo(12);
|
||||
|
||||
context.refresh();
|
||||
FooServiceImpl fooService = context.getBean("fooService", FooServiceImpl.class);
|
||||
StaticListableBeanFactory myBf = (StaticListableBeanFactory) context.getBean("myBf");
|
||||
MessageSource ms = (MessageSource) context.getBean("messageSource");
|
||||
|
||||
assertThat(fooService.isInitCalled()).isTrue();
|
||||
assertThat(fooService.foo(123)).isEqualTo("bar");
|
||||
assertThat(fooService.lookupFoo(123)).isEqualTo("bar");
|
||||
@@ -509,9 +513,10 @@ class ClassPathBeanDefinitionScannerTests {
|
||||
scanner.setIncludeAnnotationConfig(false);
|
||||
scanner.setBeanNameGenerator(new TestBeanNameGenerator());
|
||||
int beanCount = scanner.scan(BASE_PACKAGE);
|
||||
assertThat(beanCount).isGreaterThanOrEqualTo(7);
|
||||
context.refresh();
|
||||
|
||||
assertThat(beanCount).isGreaterThanOrEqualTo(7);
|
||||
|
||||
context.refresh();
|
||||
try {
|
||||
context.getBean("fooService");
|
||||
}
|
||||
@@ -545,9 +550,10 @@ class ClassPathBeanDefinitionScannerTests {
|
||||
scanner.setAutowireCandidatePatterns("*NoSuchDao");
|
||||
scanner.scan(BASE_PACKAGE);
|
||||
context.refresh();
|
||||
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() ->
|
||||
context.getBean("fooService"))
|
||||
.satisfies(ex -> assertThat(ex.getMostSpecificCause()).isInstanceOf(NoSuchBeanDefinitionException.class));
|
||||
assertThatExceptionOfType(BeanCreationException.class)
|
||||
.isThrownBy(() -> context.getBean("fooService"))
|
||||
.satisfies(ex ->
|
||||
assertThat(ex.getMostSpecificCause()).isInstanceOf(NoSuchBeanDefinitionException.class));
|
||||
}
|
||||
|
||||
|
||||
|
||||
+34
-28
@@ -409,11 +409,14 @@ class ConfigurationClassPostProcessorTests {
|
||||
beanFactory.registerBeanDefinition("config", new RootBeanDefinition(SingletonBeanConfig.class));
|
||||
beanFactory.setAllowBeanDefinitionOverriding(false);
|
||||
ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor();
|
||||
|
||||
assertThatExceptionOfType(BeanDefinitionStoreException.class)
|
||||
.isThrownBy(() -> pp.postProcessBeanFactory(beanFactory))
|
||||
.withMessageContaining("bar")
|
||||
.withMessageContaining("SingletonBeanConfig")
|
||||
.withMessageContaining(TestBean.class.getName());
|
||||
.withMessageContainingAll(
|
||||
"bar",
|
||||
"SingletonBeanConfig",
|
||||
TestBean.class.getName()
|
||||
);
|
||||
}
|
||||
|
||||
@Test // gh-25430
|
||||
@@ -422,10 +425,13 @@ class ConfigurationClassPostProcessorTests {
|
||||
DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory();
|
||||
beanFactory.setAllowBeanDefinitionOverriding(false);
|
||||
context.register(FirstConfiguration.class, SecondConfiguration.class);
|
||||
|
||||
assertThatIllegalStateException().isThrownBy(context::refresh)
|
||||
.withMessageContaining("alias 'taskExecutor'")
|
||||
.withMessageContaining("name 'applicationTaskExecutor'")
|
||||
.withMessageContaining("bean definition 'taskExecutor'");
|
||||
.withMessageContainingAll(
|
||||
"alias 'taskExecutor'",
|
||||
"name 'applicationTaskExecutor'",
|
||||
"bean definition 'taskExecutor'"
|
||||
);
|
||||
context.close();
|
||||
}
|
||||
|
||||
@@ -978,7 +984,7 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSelfReferenceExclusionForFactoryMethodOnSameBean() {
|
||||
void selfReferenceExclusionForFactoryMethodOnSameBean() {
|
||||
AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor();
|
||||
bpp.setBeanFactory(beanFactory);
|
||||
beanFactory.addBeanPostProcessor(bpp);
|
||||
@@ -992,7 +998,7 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConfigWithDefaultMethods() {
|
||||
void configWithDefaultMethods() {
|
||||
AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor();
|
||||
bpp.setBeanFactory(beanFactory);
|
||||
beanFactory.addBeanPostProcessor(bpp);
|
||||
@@ -1006,7 +1012,7 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConfigWithDefaultMethodsUsingAsm() {
|
||||
void configWithDefaultMethodsUsingAsm() {
|
||||
AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor();
|
||||
bpp.setBeanFactory(beanFactory);
|
||||
beanFactory.addBeanPostProcessor(bpp);
|
||||
@@ -1020,7 +1026,7 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConfigWithFailingInit() { // gh-23343
|
||||
void configWithFailingInit() { // gh-23343
|
||||
AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor();
|
||||
bpp.setBeanFactory(beanFactory);
|
||||
beanFactory.addBeanPostProcessor(bpp);
|
||||
@@ -1034,7 +1040,7 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCircularDependency() {
|
||||
void circularDependency() {
|
||||
AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor();
|
||||
bpp.setBeanFactory(beanFactory);
|
||||
beanFactory.addBeanPostProcessor(bpp);
|
||||
@@ -1048,42 +1054,42 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCircularDependencyWithApplicationContext() {
|
||||
void circularDependencyWithApplicationContext() {
|
||||
assertThatExceptionOfType(BeanCreationException.class)
|
||||
.isThrownBy(() -> new AnnotationConfigApplicationContext(A.class, AStrich.class))
|
||||
.withMessageContaining("Circular reference");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPrototypeArgumentThroughBeanMethodCall() {
|
||||
void prototypeArgumentThroughBeanMethodCall() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(BeanArgumentConfigWithPrototype.class);
|
||||
ctx.getBean(FooFactory.class).createFoo(new BarArgument());
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSingletonArgumentThroughBeanMethodCall() {
|
||||
void singletonArgumentThroughBeanMethodCall() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(BeanArgumentConfigWithSingleton.class);
|
||||
ctx.getBean(FooFactory.class).createFoo(new BarArgument());
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNullArgumentThroughBeanMethodCall() {
|
||||
void nullArgumentThroughBeanMethodCall() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(BeanArgumentConfigWithNull.class);
|
||||
ctx.getBean("aFoo");
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInjectionPointMatchForNarrowTargetReturnType() {
|
||||
void injectionPointMatchForNarrowTargetReturnType() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(FooBarConfiguration.class);
|
||||
assertThat(ctx.getBean(FooImpl.class).bar).isSameAs(ctx.getBean(BarImpl.class));
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVarargOnBeanMethod() {
|
||||
void varargOnBeanMethod() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(VarargConfiguration.class, TestBean.class);
|
||||
VarargConfiguration bean = ctx.getBean(VarargConfiguration.class);
|
||||
assertThat(bean.testBeans).isNotNull();
|
||||
@@ -1093,7 +1099,7 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmptyVarargOnBeanMethod() {
|
||||
void emptyVarargOnBeanMethod() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(VarargConfiguration.class);
|
||||
VarargConfiguration bean = ctx.getBean(VarargConfiguration.class);
|
||||
assertThat(bean.testBeans).isNotNull();
|
||||
@@ -1102,7 +1108,7 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCollectionArgumentOnBeanMethod() {
|
||||
void collectionArgumentOnBeanMethod() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CollectionArgumentConfiguration.class, TestBean.class);
|
||||
CollectionArgumentConfiguration bean = ctx.getBean(CollectionArgumentConfiguration.class);
|
||||
assertThat(bean.testBeans).containsExactly(ctx.getBean(TestBean.class));
|
||||
@@ -1110,7 +1116,7 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmptyCollectionArgumentOnBeanMethod() {
|
||||
void emptyCollectionArgumentOnBeanMethod() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CollectionArgumentConfiguration.class);
|
||||
CollectionArgumentConfiguration bean = ctx.getBean(CollectionArgumentConfiguration.class);
|
||||
assertThat(bean.testBeans).isEmpty();
|
||||
@@ -1118,7 +1124,7 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMapArgumentOnBeanMethod() {
|
||||
void mapArgumentOnBeanMethod() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(MapArgumentConfiguration.class, DummyRunnable.class);
|
||||
MapArgumentConfiguration bean = ctx.getBean(MapArgumentConfiguration.class);
|
||||
assertThat(bean.testBeans).hasSize(1).containsValue(ctx.getBean(Runnable.class));
|
||||
@@ -1126,7 +1132,7 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmptyMapArgumentOnBeanMethod() {
|
||||
void emptyMapArgumentOnBeanMethod() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(MapArgumentConfiguration.class);
|
||||
MapArgumentConfiguration bean = ctx.getBean(MapArgumentConfiguration.class);
|
||||
assertThat(bean.testBeans).isEmpty();
|
||||
@@ -1134,7 +1140,7 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCollectionInjectionFromSameConfigurationClass() {
|
||||
void collectionInjectionFromSameConfigurationClass() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CollectionInjectionConfiguration.class);
|
||||
CollectionInjectionConfiguration bean = ctx.getBean(CollectionInjectionConfiguration.class);
|
||||
assertThat(bean.testBeans).containsExactly(ctx.getBean(TestBean.class));
|
||||
@@ -1142,7 +1148,7 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMapInjectionFromSameConfigurationClass() {
|
||||
void mapInjectionFromSameConfigurationClass() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(MapInjectionConfiguration.class);
|
||||
MapInjectionConfiguration bean = ctx.getBean(MapInjectionConfiguration.class);
|
||||
assertThat(bean.testBeans).containsOnly(Map.entry("testBean", ctx.getBean(Runnable.class)));
|
||||
@@ -1150,20 +1156,20 @@ class ConfigurationClassPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBeanLookupFromSameConfigurationClass() {
|
||||
void beanLookupFromSameConfigurationClass() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(BeanLookupConfiguration.class);
|
||||
assertThat(ctx.getBean(BeanLookupConfiguration.class).getTestBean()).isSameAs(ctx.getBean(TestBean.class));
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNameClashBetweenConfigurationClassAndBean() {
|
||||
void nameClashBetweenConfigurationClassAndBean() {
|
||||
assertThatExceptionOfType(BeanDefinitionStoreException.class)
|
||||
.isThrownBy(() -> new AnnotationConfigApplicationContext(MyTestBean.class).getBean("myTestBean", TestBean.class));
|
||||
.isThrownBy(() -> new AnnotationConfigApplicationContext(MyTestBean.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBeanDefinitionRegistryPostProcessorConfig() {
|
||||
void beanDefinitionRegistryPostProcessorConfig() {
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(BeanDefinitionRegistryPostProcessorConfig.class);
|
||||
assertThat(ctx.getBean("myTestBean")).isInstanceOf(TestBean.class);
|
||||
ctx.close();
|
||||
|
||||
+17
-2
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.context.event;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
@@ -105,8 +106,9 @@ class AnnotationDrivenEventListenerTests {
|
||||
this.eventCollector.assertTotalEventsCount(1);
|
||||
|
||||
this.eventCollector.clear();
|
||||
this.context.publishEvent(event);
|
||||
this.eventCollector.assertEvent(listener, event);
|
||||
TestEvent otherEvent = new TestEvent(this, Integer.valueOf(1));
|
||||
this.context.publishEvent(otherEvent);
|
||||
this.eventCollector.assertEvent(listener, otherEvent);
|
||||
this.eventCollector.assertTotalEventsCount(1);
|
||||
|
||||
context.getBean(ApplicationEventMulticaster.class).removeApplicationListeners(l ->
|
||||
@@ -742,6 +744,11 @@ class AnnotationDrivenEventListenerTests {
|
||||
public void handleString(String content) {
|
||||
collectEvent(content);
|
||||
}
|
||||
|
||||
@EventListener({Boolean.class, Integer.class})
|
||||
public void handleBooleanOrInteger(Serializable content) {
|
||||
collectEvent(content);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1009,6 +1016,8 @@ class AnnotationDrivenEventListenerTests {
|
||||
|
||||
void handleString(String payload);
|
||||
|
||||
void handleBooleanOrInteger(Serializable content);
|
||||
|
||||
void handleTimestamp(Long timestamp);
|
||||
|
||||
void handleRatio(Double ratio);
|
||||
@@ -1031,6 +1040,12 @@ class AnnotationDrivenEventListenerTests {
|
||||
super.handleString(payload);
|
||||
}
|
||||
|
||||
@EventListener({Boolean.class, Integer.class})
|
||||
@Override
|
||||
public void handleBooleanOrInteger(Serializable content) {
|
||||
super.handleBooleanOrInteger(content);
|
||||
}
|
||||
|
||||
@ConditionalEvent("#root.event.timestamp > #p0")
|
||||
@Override
|
||||
public void handleTimestamp(Long timestamp) {
|
||||
|
||||
@@ -18,11 +18,12 @@ package org.springframework.context.event.test;
|
||||
|
||||
/**
|
||||
* @author Stephane Nicoll
|
||||
* @author Juergen Hoeller
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class TestEvent extends IdentifiableApplicationEvent {
|
||||
|
||||
public final String msg;
|
||||
public final Object msg;
|
||||
|
||||
public TestEvent(Object source, String id, String msg) {
|
||||
super(source, id);
|
||||
@@ -34,6 +35,11 @@ public class TestEvent extends IdentifiableApplicationEvent {
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
public TestEvent(Object source, Integer msg) {
|
||||
super(source);
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
public TestEvent(Object source) {
|
||||
this(source, "test");
|
||||
}
|
||||
|
||||
-2
@@ -32,8 +32,6 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("removal")
|
||||
public class CandidateComponentsIndexLoaderTests {
|
||||
|
||||
@Test
|
||||
|
||||
-2
@@ -30,8 +30,6 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("removal")
|
||||
public class CandidateComponentsIndexTests {
|
||||
|
||||
@Test
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1278,29 +1278,32 @@ public class Enhancer extends AbstractClassGenerator {
|
||||
Signature bridgeTarget = (Signature) bridgeToTarget.get(method.getSignature());
|
||||
if (bridgeTarget != null) {
|
||||
// checkcast each argument against the target's argument types
|
||||
for (int i = 0; i < bridgeTarget.getArgumentTypes().length; i++) {
|
||||
Type[] argTypes = method.getSignature().getArgumentTypes();
|
||||
Type[] targetTypes = bridgeTarget.getArgumentTypes();
|
||||
for (int i = 0; i < targetTypes.length; i++) {
|
||||
e.load_arg(i);
|
||||
Type target = bridgeTarget.getArgumentTypes()[i];
|
||||
if (!target.equals(method.getSignature().getArgumentTypes()[i])) {
|
||||
Type argType = argTypes[i];
|
||||
Type target = targetTypes[i];
|
||||
if (!target.equals(argType)) {
|
||||
if (!TypeUtils.isPrimitive(target)) {
|
||||
e.box(argType);
|
||||
}
|
||||
e.checkcast(target);
|
||||
}
|
||||
}
|
||||
|
||||
e.invoke_virtual_this(bridgeTarget);
|
||||
|
||||
// Not necessary to cast if the target & bridge have the same return type.
|
||||
Type retType = method.getSignature().getReturnType();
|
||||
// Not necessary to cast if the target & bridge have
|
||||
// the same return type.
|
||||
// (This conveniently includes void and primitive types,
|
||||
// which would fail if casted. It's not possible to
|
||||
// covariant from boxed to unbox (or vice versa), so no having
|
||||
// to box/unbox for bridges).
|
||||
// TODO: It also isn't necessary to checkcast if the return is
|
||||
// assignable from the target. (This would happen if a subclass
|
||||
// used covariant returns to narrow the return type within a bridge
|
||||
// method.)
|
||||
if (!retType.equals(bridgeTarget.getReturnType())) {
|
||||
e.checkcast(retType);
|
||||
Type target = bridgeTarget.getReturnType();
|
||||
if (!target.equals(retType)) {
|
||||
if (!TypeUtils.isPrimitive(target)) {
|
||||
e.unbox(retType);
|
||||
}
|
||||
else {
|
||||
e.checkcast(retType);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -38,6 +39,7 @@ import org.springframework.util.StringUtils;
|
||||
* interface-declared parameter annotations from the concrete target method.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 6.1
|
||||
* @see #getMethodAnnotation(Class)
|
||||
* @see #getMethodParameters()
|
||||
@@ -181,7 +183,7 @@ public class AnnotatedMethod {
|
||||
clazz = null;
|
||||
}
|
||||
if (clazz != null) {
|
||||
for (Method candidate : clazz.getMethods()) {
|
||||
for (Method candidate : clazz.getDeclaredMethods()) {
|
||||
if (isOverrideFor(candidate)) {
|
||||
parameterAnnotations.add(candidate.getParameterAnnotations());
|
||||
}
|
||||
@@ -194,8 +196,9 @@ public class AnnotatedMethod {
|
||||
}
|
||||
|
||||
private boolean isOverrideFor(Method candidate) {
|
||||
if (!candidate.getName().equals(this.method.getName()) ||
|
||||
candidate.getParameterCount() != this.method.getParameterCount()) {
|
||||
if (Modifier.isPrivate(candidate.getModifiers()) ||
|
||||
!candidate.getName().equals(this.method.getName()) ||
|
||||
(candidate.getParameterCount() != this.method.getParameterCount())) {
|
||||
return false;
|
||||
}
|
||||
Class<?>[] paramTypes = this.method.getParameterTypes();
|
||||
@@ -204,7 +207,7 @@ public class AnnotatedMethod {
|
||||
}
|
||||
for (int i = 0; i < paramTypes.length; i++) {
|
||||
if (paramTypes[i] !=
|
||||
ResolvableType.forMethodParameter(candidate, i, this.method.getDeclaringClass()).resolve()) {
|
||||
ResolvableType.forMethodParameter(candidate, i, this.method.getDeclaringClass()).toClass()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -372,14 +372,14 @@ abstract class AnnotationsScanner {
|
||||
private static boolean hasSameGenericTypeParameters(
|
||||
Method rootMethod, Method candidateMethod, Class<?>[] rootParameterTypes) {
|
||||
|
||||
Class<?> sourceDeclaringClass = rootMethod.getDeclaringClass();
|
||||
Class<?> rootDeclaringClass = rootMethod.getDeclaringClass();
|
||||
Class<?> candidateDeclaringClass = candidateMethod.getDeclaringClass();
|
||||
if (!candidateDeclaringClass.isAssignableFrom(sourceDeclaringClass)) {
|
||||
if (!candidateDeclaringClass.isAssignableFrom(rootDeclaringClass)) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < rootParameterTypes.length; i++) {
|
||||
Class<?> resolvedParameterType = ResolvableType.forMethodParameter(
|
||||
candidateMethod, i, sourceDeclaringClass).resolve();
|
||||
candidateMethod, i, rootDeclaringClass).toClass();
|
||||
if (rootParameterTypes[i] != resolvedParameterType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
+12
-8
@@ -175,14 +175,18 @@ public abstract class AbstractCharSequenceDecoder<T extends CharSequence> extend
|
||||
public final T decode(DataBuffer dataBuffer, ResolvableType elementType,
|
||||
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
|
||||
|
||||
Charset charset = getCharset(mimeType);
|
||||
T value = decodeInternal(dataBuffer, charset);
|
||||
DataBufferUtils.release(dataBuffer);
|
||||
LogFormatUtils.traceDebug(logger, traceOn -> {
|
||||
String formatted = LogFormatUtils.formatValue(value, !traceOn);
|
||||
return Hints.getLogPrefix(hints) + "Decoded " + formatted;
|
||||
});
|
||||
return value;
|
||||
try {
|
||||
Charset charset = getCharset(mimeType);
|
||||
T value = decodeInternal(dataBuffer, charset);
|
||||
LogFormatUtils.traceDebug(logger, traceOn -> {
|
||||
String formatted = LogFormatUtils.formatValue(value, !traceOn);
|
||||
return Hints.getLogPrefix(hints) + "Decoded " + formatted;
|
||||
});
|
||||
return value;
|
||||
}
|
||||
finally {
|
||||
DataBufferUtils.release(dataBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
private Charset getCharset(@Nullable MimeType mimeType) {
|
||||
|
||||
+11
-1
@@ -361,12 +361,22 @@ public abstract class AbstractFileResolvingResource extends AbstractResource {
|
||||
* @throws IOException if thrown from URLConnection methods
|
||||
*/
|
||||
protected void customizeConnection(URLConnection con) throws IOException {
|
||||
ResourceUtils.useCachesIfNecessary(con);
|
||||
useCachesIfNecessary(con);
|
||||
if (con instanceof HttpURLConnection httpCon) {
|
||||
customizeConnection(httpCon);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply {@link URLConnection#setUseCaches useCaches} if necessary.
|
||||
* @param con the URLConnection to customize
|
||||
* @since 6.2.10
|
||||
* @see ResourceUtils#useCachesIfNecessary(URLConnection)
|
||||
*/
|
||||
void useCachesIfNecessary(URLConnection con) {
|
||||
ResourceUtils.useCachesIfNecessary(con);
|
||||
}
|
||||
|
||||
/**
|
||||
* Customize the given {@link HttpURLConnection} before fetching the resource.
|
||||
* <p>Can be overridden in subclasses for configuring request headers and timeouts.
|
||||
|
||||
@@ -109,7 +109,9 @@ public class FileUrlResource extends UrlResource implements WritableResource {
|
||||
|
||||
@Override
|
||||
public Resource createRelative(String relativePath) throws MalformedURLException {
|
||||
return new FileUrlResource(createRelativeURL(relativePath));
|
||||
FileUrlResource resource = new FileUrlResource(createRelativeURL(relativePath));
|
||||
resource.useCaches = this.useCaches;
|
||||
return resource;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -117,8 +117,10 @@ public interface Resource extends InputStreamSource {
|
||||
|
||||
/**
|
||||
* Return a File handle for this resource.
|
||||
* @throws java.io.FileNotFoundException if the resource cannot be resolved as
|
||||
* absolute file path, i.e. if the resource is not available in a file system
|
||||
* <p>Note: This only works for files in the default file system.
|
||||
* @throws UnsupportedOperationException if the resource is a file but cannot be
|
||||
* exposed as a {@code java.io.File}; an alternative to {@code FileNotFoundException}
|
||||
* @throws java.io.FileNotFoundException if the resource cannot be resolved as a file
|
||||
* @throws IOException in case of general resolution/reading failures
|
||||
* @see #getInputStream()
|
||||
*/
|
||||
|
||||
@@ -67,6 +67,12 @@ public class UrlResource extends AbstractFileResolvingResource {
|
||||
@Nullable
|
||||
private volatile String cleanedUrl;
|
||||
|
||||
/**
|
||||
* Whether to use URLConnection caches ({@code null} means default).
|
||||
*/
|
||||
@Nullable
|
||||
volatile Boolean useCaches;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new {@code UrlResource} based on the given URL object.
|
||||
@@ -216,11 +222,22 @@ public class UrlResource extends AbstractFileResolvingResource {
|
||||
return cleanedUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an explicit flag for {@link URLConnection#setUseCaches},
|
||||
* to be applied for any {@link URLConnection} operation in this resource.
|
||||
* <p>By default, caching will be applied only to jar resources.
|
||||
* An explicit {@code true} flag applies caching to all resources, whereas an
|
||||
* explicit {@code false} flag turns off caching for jar resources as well.
|
||||
* @since 6.2.10
|
||||
* @see ResourceUtils#useCachesIfNecessary
|
||||
*/
|
||||
public void setUseCaches(boolean useCaches) {
|
||||
this.useCaches = useCaches;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This implementation opens an InputStream for the given URL.
|
||||
* <p>It sets the {@code useCaches} flag to {@code false},
|
||||
* mainly to avoid jar file locking on Windows.
|
||||
* @see java.net.URL#openConnection()
|
||||
* @see java.net.URLConnection#setUseCaches(boolean)
|
||||
* @see java.net.URLConnection#getInputStream()
|
||||
@@ -251,6 +268,17 @@ public class UrlResource extends AbstractFileResolvingResource {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void useCachesIfNecessary(URLConnection con) {
|
||||
Boolean useCaches = this.useCaches;
|
||||
if (useCaches != null) {
|
||||
con.setUseCaches(useCaches);
|
||||
}
|
||||
else {
|
||||
super.useCachesIfNecessary(con);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation returns the underlying URL reference.
|
||||
*/
|
||||
@@ -305,7 +333,9 @@ public class UrlResource extends AbstractFileResolvingResource {
|
||||
*/
|
||||
@Override
|
||||
public Resource createRelative(String relativePath) throws MalformedURLException {
|
||||
return new UrlResource(createRelativeURL(relativePath));
|
||||
UrlResource resource = new UrlResource(createRelativeURL(relativePath));
|
||||
resource.useCaches = this.useCaches;
|
||||
return resource;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,8 +36,9 @@ import org.springframework.util.Assert;
|
||||
* <p>{@code DataBuffer}s has a separate {@linkplain #readPosition() read} and
|
||||
* {@linkplain #writePosition() write} position, as opposed to {@code ByteBuffer}'s
|
||||
* single {@linkplain ByteBuffer#position() position}. As such, the {@code DataBuffer}
|
||||
* does not require a {@linkplain ByteBuffer#flip() flip} to read after writing. In general,
|
||||
* the following invariant holds for the read and write positions, and the capacity:
|
||||
* does not require a {@linkplain ByteBuffer#flip() flip} to read after writing.
|
||||
* In general, the following invariant holds for the read and write positions,
|
||||
* and the capacity:
|
||||
*
|
||||
* <blockquote>
|
||||
* {@code 0} {@code <=}
|
||||
@@ -46,12 +47,13 @@ import org.springframework.util.Assert;
|
||||
* <i>capacity</i>
|
||||
* </blockquote>
|
||||
*
|
||||
* <p>The {@linkplain #capacity() capacity} of a {@code DataBuffer} is expanded on demand,
|
||||
* similar to {@code StringBuilder}.
|
||||
* <p>The {@linkplain #capacity() capacity} of a {@code DataBuffer} is expanded on
|
||||
* demand, similar to {@code StringBuilder}.
|
||||
*
|
||||
* <p>The main purpose of the {@code DataBuffer} abstraction is to provide a convenient wrapper
|
||||
* around {@link ByteBuffer} which is similar to Netty's {@link io.netty.buffer.ByteBuf} but
|
||||
* can also be used on non-Netty platforms (i.e. Servlet containers).
|
||||
* <p>The main purpose of the {@code DataBuffer} abstraction is to provide a
|
||||
* convenient wrapper around {@link ByteBuffer} which is similar to Netty's
|
||||
* {@link io.netty.buffer.ByteBuf} but can also be used on non-Netty platforms
|
||||
* (i.e. Servlet containers).
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Brian Clozel
|
||||
@@ -113,8 +115,8 @@ public interface DataBuffer {
|
||||
* the current capacity, it will be expanded.
|
||||
* @param capacity the new capacity
|
||||
* @return this buffer
|
||||
* @deprecated as of 6.0, in favor of {@link #ensureWritable(int)}, which
|
||||
* has different semantics
|
||||
* @deprecated as of 6.0, in favor of {@link #ensureWritable(int)}
|
||||
* which has different semantics
|
||||
*/
|
||||
@Deprecated(since = "6.0")
|
||||
DataBuffer capacity(int capacity);
|
||||
@@ -186,6 +188,28 @@ public interface DataBuffer {
|
||||
*/
|
||||
byte getByte(int index);
|
||||
|
||||
/**
|
||||
* Process a range of bytes from the current buffer using a
|
||||
* {@link ByteProcessor}.
|
||||
* @param index the index at which the processing will start
|
||||
* @param length the maximum number of bytes to be processed
|
||||
* @param processor the processor that consumes bytes
|
||||
* @return the position that was reached when processing was stopped,
|
||||
* or {@code -1} if the entire byte range was processed.
|
||||
* @throws IndexOutOfBoundsException when {@code index} is out of bounds
|
||||
* @since 6.2.12
|
||||
*/
|
||||
default int forEachByte(int index, int length, ByteProcessor processor) {
|
||||
Assert.isTrue(length >= 0, "Length must be >= 0");
|
||||
for (int position = index; position < index + length; position++) {
|
||||
byte b = getByte(position);
|
||||
if(!processor.process(b)) {
|
||||
return position;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a single byte from the current reading position from this data buffer.
|
||||
* @return the byte at this buffer's current reading position
|
||||
@@ -218,16 +242,16 @@ public interface DataBuffer {
|
||||
DataBuffer write(byte b);
|
||||
|
||||
/**
|
||||
* Write the given source into this buffer, starting at the current writing position
|
||||
* of this buffer.
|
||||
* Write the given source into this buffer, starting at the current writing
|
||||
* position of this buffer.
|
||||
* @param source the bytes to be written into this buffer
|
||||
* @return this buffer
|
||||
*/
|
||||
DataBuffer write(byte[] source);
|
||||
|
||||
/**
|
||||
* Write at most {@code length} bytes of the given source into this buffer, starting
|
||||
* at the current writing position of this buffer.
|
||||
* Write at most {@code length} bytes of the given source into this buffer,
|
||||
* starting at the current writing position of this buffer.
|
||||
* @param source the bytes to be written into this buffer
|
||||
* @param offset the index within {@code source} to start writing from
|
||||
* @param length the maximum number of bytes to be written from {@code source}
|
||||
@@ -236,8 +260,8 @@ public interface DataBuffer {
|
||||
DataBuffer write(byte[] source, int offset, int length);
|
||||
|
||||
/**
|
||||
* Write one or more {@code DataBuffer}s to this buffer, starting at the current
|
||||
* writing position. It is the responsibility of the caller to
|
||||
* Write one or more {@code DataBuffer}s to this buffer, starting at the
|
||||
* current writing position. It is the responsibility of the caller to
|
||||
* {@linkplain DataBufferUtils#release(DataBuffer) release} the given data buffers.
|
||||
* @param buffers the byte buffers to write into this buffer
|
||||
* @return this buffer
|
||||
@@ -245,8 +269,8 @@ public interface DataBuffer {
|
||||
DataBuffer write(DataBuffer... buffers);
|
||||
|
||||
/**
|
||||
* Write one or more {@link ByteBuffer} to this buffer, starting at the current
|
||||
* writing position.
|
||||
* Write one or more {@link ByteBuffer} to this buffer, starting at the
|
||||
* current writing position.
|
||||
* @param buffers the byte buffers to write into this buffer
|
||||
* @return this buffer
|
||||
*/
|
||||
@@ -299,36 +323,37 @@ public interface DataBuffer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@code DataBuffer} whose contents is a shared subsequence of this
|
||||
* data buffer's content. Data between this data buffer and the returned buffer is
|
||||
* shared; though changes in the returned buffer's position will not be reflected
|
||||
* in the reading nor writing position of this data buffer.
|
||||
* Create a new {@code DataBuffer} whose contents is a shared subsequence
|
||||
* of this data buffer's content. Data between this data buffer and the
|
||||
* returned buffer is shared; though changes in the returned buffer's
|
||||
* position will not be reflected in the reading nor writing position
|
||||
* of this data buffer.
|
||||
* <p><strong>Note</strong> that this method will <strong>not</strong> call
|
||||
* {@link DataBufferUtils#retain(DataBuffer)} on the resulting slice: the reference
|
||||
* count will not be increased.
|
||||
* {@link DataBufferUtils#retain(DataBuffer)} on the resulting slice:
|
||||
* the reference count will not be increased.
|
||||
* @param index the index at which to start the slice
|
||||
* @param length the length of the slice
|
||||
* @return the specified slice of this data buffer
|
||||
* @deprecated as of 6.0, in favor of {@link #split(int)}, which
|
||||
* has different semantics
|
||||
* @deprecated as of 6.0, in favor of {@link #split(int)}
|
||||
* which has different semantics
|
||||
*/
|
||||
@Deprecated(since = "6.0")
|
||||
DataBuffer slice(int index, int length);
|
||||
|
||||
/**
|
||||
* Create a new {@code DataBuffer} whose contents is a shared, retained subsequence of this
|
||||
* data buffer's content. Data between this data buffer and the returned buffer is
|
||||
* shared; though changes in the returned buffer's position will not be reflected
|
||||
* in the reading nor writing position of this data buffer.
|
||||
* Create a new {@code DataBuffer} whose contents is a shared, retained subsequence
|
||||
* of this data buffer's content. Data between this data buffer and the returned
|
||||
* buffer is shared; though changes in the returned buffer's position will not be
|
||||
* reflected in the reading nor writing position of this data buffer.
|
||||
* <p><strong>Note</strong> that unlike {@link #slice(int, int)}, this method
|
||||
* <strong>will</strong> call {@link DataBufferUtils#retain(DataBuffer)} (or equivalent) on the
|
||||
* resulting slice.
|
||||
* <strong>will</strong> call {@link DataBufferUtils#retain(DataBuffer)}
|
||||
* (or equivalent) on the resulting slice.
|
||||
* @param index the index at which to start the slice
|
||||
* @param length the length of the slice
|
||||
* @return the specified, retained slice of this data buffer
|
||||
* @since 5.2
|
||||
* @deprecated as of 6.0, in favor of {@link #split(int)}, which
|
||||
* has different semantics
|
||||
* @deprecated as of 6.0, in favor of {@link #split(int)}
|
||||
* which has different semantics
|
||||
*/
|
||||
@Deprecated(since = "6.0")
|
||||
default DataBuffer retainedSlice(int index, int length) {
|
||||
@@ -337,12 +362,10 @@ public interface DataBuffer {
|
||||
|
||||
/**
|
||||
* Splits this data buffer into two at the given index.
|
||||
*
|
||||
* <p>Data that precedes the {@code index} will be returned in a new buffer,
|
||||
* while this buffer will contain data that follows after {@code index}.
|
||||
* Memory between the two buffers is shared, but independent and cannot
|
||||
* overlap (unlike {@link #slice(int, int) slice}).
|
||||
*
|
||||
* <p>The {@linkplain #readPosition() read} and
|
||||
* {@linkplain #writePosition() write} position of the returned buffer are
|
||||
* truncated to fit within the buffers {@linkplain #capacity() capacity} if
|
||||
@@ -362,22 +385,22 @@ public interface DataBuffer {
|
||||
* will not be reflected in the reading nor writing position of this data buffer.
|
||||
* @return this data buffer as a byte buffer
|
||||
* @deprecated as of 6.0, in favor of {@link #toByteBuffer(ByteBuffer)},
|
||||
* {@link #readableByteBuffers()}, or {@link #writableByteBuffers()}.
|
||||
* {@link #readableByteBuffers()} or {@link #writableByteBuffers()}
|
||||
*/
|
||||
@Deprecated(since = "6.0")
|
||||
ByteBuffer asByteBuffer();
|
||||
|
||||
/**
|
||||
* Expose a subsequence of this buffer's bytes as a {@link ByteBuffer}. Data between
|
||||
* this {@code DataBuffer} and the returned {@code ByteBuffer} is shared; though
|
||||
* changes in the returned buffer's {@linkplain ByteBuffer#position() position}
|
||||
* Expose a subsequence of this buffer's bytes as a {@link ByteBuffer}. Data
|
||||
* between this {@code DataBuffer} and the returned {@code ByteBuffer} is shared;
|
||||
* though changes in the returned buffer's {@linkplain ByteBuffer#position() position}
|
||||
* will not be reflected in the reading nor writing position of this data buffer.
|
||||
* @param index the index at which to start the byte buffer
|
||||
* @param length the length of the returned byte buffer
|
||||
* @return this data buffer as a byte buffer
|
||||
* @since 5.0.1
|
||||
* @deprecated as of 6.0, in favor of {@link #toByteBuffer(int, ByteBuffer, int, int)},
|
||||
* {@link #readableByteBuffers()}, or {@link #writableByteBuffers()}.
|
||||
* {@link #readableByteBuffers()} or {@link #writableByteBuffers()}
|
||||
*/
|
||||
@Deprecated(since = "6.0")
|
||||
ByteBuffer asByteBuffer(int index, int length);
|
||||
@@ -531,4 +554,22 @@ public interface DataBuffer {
|
||||
void close();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process a range of bytes one by one.
|
||||
* @since 6.2.12
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface ByteProcessor {
|
||||
|
||||
/**
|
||||
* Process the given {@code byte} and indicate whether processing
|
||||
* should continue further.
|
||||
* @param b a byte from the {@link DataBuffer}
|
||||
* @return {@code true} if processing should continue,
|
||||
* or {@code false} if processing should stop at this element
|
||||
*/
|
||||
boolean process(byte b);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+11
@@ -31,8 +31,19 @@ package org.springframework.core.io.buffer;
|
||||
public class DataBufferLimitException extends IllegalStateException {
|
||||
|
||||
|
||||
/**
|
||||
* Create an instance with the given message.
|
||||
*/
|
||||
public DataBufferLimitException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance with a message and a cause, e.g. {@link OutOfMemoryError}.
|
||||
* @since 6.2.12
|
||||
*/
|
||||
public DataBufferLimitException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package org.springframework.core.io.buffer;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
@@ -223,17 +222,17 @@ public abstract class DataBufferUtils {
|
||||
|
||||
try {
|
||||
if (resource.isFile()) {
|
||||
File file = resource.getFile();
|
||||
Path filePath = resource.getFile().toPath();
|
||||
return readAsynchronousFileChannel(
|
||||
() -> AsynchronousFileChannel.open(file.toPath(), StandardOpenOption.READ),
|
||||
() -> AsynchronousFileChannel.open(filePath, StandardOpenOption.READ),
|
||||
position, bufferFactory, bufferSize);
|
||||
}
|
||||
}
|
||||
catch (IOException ignore) {
|
||||
catch (IOException | UnsupportedOperationException ignore) {
|
||||
// fallback to resource.readableChannel(), below
|
||||
}
|
||||
Flux<DataBuffer> result = readByteChannel(resource::readableChannel, bufferFactory, bufferSize);
|
||||
return position == 0 ? result : skipUntilByteCount(result, position);
|
||||
return (position == 0 ? result : skipUntilByteCount(result, position));
|
||||
}
|
||||
|
||||
|
||||
@@ -833,13 +832,9 @@ public abstract class DataBufferUtils {
|
||||
|
||||
@Override
|
||||
public int match(DataBuffer dataBuffer) {
|
||||
for (int pos = dataBuffer.readPosition(); pos < dataBuffer.writePosition(); pos++) {
|
||||
byte b = dataBuffer.getByte(pos);
|
||||
if (match(b)) {
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
int start = dataBuffer.readPosition();
|
||||
int end = dataBuffer.writePosition();
|
||||
return dataBuffer.forEachByte(start, end - start, b -> !this.match(b));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -882,14 +877,13 @@ public abstract class DataBufferUtils {
|
||||
|
||||
@Override
|
||||
public int match(DataBuffer dataBuffer) {
|
||||
for (int pos = dataBuffer.readPosition(); pos < dataBuffer.writePosition(); pos++) {
|
||||
byte b = dataBuffer.getByte(pos);
|
||||
if (match(b)) {
|
||||
reset();
|
||||
return pos;
|
||||
}
|
||||
int start = dataBuffer.readPosition();
|
||||
int end = dataBuffer.writePosition();
|
||||
int matchPosition = dataBuffer.forEachByte(start, end - start, b -> !this.match(b));
|
||||
if (matchPosition != -1) {
|
||||
reset();
|
||||
}
|
||||
return -1;
|
||||
return matchPosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -55,7 +55,6 @@ public final class JettyDataBuffer implements PooledDataBuffer {
|
||||
this.bufferFactory = bufferFactory;
|
||||
this.delegate = delegate;
|
||||
this.chunk = chunk;
|
||||
this.chunk.retain();
|
||||
}
|
||||
|
||||
JettyDataBuffer(JettyDataBufferFactory bufferFactory, DefaultDataBuffer delegate) {
|
||||
|
||||
@@ -129,6 +129,11 @@ public class NettyDataBuffer implements PooledDataBuffer {
|
||||
return this.byteBuf.getByte(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int forEachByte(int index, int length, ByteProcessor processor) {
|
||||
return this.byteBuf.forEachByte(index, length, processor::process);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int capacity() {
|
||||
return this.byteBuf.capacity();
|
||||
@@ -374,7 +379,13 @@ public class NettyDataBuffer implements PooledDataBuffer {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.byteBuf.toString();
|
||||
try {
|
||||
return this.byteBuf.toString();
|
||||
}
|
||||
catch (OutOfMemoryError ex) {
|
||||
throw new DataBufferLimitException(
|
||||
"Failed to convert data buffer to string: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+51
-31
@@ -41,7 +41,6 @@ import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.NavigableSet;
|
||||
@@ -260,7 +259,8 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
|
||||
private PathMatcher pathMatcher = new AntPathMatcher();
|
||||
|
||||
private boolean useCaches = true;
|
||||
@Nullable
|
||||
private Boolean useCaches;
|
||||
|
||||
private final Map<String, Resource[]> rootDirCache = new ConcurrentHashMap<>();
|
||||
|
||||
@@ -342,10 +342,12 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
* the {@link JarURLConnection} level as well as within this resolver instance.
|
||||
* <p>Note that {@link JarURLConnection#setDefaultUseCaches} can be turned off
|
||||
* independently. This resolver-level setting is designed to only enforce
|
||||
* {@code JarURLConnection#setUseCaches(false)} if necessary but otherwise
|
||||
* leaves the JVM-level default in place.
|
||||
* {@code JarURLConnection#setUseCaches(true/false)} if necessary but otherwise
|
||||
* leaves the JVM-level default in place (if this setter has not been called).
|
||||
* <p>As of 6.2.10, this setting propagates to {@link UrlResource#setUseCaches}.
|
||||
* @since 6.1.19
|
||||
* @see JarURLConnection#setUseCaches
|
||||
* @see UrlResource#setUseCaches
|
||||
* @see #clearCache()
|
||||
*/
|
||||
public void setUseCaches(boolean useCaches) {
|
||||
@@ -355,7 +357,11 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
|
||||
@Override
|
||||
public Resource getResource(String location) {
|
||||
return getResourceLoader().getResource(location);
|
||||
Resource resource = getResourceLoader().getResource(location);
|
||||
if (this.useCaches != null && resource instanceof UrlResource urlResource) {
|
||||
urlResource.setUseCaches(this.useCaches);
|
||||
}
|
||||
return resource;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -388,7 +394,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
}
|
||||
else {
|
||||
// a single resource with the given name
|
||||
return new Resource[] {getResourceLoader().getResource(locationPattern)};
|
||||
return new Resource[] {getResource(locationPattern)};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -473,20 +479,27 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
}
|
||||
}
|
||||
else {
|
||||
UrlResource resource = null;
|
||||
String urlString = url.toString();
|
||||
String cleanedPath = StringUtils.cleanPath(urlString);
|
||||
if (!cleanedPath.equals(urlString)) {
|
||||
// Prefer cleaned URL, aligned with UrlResource#createRelative(String)
|
||||
try {
|
||||
// Retain original URL instance, potentially including custom URLStreamHandler.
|
||||
return new UrlResource(new URL(url, cleanedPath));
|
||||
resource = new UrlResource(new URL(url, cleanedPath));
|
||||
}
|
||||
catch (MalformedURLException ex) {
|
||||
// Fallback to regular URL construction below...
|
||||
}
|
||||
}
|
||||
// Retain original URL instance, potentially including custom URLStreamHandler.
|
||||
return new UrlResource(url);
|
||||
if (resource == null) {
|
||||
resource = new UrlResource(url);
|
||||
}
|
||||
if (this.useCaches != null) {
|
||||
resource.setUseCaches(this.useCaches);
|
||||
}
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,6 +518,9 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
UrlResource jarResource = (ResourceUtils.URL_PROTOCOL_JAR.equals(url.getProtocol()) ?
|
||||
new UrlResource(url) :
|
||||
new UrlResource(ResourceUtils.JAR_URL_PREFIX + url + ResourceUtils.JAR_URL_SEPARATOR));
|
||||
if (this.useCaches != null) {
|
||||
jarResource.setUseCaches(this.useCaches);
|
||||
}
|
||||
if (jarResource.exists()) {
|
||||
result.add(jarResource);
|
||||
}
|
||||
@@ -556,7 +572,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
Set<ClassPathManifestEntry> entries = this.manifestEntriesCache;
|
||||
if (entries == null) {
|
||||
entries = getClassPathManifestEntries();
|
||||
if (this.useCaches) {
|
||||
if (this.useCaches == null || this.useCaches) {
|
||||
this.manifestEntriesCache = entries;
|
||||
}
|
||||
}
|
||||
@@ -577,7 +593,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
try {
|
||||
File jar = new File(path).getAbsoluteFile();
|
||||
if (jar.isFile() && seen.add(jar)) {
|
||||
manifestEntries.add(ClassPathManifestEntry.of(jar));
|
||||
manifestEntries.add(ClassPathManifestEntry.of(jar, this.useCaches));
|
||||
manifestEntries.addAll(getClassPathManifestEntriesFromJar(jar));
|
||||
}
|
||||
}
|
||||
@@ -616,7 +632,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
}
|
||||
File candidate = new File(parent, path);
|
||||
if (candidate.isFile() && candidate.getCanonicalPath().contains(parent.getCanonicalPath())) {
|
||||
manifestEntries.add(ClassPathManifestEntry.of(candidate));
|
||||
manifestEntries.add(ClassPathManifestEntry.of(candidate, this.useCaches));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -710,7 +726,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
if (rootDirResources == null) {
|
||||
// Lookup for specific directory, creating a cache entry for it.
|
||||
rootDirResources = getResources(rootDirPath);
|
||||
if (this.useCaches) {
|
||||
if (this.useCaches == null || this.useCaches) {
|
||||
this.rootDirCache.put(rootDirPath, rootDirResources);
|
||||
}
|
||||
}
|
||||
@@ -729,7 +745,11 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
if (resolvedUrl != null) {
|
||||
rootDirUrl = resolvedUrl;
|
||||
}
|
||||
rootDirResource = new UrlResource(rootDirUrl);
|
||||
UrlResource urlResource = new UrlResource(rootDirUrl);
|
||||
if (this.useCaches != null) {
|
||||
urlResource.setUseCaches(this.useCaches);
|
||||
}
|
||||
rootDirResource = urlResource;
|
||||
}
|
||||
if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
|
||||
result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
|
||||
@@ -865,8 +885,8 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
|
||||
if (con instanceof JarURLConnection jarCon) {
|
||||
// Should usually be the case for traditional JAR files.
|
||||
if (!this.useCaches) {
|
||||
jarCon.setUseCaches(false);
|
||||
if (this.useCaches != null) {
|
||||
jarCon.setUseCaches(this.useCaches);
|
||||
}
|
||||
try {
|
||||
jarFile = jarCon.getJarFile();
|
||||
@@ -916,14 +936,10 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
}
|
||||
Set<Resource> result = new LinkedHashSet<>(64);
|
||||
NavigableSet<String> entriesCache = new TreeSet<>();
|
||||
Iterator<String> entryIterator = jarFile.stream().map(JarEntry::getName).sorted().iterator();
|
||||
while (entryIterator.hasNext()) {
|
||||
String entryPath = entryIterator.next();
|
||||
int entrySeparatorIndex = entryPath.indexOf(ResourceUtils.JAR_URL_SEPARATOR);
|
||||
if (entrySeparatorIndex >= 0) {
|
||||
entryPath = entryPath.substring(entrySeparatorIndex + ResourceUtils.JAR_URL_SEPARATOR.length());
|
||||
}
|
||||
entriesCache.add(entryPath);
|
||||
for (Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements();) {
|
||||
entriesCache.add(entries.nextElement().getName());
|
||||
}
|
||||
for (String entryPath : entriesCache) {
|
||||
if (entryPath.startsWith(rootEntryPath)) {
|
||||
String relativePath = entryPath.substring(rootEntryPath.length());
|
||||
if (getPathMatcher().match(subPattern, relativePath)) {
|
||||
@@ -931,7 +947,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.useCaches) {
|
||||
if (this.useCaches == null || this.useCaches) {
|
||||
// Cache jar entries in TreeSet for efficient searching on re-encounter.
|
||||
this.jarEntriesCache.put(jarFileUrl, entriesCache);
|
||||
}
|
||||
@@ -1257,10 +1273,10 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
|
||||
private static final String JARFILE_URL_PREFIX = ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX;
|
||||
|
||||
static ClassPathManifestEntry of(File file) throws MalformedURLException {
|
||||
static ClassPathManifestEntry of(File file, @Nullable Boolean useCaches) throws MalformedURLException {
|
||||
String path = fixPath(file.getAbsolutePath());
|
||||
Resource resource = asJarFileResource(path);
|
||||
Resource alternative = createAlternative(path);
|
||||
Resource resource = asJarFileResource(path, useCaches);
|
||||
Resource alternative = createAlternative(path, useCaches);
|
||||
return new ClassPathManifestEntry(resource, alternative);
|
||||
}
|
||||
|
||||
@@ -1281,18 +1297,22 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
* @return the alternative form or {@code null}
|
||||
*/
|
||||
@Nullable
|
||||
private static Resource createAlternative(String path) {
|
||||
private static Resource createAlternative(String path, @Nullable Boolean useCaches) {
|
||||
try {
|
||||
String alternativePath = path.startsWith("/") ? path.substring(1) : "/" + path;
|
||||
return asJarFileResource(alternativePath);
|
||||
return asJarFileResource(alternativePath, useCaches);
|
||||
}
|
||||
catch (MalformedURLException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Resource asJarFileResource(String path) throws MalformedURLException {
|
||||
return new UrlResource(JARFILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR);
|
||||
private static Resource asJarFileResource(String path, @Nullable Boolean useCaches) throws MalformedURLException {
|
||||
UrlResource resource = new UrlResource(JARFILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR);
|
||||
if (useCaches != null) {
|
||||
resource.setUseCaches(useCaches);
|
||||
}
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+50
-9
@@ -92,6 +92,8 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
|
||||
@Nullable
|
||||
private Set<Thread> activeThreads;
|
||||
|
||||
private boolean cancelRemainingTasksOnClose = false;
|
||||
|
||||
private boolean rejectTasksWhenLimitReached = false;
|
||||
|
||||
private volatile boolean active = true;
|
||||
@@ -184,12 +186,33 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
|
||||
* @param timeout the timeout in milliseconds
|
||||
* @since 6.1
|
||||
* @see #close()
|
||||
* @see #setCancelRemainingTasksOnClose
|
||||
* @see org.springframework.scheduling.concurrent.ExecutorConfigurationSupport#setAwaitTerminationMillis
|
||||
*/
|
||||
public void setTaskTerminationTimeout(long timeout) {
|
||||
Assert.isTrue(timeout >= 0, "Timeout value must be >=0");
|
||||
this.taskTerminationTimeout = timeout;
|
||||
this.activeThreads = (timeout > 0 ? ConcurrentHashMap.newKeySet() : null);
|
||||
trackActiveThreadsIfNecessary();
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify whether to cancel remaining tasks on close: that is, whether to
|
||||
* interrupt any active threads at the time of the {@link #close()} call.
|
||||
* <p>The default is {@code false}, not tracking active threads at all or
|
||||
* just interrupting any remaining threads that still have not finished after
|
||||
* the specified {@link #setTaskTerminationTimeout taskTerminationTimeout}.
|
||||
* Switch this to {@code true} for immediate interruption on close, either in
|
||||
* combination with a subsequent termination timeout or without any waiting
|
||||
* at all, depending on whether a {@code taskTerminationTimeout} has been
|
||||
* specified as well.
|
||||
* @since 6.2.11
|
||||
* @see #close()
|
||||
* @see #setTaskTerminationTimeout
|
||||
* @see org.springframework.scheduling.concurrent.ExecutorConfigurationSupport#setWaitForTasksToCompleteOnShutdown
|
||||
*/
|
||||
public void setCancelRemainingTasksOnClose(boolean cancelRemainingTasksOnClose) {
|
||||
this.cancelRemainingTasksOnClose = cancelRemainingTasksOnClose;
|
||||
trackActiveThreadsIfNecessary();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -249,6 +272,15 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
|
||||
return this.active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track active threads only when a task termination timeout has been
|
||||
* specified or interruption of remaining threads has been requested.
|
||||
*/
|
||||
private void trackActiveThreadsIfNecessary() {
|
||||
this.activeThreads = (this.taskTerminationTimeout > 0 || this.cancelRemainingTasksOnClose ?
|
||||
ConcurrentHashMap.newKeySet() : null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Executes the given task, within a concurrency throttle
|
||||
@@ -353,7 +385,7 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
|
||||
}
|
||||
|
||||
/**
|
||||
* This close methods tracks the termination of active threads if a concrete
|
||||
* This close method tracks the termination of active threads if a concrete
|
||||
* {@link #setTaskTerminationTimeout task termination timeout} has been set.
|
||||
* Otherwise, it is not necessary to close this executor.
|
||||
* @since 6.1
|
||||
@@ -364,15 +396,24 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
|
||||
this.active = false;
|
||||
Set<Thread> threads = this.activeThreads;
|
||||
if (threads != null) {
|
||||
threads.forEach(Thread::interrupt);
|
||||
synchronized (threads) {
|
||||
try {
|
||||
if (!threads.isEmpty()) {
|
||||
threads.wait(this.taskTerminationTimeout);
|
||||
if (this.cancelRemainingTasksOnClose) {
|
||||
// Early interrupt for remaining tasks on close
|
||||
threads.forEach(Thread::interrupt);
|
||||
}
|
||||
if (this.taskTerminationTimeout > 0) {
|
||||
synchronized (threads) {
|
||||
try {
|
||||
if (!threads.isEmpty()) {
|
||||
threads.wait(this.taskTerminationTimeout);
|
||||
}
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
if (!this.cancelRemainingTasksOnClose) {
|
||||
// Late interrupt for remaining tasks after timeout
|
||||
threads.forEach(Thread::interrupt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,7 @@ import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link TaskExecutor} implementation that executes each task <i>synchronously</i>
|
||||
* in the calling thread.
|
||||
*
|
||||
* <p>Mainly intended for testing scenarios.
|
||||
* in the calling thread. Mainly intended for testing scenarios.
|
||||
*
|
||||
* <p>Execution in the calling thread does have the advantage of participating
|
||||
* in its thread context, for example the thread context class loader or the
|
||||
@@ -40,13 +38,13 @@ import org.springframework.util.Assert;
|
||||
public class SyncTaskExecutor implements TaskExecutor, Serializable {
|
||||
|
||||
/**
|
||||
* Executes the given {@code task} synchronously, through direct
|
||||
* invocation of it's {@link Runnable#run() run()} method.
|
||||
* @throws IllegalArgumentException if the given {@code task} is {@code null}
|
||||
* Execute the given {@code task} synchronously, through direct
|
||||
* invocation of its {@link Runnable#run() run()} method.
|
||||
* @throws RuntimeException if propagated from the given {@code Runnable}
|
||||
*/
|
||||
@Override
|
||||
public void execute(Runnable task) {
|
||||
Assert.notNull(task, "Runnable must not be null");
|
||||
Assert.notNull(task, "Task must not be null");
|
||||
task.run();
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,8 @@ import java.lang.annotation.Target;
|
||||
* <p>The additional return values denote the following:
|
||||
* <ul>
|
||||
* <li>{@code fail} - the method throws an exception, if the arguments satisfy argument constraints
|
||||
* <li>{@code new} - the method returns a non-null new object which is distinct from any other object existing in the heap prior to method execution. If method is also pure, then we can be sure that the new object is not stored to any field/array and will be lost if method return value is not used.
|
||||
* <li>{@code new} - the method returns a non-null new object which is distinct from any other object existing in the heap prior to method execution.
|
||||
* If the method has no visible side effects, then we can be sure that the new object is not stored to any field/array and will be lost if the method's return value is not used.
|
||||
* <li>{@code this} - the method returns its qualifier value (not applicable for static methods)
|
||||
* <li>{@code param1, param2, ...} - the method returns its first (second, ...) parameter value
|
||||
* </ul>
|
||||
|
||||
@@ -1483,8 +1483,8 @@ public abstract class ClassUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the highest publicly accessible method in the supplied method's type hierarchy that
|
||||
* has a method signature equivalent to the supplied method, if possible.
|
||||
* Get the closest publicly accessible (and exported) method in the supplied method's type
|
||||
* hierarchy that has a method signature equivalent to the supplied method, if possible.
|
||||
* <p>Otherwise, this method recursively searches the class hierarchy and implemented
|
||||
* interfaces for an equivalent method that is {@code public} and declared in a
|
||||
* {@code public} type.
|
||||
@@ -1507,18 +1507,21 @@ public abstract class ClassUtils {
|
||||
* @see #getMostSpecificMethod(Method, Class)
|
||||
*/
|
||||
public static Method getPubliclyAccessibleMethodIfPossible(Method method, @Nullable Class<?> targetClass) {
|
||||
// If the method is not public, we can abort the search immediately.
|
||||
if (!Modifier.isPublic(method.getModifiers())) {
|
||||
Class<?> declaringClass = method.getDeclaringClass();
|
||||
// If the method is not public or its declaring class is public and exported already,
|
||||
// we can abort the search immediately (avoiding reflection as well as cache access).
|
||||
if (!Modifier.isPublic(method.getModifiers()) || (Modifier.isPublic(declaringClass.getModifiers()) &&
|
||||
declaringClass.getModule().isExported(declaringClass.getPackageName(), ClassUtils.class.getModule()))) {
|
||||
return method;
|
||||
}
|
||||
|
||||
Method interfaceMethod = getInterfaceMethodIfPossible(method, targetClass, true);
|
||||
// If we found a method in a public interface, return the interface method.
|
||||
if (interfaceMethod != method) {
|
||||
if (interfaceMethod != method && interfaceMethod.getDeclaringClass().getModule().isExported(
|
||||
interfaceMethod.getDeclaringClass().getPackageName(), ClassUtils.class.getModule())) {
|
||||
return interfaceMethod;
|
||||
}
|
||||
|
||||
Class<?> declaringClass = method.getDeclaringClass();
|
||||
// Bypass cache for java.lang.Object unless it is actually an overridable method declared there.
|
||||
if (declaringClass.getSuperclass() == Object.class && !ReflectionUtils.isObjectMethod(method)) {
|
||||
return method;
|
||||
@@ -1533,19 +1536,20 @@ public abstract class ClassUtils {
|
||||
private static Method findPubliclyAccessibleMethodIfPossible(
|
||||
String methodName, Class<?>[] parameterTypes, Class<?> declaringClass) {
|
||||
|
||||
Method result = null;
|
||||
Class<?> current = declaringClass.getSuperclass();
|
||||
while (current != null) {
|
||||
Method method = getMethodOrNull(current, methodName, parameterTypes);
|
||||
if (method == null) {
|
||||
break;
|
||||
}
|
||||
if (Modifier.isPublic(method.getDeclaringClass().getModifiers())) {
|
||||
result = method;
|
||||
if (Modifier.isPublic(method.getDeclaringClass().getModifiers()) &&
|
||||
method.getDeclaringClass().getModule().isExported(
|
||||
method.getDeclaringClass().getPackageName(), ClassUtils.class.getModule())) {
|
||||
return method;
|
||||
}
|
||||
current = method.getDeclaringClass().getSuperclass();
|
||||
}
|
||||
return result;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -86,13 +86,12 @@ public abstract class FileSystemUtils {
|
||||
|
||||
Files.walkFileTree(root, new SimpleFileVisitor<>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attr) throws IOException {
|
||||
Files.delete(file);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException ex) throws IOException {
|
||||
Files.delete(dir);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
@@ -127,19 +126,34 @@ public abstract class FileSystemUtils {
|
||||
BasicFileAttributes srcAttr = Files.readAttributes(src, BasicFileAttributes.class);
|
||||
|
||||
if (srcAttr.isDirectory()) {
|
||||
Files.walkFileTree(src, EnumSet.of(FOLLOW_LINKS), Integer.MAX_VALUE, new SimpleFileVisitor<>() {
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
||||
Files.createDirectories(dest.resolve(src.relativize(dir)));
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
Files.copy(file, dest.resolve(src.relativize(file)), StandardCopyOption.REPLACE_EXISTING);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
if (src.getClass() == dest.getClass()) { // dest.resolve(Path) only works for same Path type
|
||||
Files.walkFileTree(src, EnumSet.of(FOLLOW_LINKS), Integer.MAX_VALUE, new SimpleFileVisitor<>() {
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attr) throws IOException {
|
||||
Files.createDirectories(dest.resolve(src.relativize(dir)));
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attr) throws IOException {
|
||||
Files.copy(file, dest.resolve(src.relativize(file)), StandardCopyOption.REPLACE_EXISTING);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
}
|
||||
else { // use dest.resolve(String) for different Path types
|
||||
Files.walkFileTree(src, EnumSet.of(FOLLOW_LINKS), Integer.MAX_VALUE, new SimpleFileVisitor<>() {
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attr) throws IOException {
|
||||
Files.createDirectories(dest.resolve(src.relativize(dir).toString()));
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attr) throws IOException {
|
||||
Files.copy(file, dest.resolve(src.relativize(file).toString()), StandardCopyOption.REPLACE_EXISTING);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (srcAttr.isRegularFile()) {
|
||||
Files.copy(src, dest);
|
||||
|
||||
@@ -18,7 +18,6 @@ package org.springframework.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -36,10 +35,10 @@ import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
|
||||
* that can be resolved using a {@link PlaceholderResolver PlaceholderResolver},
|
||||
* <code>${</code> the prefix, and <code>}</code> the suffix.
|
||||
*
|
||||
* <p>A placeholder can also have a default value if its key does not represent a
|
||||
* known property. The default value is separated from the key using a
|
||||
* {@code separator}. For instance {@code ${name:John}} resolves to {@code John} if
|
||||
* the placeholder resolver does not provide a value for the {@code name}
|
||||
* <p>A placeholder can also have a default value if its key does not represent
|
||||
* a known property. The default value is separated from the key using a
|
||||
* {@code separator}. For instance {@code ${name:John}} resolves to {@code John}
|
||||
* if the placeholder resolver does not provide a value for the {@code name}
|
||||
* property.
|
||||
*
|
||||
* <p>Placeholders can also have a more complex structure, and the resolution of
|
||||
@@ -50,13 +49,14 @@ import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
|
||||
* must be rendered as is, the placeholder can be escaped using an {@code escape}
|
||||
* character. For instance {@code \${name}} resolves as {@code ${name}}.
|
||||
*
|
||||
* <p>The prefix, suffix, separator, and escape characters are configurable. Only
|
||||
* the prefix and suffix are mandatory, and the support for default values or
|
||||
* escaping is conditional on providing non-null values for them.
|
||||
* <p>The prefix, suffix, separator, and escape characters are configurable.
|
||||
* Only the prefix and suffix are mandatory, and the support for default values
|
||||
* or escaping is conditional on providing non-null values for them.
|
||||
*
|
||||
* <p>This parser makes sure to resolves placeholders as lazily as possible.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Juergen Hoeller
|
||||
* @since 6.2
|
||||
*/
|
||||
final class PlaceholderParser {
|
||||
@@ -120,51 +120,47 @@ final class PlaceholderParser {
|
||||
* @return the supplied value with placeholders replaced inline
|
||||
*/
|
||||
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
|
||||
Assert.notNull(value, "'value' must not be null");
|
||||
ParsedValue parsedValue = parse(value);
|
||||
List<Part> parts = parse(value, false);
|
||||
if (parts == null) {
|
||||
return value;
|
||||
}
|
||||
ParsedValue parsedValue = new ParsedValue(value, parts);
|
||||
PartResolutionContext resolutionContext = new PartResolutionContext(placeholderResolver,
|
||||
this.prefix, this.suffix, this.ignoreUnresolvablePlaceholders,
|
||||
candidate -> parse(candidate, false));
|
||||
return parsedValue.resolve(resolutionContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the specified value.
|
||||
* @param value the value containing the placeholders to be replaced
|
||||
* @return the different parts that have been identified
|
||||
*/
|
||||
ParsedValue parse(String value) {
|
||||
List<Part> parts = parse(value, false);
|
||||
return new ParsedValue(value, parts);
|
||||
}
|
||||
|
||||
private List<Part> parse(String value, boolean inPlaceholder) {
|
||||
LinkedList<Part> parts = new LinkedList<>();
|
||||
private @Nullable List<Part> parse(String value, boolean inPlaceholder) {
|
||||
int startIndex = nextStartPrefix(value, 0);
|
||||
if (startIndex == -1) {
|
||||
Part part = (inPlaceholder ? createSimplePlaceholderPart(value) : new TextPart(value));
|
||||
parts.add(part);
|
||||
return parts;
|
||||
return null;
|
||||
}
|
||||
List<Part> parts = new ArrayList<>(4);
|
||||
int position = 0;
|
||||
while (startIndex != -1) {
|
||||
int endIndex = nextValidEndPrefix(value, startIndex);
|
||||
if (endIndex == -1) { // Not a valid placeholder, consume the prefix and continue
|
||||
if (endIndex == -1) { // Not a valid placeholder, consume the prefix and continue
|
||||
addText(value, position, startIndex + this.prefix.length(), parts);
|
||||
position = startIndex + this.prefix.length();
|
||||
startIndex = nextStartPrefix(value, position);
|
||||
}
|
||||
else if (isEscaped(value, startIndex)) { // Not a valid index, accumulate and skip the escape character
|
||||
else if (isEscaped(value, startIndex)) { // Not a valid index, accumulate and skip the escape character
|
||||
addText(value, position, startIndex - 1, parts);
|
||||
addText(value, startIndex, startIndex + this.prefix.length(), parts);
|
||||
position = startIndex + this.prefix.length();
|
||||
startIndex = nextStartPrefix(value, position);
|
||||
}
|
||||
else { // Found valid placeholder, recursive parsing
|
||||
else { // Found valid placeholder, recursive parsing
|
||||
addText(value, position, startIndex, parts);
|
||||
String placeholder = value.substring(startIndex + this.prefix.length(), endIndex);
|
||||
List<Part> placeholderParts = parse(placeholder, true);
|
||||
parts.addAll(placeholderParts);
|
||||
if (placeholderParts == null) {
|
||||
parts.add(createSimplePlaceholderPart(placeholder));
|
||||
}
|
||||
else {
|
||||
parts.addAll(placeholderParts);
|
||||
}
|
||||
startIndex = nextStartPrefix(value, endIndex + this.suffix.length());
|
||||
position = endIndex + this.suffix.length();
|
||||
}
|
||||
@@ -241,29 +237,6 @@ final class PlaceholderParser {
|
||||
return new ParsedSection(buffer.toString(), null);
|
||||
}
|
||||
|
||||
private static void addText(String value, int start, int end, LinkedList<Part> parts) {
|
||||
if (start > end) {
|
||||
return;
|
||||
}
|
||||
String text = value.substring(start, end);
|
||||
if (!text.isEmpty()) {
|
||||
if (!parts.isEmpty()) {
|
||||
Part current = parts.removeLast();
|
||||
if (current instanceof TextPart textPart) {
|
||||
parts.add(new TextPart(textPart.text() + text));
|
||||
}
|
||||
else {
|
||||
parts.add(current);
|
||||
parts.add(new TextPart(text));
|
||||
}
|
||||
}
|
||||
else {
|
||||
parts.add(new TextPart(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private int nextStartPrefix(String value, int index) {
|
||||
return value.indexOf(this.prefix, index);
|
||||
}
|
||||
@@ -296,15 +269,46 @@ final class PlaceholderParser {
|
||||
return (this.escape != null && index > 0 && value.charAt(index - 1) == this.escape);
|
||||
}
|
||||
|
||||
record ParsedSection(String key, @Nullable String fallback) {
|
||||
private static void addText(String value, int start, int end, List<Part> parts) {
|
||||
if (start >= end) {
|
||||
return;
|
||||
}
|
||||
String text = value.substring(start, end);
|
||||
if (!parts.isEmpty() && parts.get(parts.size() - 1) instanceof TextPart textPart) {
|
||||
parts.set(parts.size() - 1, new TextPart(textPart.text() + text));
|
||||
}
|
||||
else {
|
||||
parts.add(new TextPart(text));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A representation of the parsing of an input string.
|
||||
* @param text the raw input string
|
||||
* @param parts the parts that appear in the string, in order
|
||||
*/
|
||||
private record ParsedValue(String text, List<Part> parts) {
|
||||
|
||||
public String resolve(PartResolutionContext resolutionContext) {
|
||||
try {
|
||||
return Part.resolveAll(this.parts, resolutionContext);
|
||||
}
|
||||
catch (PlaceholderResolutionException ex) {
|
||||
throw ex.withValue(this.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private record ParsedSection(String key, @Nullable String fallback) {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Provide the necessary context to handle and resolve underlying placeholders.
|
||||
*/
|
||||
static class PartResolutionContext implements PlaceholderResolver {
|
||||
private static class PartResolutionContext implements PlaceholderResolver {
|
||||
|
||||
private final String prefix;
|
||||
|
||||
@@ -319,7 +323,6 @@ final class PlaceholderParser {
|
||||
@Nullable
|
||||
private Set<String> visitedPlaceholders;
|
||||
|
||||
|
||||
PartResolutionContext(PlaceholderResolver resolver, String prefix, String suffix,
|
||||
boolean ignoreUnresolvablePlaceholders, Function<String, List<Part>> parser) {
|
||||
this.prefix = prefix;
|
||||
@@ -352,7 +355,7 @@ final class PlaceholderParser {
|
||||
return this.prefix + text + this.suffix;
|
||||
}
|
||||
|
||||
public List<Part> parse(String text) {
|
||||
public @Nullable List<Part> parse(String text) {
|
||||
return this.parser.apply(text);
|
||||
}
|
||||
|
||||
@@ -367,17 +370,17 @@ final class PlaceholderParser {
|
||||
}
|
||||
|
||||
public void removePlaceholder(String placeholder) {
|
||||
Assert.state(this.visitedPlaceholders != null, "Visited placeholders must not be null");
|
||||
this.visitedPlaceholders.remove(placeholder);
|
||||
if (this.visitedPlaceholders != null) {
|
||||
this.visitedPlaceholders.remove(placeholder);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A part is a section of a String containing placeholders to replace.
|
||||
*/
|
||||
interface Part {
|
||||
private interface Part {
|
||||
|
||||
/**
|
||||
* Resolve this part using the specified {@link PartResolutionContext}.
|
||||
@@ -408,30 +411,12 @@ final class PlaceholderParser {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A representation of the parsing of an input string.
|
||||
* @param text the raw input string
|
||||
* @param parts the parts that appear in the string, in order
|
||||
*/
|
||||
record ParsedValue(String text, List<Part> parts) {
|
||||
|
||||
public String resolve(PartResolutionContext resolutionContext) {
|
||||
try {
|
||||
return Part.resolveAll(this.parts, resolutionContext);
|
||||
}
|
||||
catch (PlaceholderResolutionException ex) {
|
||||
throw ex.withValue(this.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A base {@link Part} implementation.
|
||||
*/
|
||||
abstract static class AbstractPart implements Part {
|
||||
private abstract static class AbstractPart implements Part {
|
||||
|
||||
private final String text;
|
||||
final String text;
|
||||
|
||||
protected AbstractPart(String text) {
|
||||
this.text = text;
|
||||
@@ -454,29 +439,19 @@ final class PlaceholderParser {
|
||||
@Nullable
|
||||
protected String resolveRecursively(PartResolutionContext resolutionContext, String key) {
|
||||
String resolvedValue = resolutionContext.resolvePlaceholder(key);
|
||||
if (resolvedValue != null) {
|
||||
resolutionContext.flagPlaceholderAsVisited(key);
|
||||
// Let's check if we need to recursively resolve that value
|
||||
List<Part> nestedParts = resolutionContext.parse(resolvedValue);
|
||||
String value = toText(nestedParts);
|
||||
if (!isTextOnly(nestedParts)) {
|
||||
value = new ParsedValue(resolvedValue, nestedParts).resolve(resolutionContext);
|
||||
}
|
||||
resolutionContext.removePlaceholder(key);
|
||||
return value;
|
||||
if (resolvedValue == null) {
|
||||
// Not found
|
||||
return null;
|
||||
}
|
||||
// Not found
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isTextOnly(List<Part> parts) {
|
||||
return parts.stream().allMatch(TextPart.class::isInstance);
|
||||
}
|
||||
|
||||
private String toText(List<Part> parts) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
parts.forEach(part -> sb.append(part.text()));
|
||||
return sb.toString();
|
||||
// Let's check if we need to recursively resolve that value
|
||||
List<Part> nestedParts = resolutionContext.parse(resolvedValue);
|
||||
if (nestedParts == null) {
|
||||
return resolvedValue;
|
||||
}
|
||||
resolutionContext.flagPlaceholderAsVisited(key);
|
||||
String value = new ParsedValue(resolvedValue, nestedParts).resolve(resolutionContext);
|
||||
resolutionContext.removePlaceholder(key);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -484,7 +459,7 @@ final class PlaceholderParser {
|
||||
/**
|
||||
* A {@link Part} implementation that does not contain a valid placeholder.
|
||||
*/
|
||||
static class TextPart extends AbstractPart {
|
||||
private static class TextPart extends AbstractPart {
|
||||
|
||||
/**
|
||||
* Create a new instance.
|
||||
@@ -496,7 +471,7 @@ final class PlaceholderParser {
|
||||
|
||||
@Override
|
||||
public String resolve(PartResolutionContext resolutionContext) {
|
||||
return text();
|
||||
return this.text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,7 +480,7 @@ final class PlaceholderParser {
|
||||
* A {@link Part} implementation that represents a single placeholder with
|
||||
* a hard-coded fallback.
|
||||
*/
|
||||
static class SimplePlaceholderPart extends AbstractPart {
|
||||
private static class SimplePlaceholderPart extends AbstractPart {
|
||||
|
||||
private final String key;
|
||||
|
||||
@@ -533,13 +508,13 @@ final class PlaceholderParser {
|
||||
else if (this.fallback != null) {
|
||||
return this.fallback;
|
||||
}
|
||||
return resolutionContext.handleUnresolvablePlaceholder(this.key, text());
|
||||
return resolutionContext.handleUnresolvablePlaceholder(this.key, this.text);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String resolveRecursively(PartResolutionContext resolutionContext) {
|
||||
if (!this.text().equals(this.key)) {
|
||||
String value = resolveRecursively(resolutionContext, this.text());
|
||||
if (!this.text.equals(this.key)) {
|
||||
String value = resolveRecursively(resolutionContext, this.text);
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
@@ -553,7 +528,7 @@ final class PlaceholderParser {
|
||||
* A {@link Part} implementation that represents a single placeholder
|
||||
* containing nested placeholders.
|
||||
*/
|
||||
static class NestedPlaceholderPart extends AbstractPart {
|
||||
private static class NestedPlaceholderPart extends AbstractPart {
|
||||
|
||||
private final List<Part> keyParts;
|
||||
|
||||
@@ -582,7 +557,7 @@ final class PlaceholderParser {
|
||||
else if (this.defaultParts != null) {
|
||||
return Part.resolveAll(this.defaultParts, resolutionContext);
|
||||
}
|
||||
return resolutionContext.handleUnresolvablePlaceholder(resolvedKey, text());
|
||||
return resolutionContext.handleUnresolvablePlaceholder(resolvedKey, this.text);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ public class PropertyPlaceholderHelper {
|
||||
* @param properties the {@code Properties} to use for replacement
|
||||
* @return the supplied value with placeholders replaced inline
|
||||
*/
|
||||
public String replacePlaceholders(String value, final Properties properties) {
|
||||
public String replacePlaceholders(String value, Properties properties) {
|
||||
Assert.notNull(properties, "'properties' must not be null");
|
||||
return replacePlaceholders(value, properties::getProperty);
|
||||
}
|
||||
@@ -111,9 +111,10 @@ public class PropertyPlaceholderHelper {
|
||||
*/
|
||||
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
|
||||
Assert.notNull(value, "'value' must not be null");
|
||||
return parseStringValue(value, placeholderResolver);
|
||||
return this.parser.replacePlaceholders(value, placeholderResolver);
|
||||
}
|
||||
|
||||
@Deprecated(since = "6.2.12", forRemoval = true)
|
||||
protected String parseStringValue(String value, PlaceholderResolver placeholderResolver) {
|
||||
return this.parser.replacePlaceholders(value, placeholderResolver);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package org.springframework.util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
@@ -25,6 +24,7 @@ import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
@@ -803,54 +803,60 @@ public abstract class StringUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the given encoded URI component value. Based on the following rules:
|
||||
* <ul>
|
||||
* <li>Alphanumeric characters {@code "a"} through {@code "z"}, {@code "A"} through {@code "Z"},
|
||||
* and {@code "0"} through {@code "9"} stay the same.</li>
|
||||
* <li>Special characters {@code "-"}, {@code "_"}, {@code "."}, and {@code "*"} stay the same.</li>
|
||||
* <li>A sequence "<i>{@code %xy}</i>" is interpreted as a hexadecimal representation of the character.</li>
|
||||
* <li>For all other characters (including those already decoded), the output is undefined.</li>
|
||||
* </ul>
|
||||
* @param source the encoded String
|
||||
* @param charset the character set
|
||||
* Decode the given encoded URI component value by replacing "<i>{@code %xy}</i>" sequences
|
||||
* by an hexadecimal representation of the character in the specified charset, letting other
|
||||
* characters unchanged.
|
||||
* @param source the encoded {@code String}
|
||||
* @param charset the character encoding to use to decode the "<i>{@code %xy}</i>" sequences
|
||||
* @return the decoded value
|
||||
* @throws IllegalArgumentException when the given source contains invalid encoded sequences
|
||||
* @since 5.0
|
||||
* @see java.net.URLDecoder#decode(String, String)
|
||||
* @see java.net.URLDecoder#decode(String, String) java.net.URLDecoder#decode for HTML form decoding
|
||||
*/
|
||||
public static String uriDecode(String source, Charset charset) {
|
||||
int length = source.length();
|
||||
if (length == 0) {
|
||||
int firstPercentIndex = source.indexOf('%');
|
||||
if (length == 0 || firstPercentIndex < 0) {
|
||||
return source;
|
||||
}
|
||||
Assert.notNull(charset, "Charset must not be null");
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(length);
|
||||
boolean changed = false;
|
||||
for (int i = 0; i < length; i++) {
|
||||
int ch = source.charAt(i);
|
||||
StringBuilder output = new StringBuilder(length);
|
||||
output.append(source, 0, firstPercentIndex);
|
||||
byte[] bytes = null;
|
||||
int i = firstPercentIndex;
|
||||
while (i < length) {
|
||||
char ch = source.charAt(i);
|
||||
if (ch == '%') {
|
||||
if (i + 2 < length) {
|
||||
char hex1 = source.charAt(i + 1);
|
||||
char hex2 = source.charAt(i + 2);
|
||||
int u = Character.digit(hex1, 16);
|
||||
int l = Character.digit(hex2, 16);
|
||||
if (u == -1 || l == -1) {
|
||||
throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\"");
|
||||
try {
|
||||
if (bytes == null) {
|
||||
bytes = new byte[(length - i) / 3];
|
||||
}
|
||||
baos.write((char) ((u << 4) + l));
|
||||
i += 2;
|
||||
changed = true;
|
||||
|
||||
int pos = 0;
|
||||
while (i + 2 < length && ch == '%') {
|
||||
bytes[pos++] = (byte) HexFormat.fromHexDigits(source, i + 1, i + 3);
|
||||
i += 3;
|
||||
if (i < length) {
|
||||
ch = source.charAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (i < length && ch == '%') {
|
||||
throw new IllegalArgumentException("Incomplete trailing escape (%) pattern");
|
||||
}
|
||||
|
||||
output.append(new String(bytes, 0, pos, charset));
|
||||
}
|
||||
else {
|
||||
catch (NumberFormatException ex) {
|
||||
throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\"");
|
||||
}
|
||||
}
|
||||
else {
|
||||
baos.write(ch);
|
||||
output.append(ch);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return (changed ? StreamUtils.copyToString(baos, charset) : source);
|
||||
return output.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -137,14 +137,19 @@ public final class DataSize implements Comparable<DataSize>, Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a {@link DataSize} from a text string such as {@code 12MB} using
|
||||
* Obtain a {@link DataSize} from a text string such as {@code "5MB"} using
|
||||
* {@link DataUnit#BYTES} if no unit is specified.
|
||||
* <p>Examples:
|
||||
* <pre>
|
||||
* "12KB" -- parses as "12 kilobytes"
|
||||
* "5MB" -- parses as "5 megabytes"
|
||||
* "20" -- parses as "20 bytes"
|
||||
* </pre>
|
||||
* <h4>Examples</h4>
|
||||
* <table border="1">
|
||||
* <tr><th>Text</th><th>Parsed As</th><th>Size in Bytes</th></tr>
|
||||
* <tr><td>"20"</td><td>20 bytes</td><td>20</td></tr>
|
||||
* <tr><td>"20B"</td><td>20 bytes</td><td>20</td></tr>
|
||||
* <tr><td>"12KB"</td><td>12 kilobytes</td><td>12,288</td></tr>
|
||||
* <tr><td>"5MB"</td><td>5 megabytes</td><td>5,242,880</td></tr>
|
||||
* </table>
|
||||
* <p>Note that the terms and units used in the above examples are based on
|
||||
* <a href="https://en.wikipedia.org/wiki/Binary_prefix">binary prefixes</a>.
|
||||
* Consult the {@linkplain DataSize class-level Javadoc} for details.
|
||||
* @param text the text to parse
|
||||
* @return the parsed {@code DataSize}
|
||||
* @see #parse(CharSequence, DataUnit)
|
||||
@@ -154,19 +159,24 @@ public final class DataSize implements Comparable<DataSize>, Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a {@link DataSize} from a text string such as {@code 12MB} using
|
||||
* Obtain a {@link DataSize} from a text string such as {@code "5MB"} using
|
||||
* the specified default {@link DataUnit} if no unit is specified.
|
||||
* <p>The string starts with a number followed optionally by a unit matching
|
||||
* one of the supported {@linkplain DataUnit suffixes}.
|
||||
* <p>If neither a unit nor a default {@code DataUnit} is specified,
|
||||
* {@link DataUnit#BYTES} will be inferred.
|
||||
* <p>Examples:
|
||||
* <pre>
|
||||
* "12KB" -- parses as "12 kilobytes"
|
||||
* "5MB" -- parses as "5 megabytes"
|
||||
* "20" -- parses as "20 kilobytes" (where the {@code defaultUnit} is {@link DataUnit#KILOBYTES})
|
||||
* "20" -- parses as "20 bytes" (if the {@code defaultUnit} is {@code null})
|
||||
* </pre>
|
||||
* <h4>Examples</h4>
|
||||
* <table border="1">
|
||||
* <tr><th>Text</th><th>Default Unit</th><th>Parsed As</th><th>Size in Bytes</th></tr>
|
||||
* <tr><td>"20"</td><td>{@code null}</td><td>20 bytes</td><td>20</td></tr>
|
||||
* <tr><td>"20"</td><td>{@link DataUnit#KILOBYTES KILOBYTES}</td><td>20 kilobytes</td><td>20,480</td></tr>
|
||||
* <tr><td>"20B"</td><td>N/A</td><td>20 bytes</td><td>20</td></tr>
|
||||
* <tr><td>"12KB"</td><td>N/A</td><td>12 kilobytes</td><td>12,288</td></tr>
|
||||
* <tr><td>"5MB"</td><td>N/A</td><td>5 megabytes</td><td>5,242,880</td></tr>
|
||||
* </table>
|
||||
* <p>Note that the terms and units used in the above examples are based on
|
||||
* <a href="https://en.wikipedia.org/wiki/Binary_prefix">binary prefixes</a>.
|
||||
* Consult the {@linkplain DataSize class-level Javadoc} for details.
|
||||
* @param text the text to parse
|
||||
* @param defaultUnit the default {@code DataUnit} to use
|
||||
* @return the parsed {@code DataSize}
|
||||
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link AnnotatedMethod}.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @since 6.2.11
|
||||
*/
|
||||
class AnnotatedMethodTests {
|
||||
|
||||
@Test
|
||||
void shouldFindAnnotationOnMethodInGenericAbstractSuperclass() {
|
||||
Method processTwo = getMethod("processTwo", String.class);
|
||||
|
||||
AnnotatedMethod annotatedMethod = new AnnotatedMethod(processTwo);
|
||||
|
||||
assertThat(annotatedMethod.hasMethodAnnotation(Handler.class)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindAnnotationOnMethodInGenericInterface() {
|
||||
Method processOneAndTwo = getMethod("processOneAndTwo", Long.class, Object.class);
|
||||
|
||||
AnnotatedMethod annotatedMethod = new AnnotatedMethod(processOneAndTwo);
|
||||
|
||||
assertThat(annotatedMethod.hasMethodAnnotation(Handler.class)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindAnnotationOnMethodParameterInGenericAbstractSuperclass() {
|
||||
// Prerequisites for gh-35349
|
||||
Method abstractMethod = ReflectionUtils.findMethod(GenericAbstractSuperclass.class, "processTwo", Object.class);
|
||||
assertThat(abstractMethod).isNotNull();
|
||||
assertThat(Modifier.isAbstract(abstractMethod.getModifiers())).as("abstract").isTrue();
|
||||
assertThat(Modifier.isPublic(abstractMethod.getModifiers())).as("public").isFalse();
|
||||
|
||||
Method processTwo = getMethod("processTwo", String.class);
|
||||
|
||||
AnnotatedMethod annotatedMethod = new AnnotatedMethod(processTwo);
|
||||
MethodParameter[] methodParameters = annotatedMethod.getMethodParameters();
|
||||
|
||||
assertThat(methodParameters).hasSize(1);
|
||||
assertThat(methodParameters[0].hasParameterAnnotation(Param.class)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindAnnotationOnMethodParameterInGenericInterface() {
|
||||
Method processOneAndTwo = getMethod("processOneAndTwo", Long.class, Object.class);
|
||||
|
||||
AnnotatedMethod annotatedMethod = new AnnotatedMethod(processOneAndTwo);
|
||||
MethodParameter[] methodParameters = annotatedMethod.getMethodParameters();
|
||||
|
||||
assertThat(methodParameters).hasSize(2);
|
||||
assertThat(methodParameters[0].hasParameterAnnotation(Param.class)).isFalse();
|
||||
assertThat(methodParameters[1].hasParameterAnnotation(Param.class)).isTrue();
|
||||
}
|
||||
|
||||
|
||||
private static Method getMethod(String name, Class<?>...parameterTypes) {
|
||||
Class<?> clazz = GenericInterfaceImpl.class;
|
||||
Method method = ReflectionUtils.findMethod(clazz, name, parameterTypes);
|
||||
if (method == null) {
|
||||
String parameterNames = stream(parameterTypes).map(Class::getName).collect(joining(", "));
|
||||
throw new IllegalStateException("Expected method not found: %s#%s(%s)"
|
||||
.formatted(clazz.getSimpleName(), name, parameterNames));
|
||||
}
|
||||
return method;
|
||||
}
|
||||
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface Handler {
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface Param {
|
||||
}
|
||||
|
||||
interface GenericInterface<A, B> {
|
||||
|
||||
@Handler
|
||||
void processOneAndTwo(A value1, @Param B value2);
|
||||
}
|
||||
|
||||
abstract static class GenericAbstractSuperclass<C> implements GenericInterface<Long, C> {
|
||||
|
||||
@Override
|
||||
public void processOneAndTwo(Long value1, C value2) {
|
||||
}
|
||||
|
||||
@Handler
|
||||
// Intentionally NOT public
|
||||
abstract void processTwo(@Param C value);
|
||||
}
|
||||
|
||||
static class GenericInterfaceImpl extends GenericAbstractSuperclass<String> {
|
||||
|
||||
@Override
|
||||
void processTwo(String value) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+29
@@ -945,6 +945,15 @@ class MergedAnnotationsTests {
|
||||
Order.class).getDistance()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFromMethodWithUnresolvedGenericsInGenericTypeHierarchy() {
|
||||
// The following method is GenericAbstractSuperclass.processOneAndTwo(java.lang.Long, C),
|
||||
// where 'C' is an unresolved generic, for which ResolvableType.resolve() returns null.
|
||||
Method method = ClassUtils.getMethod(GenericInterfaceImpl.class, "processOneAndTwo", Long.class, Object.class);
|
||||
assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY)
|
||||
.get(Transactional.class).isDirectlyPresent()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFromMethodWithInterfaceOnSuper() throws Exception {
|
||||
Method method = SubOfImplementsInterfaceWithAnnotatedMethod.class.getMethod("foo");
|
||||
@@ -3032,6 +3041,26 @@ class MergedAnnotationsTests {
|
||||
}
|
||||
}
|
||||
|
||||
interface GenericInterface<A, B> {
|
||||
|
||||
@Transactional
|
||||
void processOneAndTwo(A value1, B value2);
|
||||
}
|
||||
|
||||
abstract static class GenericAbstractSuperclass<C> implements GenericInterface<Long, C> {
|
||||
|
||||
@Override
|
||||
public void processOneAndTwo(Long value1, C value2) {
|
||||
}
|
||||
}
|
||||
|
||||
static class GenericInterfaceImpl extends GenericAbstractSuperclass<String> {
|
||||
// The compiler does not require us to declare a concrete
|
||||
// processOneAndTwo(Long, String) method, and we intentionally
|
||||
// do not declare one here.
|
||||
}
|
||||
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
@interface MyRepeatableContainer {
|
||||
|
||||
@@ -21,7 +21,9 @@ import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.testfixture.io.buffer.AbstractDataBufferAllocatingTests;
|
||||
|
||||
@@ -1045,4 +1047,35 @@ class DataBufferTests extends AbstractDataBufferAllocatingTests {
|
||||
release(buffer);
|
||||
}
|
||||
|
||||
@ParameterizedDataBufferAllocatingTest
|
||||
void forEachByteProcessAll(DataBufferFactory bufferFactory) {
|
||||
super.bufferFactory = bufferFactory;
|
||||
|
||||
List<Byte> result = new ArrayList<>();
|
||||
DataBuffer buffer = byteBuffer(new byte[]{'a', 'b', 'c', 'd'});
|
||||
int index = buffer.forEachByte(0, 4, b -> {
|
||||
result.add(b);
|
||||
return true;
|
||||
});
|
||||
assertThat(index).isEqualTo(-1);
|
||||
assertThat(result).containsExactly((byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd');
|
||||
release(buffer);
|
||||
}
|
||||
|
||||
|
||||
@ParameterizedDataBufferAllocatingTest
|
||||
void forEachByteProcessSome(DataBufferFactory bufferFactory) {
|
||||
super.bufferFactory = bufferFactory;
|
||||
|
||||
List<Byte> result = new ArrayList<>();
|
||||
DataBuffer buffer = byteBuffer(new byte[]{'a', 'b', 'c', 'd'});
|
||||
int index = buffer.forEachByte(0, 4, b -> {
|
||||
result.add(b);
|
||||
return (b != 'c');
|
||||
});
|
||||
assertThat(index).isEqualTo(2);
|
||||
assertThat(result).containsExactly((byte) 'a', (byte) 'b', (byte) 'c');
|
||||
release(buffer);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+18
-13
@@ -18,33 +18,34 @@ package org.springframework.core.io.buffer;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.eclipse.jetty.io.ArrayByteBufferPool;
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.io.RetainableByteBuffer;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
|
||||
/**
|
||||
* Tests for {@link JettyDataBuffer}
|
||||
* @author Arjen Poutsma
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class JettyDataBufferTests {
|
||||
|
||||
private final JettyDataBufferFactory dataBufferFactory = new JettyDataBufferFactory();
|
||||
|
||||
private ArrayByteBufferPool.Tracking byteBufferPool = new ArrayByteBufferPool.Tracking();
|
||||
|
||||
@Test
|
||||
void releaseRetainChunk() {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(3);
|
||||
Content.Chunk mockChunk = mock();
|
||||
given(mockChunk.getByteBuffer()).willReturn(buffer);
|
||||
given(mockChunk.release()).willReturn(false, false, true);
|
||||
RetainableByteBuffer retainableBuffer = byteBufferPool.acquire(3, false);
|
||||
ByteBuffer buffer = retainableBuffer.getByteBuffer();
|
||||
buffer.position(0).limit(1);
|
||||
Content.Chunk chunk = Content.Chunk.asChunk(buffer, false, retainableBuffer);
|
||||
|
||||
|
||||
|
||||
JettyDataBuffer dataBuffer = this.dataBufferFactory.wrap(mockChunk);
|
||||
JettyDataBuffer dataBuffer = this.dataBufferFactory.wrap(chunk);
|
||||
dataBuffer.retain();
|
||||
dataBuffer.retain();
|
||||
assertThat(dataBuffer.release()).isFalse();
|
||||
@@ -52,8 +53,12 @@ public class JettyDataBufferTests {
|
||||
assertThat(dataBuffer.release()).isTrue();
|
||||
|
||||
assertThatIllegalStateException().isThrownBy(dataBuffer::release);
|
||||
assertThat(retainableBuffer.isRetained()).isFalse();
|
||||
assertThat(byteBufferPool.getLeaks()).isEmpty();
|
||||
}
|
||||
|
||||
then(mockChunk).should(times(3)).retain();
|
||||
then(mockChunk).should(times(3)).release();
|
||||
@AfterEach
|
||||
public void tearDown() throws Exception {
|
||||
this.byteBufferPool.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import java.lang.reflect.Member;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.net.URLConnection;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -687,13 +688,13 @@ class ClassUtilsTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void publicMethodInObjectClass() throws Exception {
|
||||
void publicMethodInPublicClass() throws Exception {
|
||||
Class<?> originalType = String.class;
|
||||
Method originalMethod = originalType.getDeclaredMethod("hashCode");
|
||||
Method originalMethod = originalType.getDeclaredMethod("toString");
|
||||
|
||||
Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, null);
|
||||
assertThat(publiclyAccessibleMethod.getDeclaringClass()).isEqualTo(Object.class);
|
||||
assertThat(publiclyAccessibleMethod.getName()).isEqualTo("hashCode");
|
||||
assertThat(publiclyAccessibleMethod.getDeclaringClass()).isEqualTo(originalType);
|
||||
assertThat(publiclyAccessibleMethod).isSameAs(originalMethod);
|
||||
assertPubliclyAccessible(publiclyAccessibleMethod);
|
||||
}
|
||||
|
||||
@@ -703,9 +704,20 @@ class ClassUtilsTests {
|
||||
Method originalMethod = originalType.getDeclaredMethod("size");
|
||||
|
||||
Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, null);
|
||||
// Should find the interface method in List.
|
||||
assertThat(publiclyAccessibleMethod.getDeclaringClass()).isEqualTo(List.class);
|
||||
assertThat(publiclyAccessibleMethod.getName()).isEqualTo("size");
|
||||
// Should not find the interface method in List.
|
||||
assertThat(publiclyAccessibleMethod.getDeclaringClass()).isEqualTo(originalType);
|
||||
assertThat(publiclyAccessibleMethod).isSameAs(originalMethod);
|
||||
assertPubliclyAccessible(publiclyAccessibleMethod);
|
||||
}
|
||||
|
||||
@Test
|
||||
void publicMethodInNonExportedClass() throws Exception {
|
||||
Class<?> originalType = getClass().getClassLoader().loadClass("sun.net.www.protocol.http.HttpURLConnection");
|
||||
Method originalMethod = originalType.getDeclaredMethod("getOutputStream");
|
||||
|
||||
Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, null);
|
||||
assertThat(publiclyAccessibleMethod.getDeclaringClass()).isEqualTo(URLConnection.class);
|
||||
assertThat(publiclyAccessibleMethod.getName()).isSameAs(originalMethod.getName());
|
||||
assertPubliclyAccessible(publiclyAccessibleMethod);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,20 +17,29 @@
|
||||
package org.springframework.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URI;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link FileSystemUtils}.
|
||||
*
|
||||
* @author Rob Harrop
|
||||
* @author Sam Brannen
|
||||
* @author Juergen Hoeller
|
||||
*/
|
||||
class FileSystemUtilsTests {
|
||||
|
||||
@Test
|
||||
void deleteRecursively() throws Exception {
|
||||
File root = new File("./tmp/root");
|
||||
void deleteRecursively(@TempDir File tempDir) throws Exception {
|
||||
File root = new File(tempDir, "root");
|
||||
File child = new File(root, "child");
|
||||
File grandchild = new File(child, "grandchild");
|
||||
|
||||
@@ -53,8 +62,8 @@ class FileSystemUtilsTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void copyRecursively() throws Exception {
|
||||
File src = new File("./tmp/src");
|
||||
void copyRecursively(@TempDir File tempDir) throws Exception {
|
||||
File src = new File(tempDir, "src");
|
||||
File child = new File(src, "child");
|
||||
File grandchild = new File(child, "grandchild");
|
||||
|
||||
@@ -68,27 +77,29 @@ class FileSystemUtilsTests {
|
||||
assertThat(grandchild).exists();
|
||||
assertThat(bar).exists();
|
||||
|
||||
File dest = new File("./dest");
|
||||
File dest = new File(tempDir, "/dest");
|
||||
FileSystemUtils.copyRecursively(src, dest);
|
||||
|
||||
assertThat(dest).exists();
|
||||
assertThat(new File(dest, child.getName())).exists();
|
||||
assertThat(new File(dest, "child")).exists();
|
||||
assertThat(new File(dest, "child/bar.txt")).exists();
|
||||
|
||||
String destPath = dest.toString().replace('\\', '/');
|
||||
if (!destPath.startsWith("/")) {
|
||||
destPath = "/" + destPath;
|
||||
}
|
||||
URI uri = URI.create("jar:file:" + destPath + "/archive.zip");
|
||||
Map<String, String> env = Map.of("create", "true");
|
||||
FileSystem zipfs = FileSystems.newFileSystem(uri, env);
|
||||
Path ziproot = zipfs.getPath("/");
|
||||
FileSystemUtils.copyRecursively(src.toPath(), ziproot);
|
||||
|
||||
assertThat(zipfs.getPath("/child")).exists();
|
||||
assertThat(zipfs.getPath("/child/bar.txt")).exists();
|
||||
|
||||
zipfs.close();
|
||||
FileSystemUtils.deleteRecursively(src);
|
||||
assertThat(src).doesNotExist();
|
||||
}
|
||||
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
File tmp = new File("./tmp");
|
||||
if (tmp.exists()) {
|
||||
FileSystemUtils.deleteRecursively(tmp);
|
||||
}
|
||||
File dest = new File("./dest");
|
||||
if (dest.exists()) {
|
||||
FileSystemUtils.deleteRecursively(dest);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,8 +26,6 @@ import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import org.springframework.util.PlaceholderParser.ParsedValue;
|
||||
import org.springframework.util.PlaceholderParser.TextPart;
|
||||
import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -43,10 +41,11 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Sam Brannen
|
||||
* @author Juergen Hoeller
|
||||
*/
|
||||
class PlaceholderParserTests {
|
||||
|
||||
@Nested // Tests with only the basic placeholder feature enabled
|
||||
@Nested // Tests with only the basic placeholder feature enabled
|
||||
class OnlyPlaceholderTests {
|
||||
|
||||
private final PlaceholderParser parser = new PlaceholderParser("${", "}", null, null, true);
|
||||
@@ -82,7 +81,7 @@ class PlaceholderParserTests {
|
||||
Map<String, String> properties = Map.of(
|
||||
"p1", "v1",
|
||||
"p2", "v2",
|
||||
"p3", "${p1}:${p2}", // nested placeholders
|
||||
"p3", "${p1}:${p2}", // nested placeholders
|
||||
"p4", "${p3}", // deeply nested placeholders
|
||||
"p5", "${p1}:${p2}:${bogus}"); // unresolvable placeholder
|
||||
assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected);
|
||||
@@ -154,14 +153,13 @@ class PlaceholderParserTests {
|
||||
@Test
|
||||
void textWithInvalidPlaceholderSyntaxIsMerged() {
|
||||
String text = "test${of${with${and${";
|
||||
ParsedValue parsedValue = this.parser.parse(text);
|
||||
assertThat(parsedValue.parts()).singleElement().isInstanceOfSatisfying(TextPart.class,
|
||||
textPart -> assertThat(textPart.text()).isEqualTo(text));
|
||||
assertThat(this.parser.replacePlaceholders(text,
|
||||
placeholder -> {throw new UnsupportedOperationException();})).isEqualTo(text);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested // Tests with the use of a separator
|
||||
|
||||
@Nested // Tests with the use of a separator
|
||||
class DefaultValueTests {
|
||||
|
||||
private final PlaceholderParser parser = new PlaceholderParser("${", "}", ":", null, true);
|
||||
@@ -195,7 +193,7 @@ class PlaceholderParserTests {
|
||||
Map<String, String> properties = Map.of(
|
||||
"p1", "v1",
|
||||
"p2", "v2",
|
||||
"p3", "${p1}:${p2}", // nested placeholders
|
||||
"p3", "${p1}:${p2}", // nested placeholders
|
||||
"p4", "${p3}", // deeply nested placeholders
|
||||
"p5", "${p1}:${p2}:${bogus}", // unresolvable placeholder
|
||||
"p6", "${p1}:${p2}:${bogus:def}"); // unresolvable w/ default
|
||||
@@ -259,9 +257,9 @@ class PlaceholderParserTests {
|
||||
assertThat(this.parser.replacePlaceholders("${invalid:${firstName}}", resolver)).isEqualTo("John");
|
||||
verifyPlaceholderResolutions(resolver, "invalid", "firstName");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests that use the escape character.
|
||||
*/
|
||||
@@ -341,9 +339,9 @@ class PlaceholderParserTests {
|
||||
Arguments.of("${service/host/${app.environment}/name:\\value}", "https://example.com/qa/name"),
|
||||
Arguments.of("${service/host/${name\\:value}/}", "${service/host/${name:value}/}"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
class ExceptionTests {
|
||||
|
||||
@@ -378,7 +376,6 @@ class PlaceholderParserTests {
|
||||
.withMessage("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\" <-- \"${p3}\"")
|
||||
.withNoCause();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
+121
-13
@@ -129,34 +129,72 @@ public class StandardEvaluationContext implements EvaluationContext {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Specify the default root context object (including a type descriptor)
|
||||
* against which unqualified properties, methods, etc. should be resolved.
|
||||
* @param rootObject the root object to use
|
||||
* @param typeDescriptor a corresponding type descriptor
|
||||
*/
|
||||
public void setRootObject(@Nullable Object rootObject, TypeDescriptor typeDescriptor) {
|
||||
this.rootObject = new TypedValue(rootObject, typeDescriptor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the default root context object against which unqualified
|
||||
* properties, methods, etc. should be resolved.
|
||||
* @param rootObject the root object to use
|
||||
*/
|
||||
public void setRootObject(@Nullable Object rootObject) {
|
||||
this.rootObject = (rootObject != null ? new TypedValue(rootObject) : TypedValue.NULL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured default root context object against which unqualified
|
||||
* properties, methods, etc. should be resolved (can be {@link TypedValue#NULL}).
|
||||
*/
|
||||
@Override
|
||||
public TypedValue getRootObject() {
|
||||
return this.rootObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the list of property accessors to use in this evaluation context.
|
||||
* <p>Replaces any previously configured property accessors.
|
||||
*/
|
||||
public void setPropertyAccessors(List<PropertyAccessor> propertyAccessors) {
|
||||
this.propertyAccessors = propertyAccessors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of property accessors configured in this evaluation context.
|
||||
*/
|
||||
@Override
|
||||
public List<PropertyAccessor> getPropertyAccessors() {
|
||||
return initPropertyAccessors();
|
||||
}
|
||||
|
||||
public void addPropertyAccessor(PropertyAccessor accessor) {
|
||||
addBeforeDefault(initPropertyAccessors(), accessor);
|
||||
/**
|
||||
* Add the supplied property accessor to this evaluation context.
|
||||
* @param propertyAccessor the property accessor to add
|
||||
* @see #getPropertyAccessors()
|
||||
* @see #setPropertyAccessors(List)
|
||||
* @see #removePropertyAccessor(PropertyAccessor)
|
||||
*/
|
||||
public void addPropertyAccessor(PropertyAccessor propertyAccessor) {
|
||||
addBeforeDefault(initPropertyAccessors(), propertyAccessor);
|
||||
}
|
||||
|
||||
public boolean removePropertyAccessor(PropertyAccessor accessor) {
|
||||
return initPropertyAccessors().remove(accessor);
|
||||
/**
|
||||
* Remove the supplied property accessor from this evaluation context.
|
||||
* @param propertyAccessor the property accessor to remove
|
||||
* @return {@code true} if the property accessor was removed, {@code false}
|
||||
* if the property accessor was not configured in this evaluation context
|
||||
* @see #getPropertyAccessors()
|
||||
* @see #setPropertyAccessors(List)
|
||||
* @see #addPropertyAccessor(PropertyAccessor)
|
||||
*/
|
||||
public boolean removePropertyAccessor(PropertyAccessor propertyAccessor) {
|
||||
return initPropertyAccessors().remove(propertyAccessor);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,8 +236,8 @@ public class StandardEvaluationContext implements EvaluationContext {
|
||||
/**
|
||||
* Remove the supplied index accessor from this evaluation context.
|
||||
* @param indexAccessor the index accessor to remove
|
||||
* @return {@code true} if the index accessor was removed, {@code false} if
|
||||
* the index accessor was not configured in this evaluation context
|
||||
* @return {@code true} if the index accessor was removed, {@code false}
|
||||
* if the index accessor was not configured in this evaluation context
|
||||
* @since 6.2
|
||||
* @see #getIndexAccessors()
|
||||
* @see #setIndexAccessors(List)
|
||||
@@ -209,44 +247,96 @@ public class StandardEvaluationContext implements EvaluationContext {
|
||||
return initIndexAccessors().remove(indexAccessor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the list of constructor resolvers to use in this evaluation context.
|
||||
* <p>Replaces any previously configured constructor resolvers.
|
||||
*/
|
||||
public void setConstructorResolvers(List<ConstructorResolver> constructorResolvers) {
|
||||
this.constructorResolvers = constructorResolvers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of constructor resolvers to use in this evaluation context.
|
||||
*/
|
||||
@Override
|
||||
public List<ConstructorResolver> getConstructorResolvers() {
|
||||
return initConstructorResolvers();
|
||||
}
|
||||
|
||||
public void addConstructorResolver(ConstructorResolver resolver) {
|
||||
addBeforeDefault(initConstructorResolvers(), resolver);
|
||||
/**
|
||||
* Add the supplied constructor resolver to this evaluation context.
|
||||
* @param constructorResolver the constructor resolver to add
|
||||
* @see #getConstructorResolvers()
|
||||
* @see #setConstructorResolvers(List)
|
||||
* @see #removeConstructorResolver(ConstructorResolver)
|
||||
*/
|
||||
public void addConstructorResolver(ConstructorResolver constructorResolver) {
|
||||
addBeforeDefault(initConstructorResolvers(), constructorResolver);
|
||||
}
|
||||
|
||||
public boolean removeConstructorResolver(ConstructorResolver resolver) {
|
||||
return initConstructorResolvers().remove(resolver);
|
||||
/**
|
||||
* Remove the supplied constructor resolver from this evaluation context.
|
||||
* @param constructorResolver the constructor resolver to remove
|
||||
* @return {@code true} if the constructor resolver was removed, {@code false}
|
||||
* if the constructor resolver was not configured in this evaluation context
|
||||
* @see #getConstructorResolvers()
|
||||
* @see #setConstructorResolvers(List)
|
||||
* @see #addConstructorResolver(ConstructorResolver)
|
||||
*/
|
||||
public boolean removeConstructorResolver(ConstructorResolver constructorResolver) {
|
||||
return initConstructorResolvers().remove(constructorResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the list of method resolvers to use in this evaluation context.
|
||||
* <p>Replaces any previously configured method resolvers.
|
||||
*/
|
||||
public void setMethodResolvers(List<MethodResolver> methodResolvers) {
|
||||
this.methodResolvers = methodResolvers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of method resolvers to use in this evaluation context.
|
||||
*/
|
||||
@Override
|
||||
public List<MethodResolver> getMethodResolvers() {
|
||||
return initMethodResolvers();
|
||||
}
|
||||
|
||||
public void addMethodResolver(MethodResolver resolver) {
|
||||
addBeforeDefault(initMethodResolvers(), resolver);
|
||||
/**
|
||||
* Add the supplied method resolver to this evaluation context.
|
||||
* @param methodResolver the method resolver to add
|
||||
* @see #getMethodResolvers()
|
||||
* @see #setMethodResolvers(List)
|
||||
* @see #removeMethodResolver(MethodResolver)
|
||||
*/
|
||||
public void addMethodResolver(MethodResolver methodResolver) {
|
||||
addBeforeDefault(initMethodResolvers(), methodResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the supplied method resolver from this evaluation context.
|
||||
* @param methodResolver the method resolver to remove
|
||||
* @return {@code true} if the method resolver was removed, {@code false}
|
||||
* if the method resolver was not configured in this evaluation context
|
||||
* @see #getMethodResolvers()
|
||||
* @see #setMethodResolvers(List)
|
||||
* @see #addMethodResolver(MethodResolver)
|
||||
*/
|
||||
public boolean removeMethodResolver(MethodResolver methodResolver) {
|
||||
return initMethodResolvers().remove(methodResolver);
|
||||
}
|
||||
|
||||
public void setBeanResolver(BeanResolver beanResolver) {
|
||||
/**
|
||||
* Set the {@link BeanResolver} to use for looking up beans, if any.
|
||||
*/
|
||||
public void setBeanResolver(@Nullable BeanResolver beanResolver) {
|
||||
this.beanResolver = beanResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configured {@link BeanResolver} for looking up beans, if any.
|
||||
*/
|
||||
@Override
|
||||
@Nullable
|
||||
public BeanResolver getBeanResolver() {
|
||||
@@ -284,11 +374,17 @@ public class StandardEvaluationContext implements EvaluationContext {
|
||||
return this.typeLocator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link TypeConverter} for value conversion.
|
||||
*/
|
||||
public void setTypeConverter(TypeConverter typeConverter) {
|
||||
Assert.notNull(typeConverter, "TypeConverter must not be null");
|
||||
this.typeConverter = typeConverter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configured {@link TypeConverter} for value conversion.
|
||||
*/
|
||||
@Override
|
||||
public TypeConverter getTypeConverter() {
|
||||
if (this.typeConverter == null) {
|
||||
@@ -297,21 +393,33 @@ public class StandardEvaluationContext implements EvaluationContext {
|
||||
return this.typeConverter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link TypeComparator} for comparing pairs of objects.
|
||||
*/
|
||||
public void setTypeComparator(TypeComparator typeComparator) {
|
||||
Assert.notNull(typeComparator, "TypeComparator must not be null");
|
||||
this.typeComparator = typeComparator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configured {@link TypeComparator} for comparing pairs of objects.
|
||||
*/
|
||||
@Override
|
||||
public TypeComparator getTypeComparator() {
|
||||
return this.typeComparator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link OperatorOverloader} for mathematical operations.
|
||||
*/
|
||||
public void setOperatorOverloader(OperatorOverloader operatorOverloader) {
|
||||
Assert.notNull(operatorOverloader, "OperatorOverloader must not be null");
|
||||
this.operatorOverloader = operatorOverloader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configured {@link OperatorOverloader} for mathematical operations.
|
||||
*/
|
||||
@Override
|
||||
public OperatorOverloader getOperatorOverloader() {
|
||||
return this.operatorOverloader;
|
||||
|
||||
+11
-44
@@ -21,7 +21,6 @@ import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Types;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -48,11 +47,6 @@ public class GenericTableMetaDataProvider implements TableMetaDataProvider {
|
||||
/** Logger available to subclasses. */
|
||||
protected static final Log logger = LogFactory.getLog(TableMetaDataProvider.class);
|
||||
|
||||
/** Database products we know not supporting the use of a String[] for generated keys. */
|
||||
private static final List<String> productsNotSupportingGeneratedKeysColumnNameArray =
|
||||
Arrays.asList("Apache Derby", "HSQL Database Engine");
|
||||
|
||||
|
||||
/** The name of the user currently connected. */
|
||||
@Nullable
|
||||
private final String userName;
|
||||
@@ -95,45 +89,14 @@ public class GenericTableMetaDataProvider implements TableMetaDataProvider {
|
||||
@Override
|
||||
public void initializeWithMetaData(DatabaseMetaData databaseMetaData) throws SQLException {
|
||||
try {
|
||||
if (databaseMetaData.supportsGetGeneratedKeys()) {
|
||||
logger.debug("GetGeneratedKeys is supported");
|
||||
setGetGeneratedKeysSupported(true);
|
||||
}
|
||||
else {
|
||||
logger.debug("GetGeneratedKeys is not supported");
|
||||
setGetGeneratedKeysSupported(false);
|
||||
}
|
||||
setGetGeneratedKeysSupported(databaseMetaData.supportsGetGeneratedKeys());
|
||||
setGeneratedKeysColumnNameArraySupported(isGetGeneratedKeysSupported());
|
||||
}
|
||||
catch (SQLException ex) {
|
||||
if (logger.isWarnEnabled()) {
|
||||
logger.warn("Error retrieving 'DatabaseMetaData.supportsGetGeneratedKeys': " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
try {
|
||||
String databaseProductName = databaseMetaData.getDatabaseProductName();
|
||||
if (productsNotSupportingGeneratedKeysColumnNameArray.contains(databaseProductName)) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("GeneratedKeysColumnNameArray is not supported for " + databaseProductName);
|
||||
}
|
||||
setGeneratedKeysColumnNameArraySupported(false);
|
||||
}
|
||||
else {
|
||||
if (isGetGeneratedKeysSupported()) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("GeneratedKeysColumnNameArray is supported for " + databaseProductName);
|
||||
}
|
||||
setGeneratedKeysColumnNameArraySupported(true);
|
||||
}
|
||||
else {
|
||||
setGeneratedKeysColumnNameArraySupported(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (SQLException ex) {
|
||||
if (logger.isWarnEnabled()) {
|
||||
logger.warn("Error retrieving 'DatabaseMetaData.getDatabaseProductName': " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
this.databaseVersion = databaseMetaData.getDatabaseProductVersion();
|
||||
@@ -225,19 +188,23 @@ public class GenericTableMetaDataProvider implements TableMetaDataProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation delegates to {@link #catalogNameToUse}.
|
||||
*/
|
||||
@Override
|
||||
@Nullable
|
||||
public String metaDataCatalogNameToUse(@Nullable String catalogName) {
|
||||
return catalogNameToUse(catalogName);
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation delegates to {@link #schemaNameToUse}.
|
||||
* @see #getDefaultSchema()
|
||||
*/
|
||||
@Override
|
||||
@Nullable
|
||||
public String metaDataSchemaNameToUse(@Nullable String schemaName) {
|
||||
if (schemaName == null) {
|
||||
return schemaNameToUse(getDefaultSchema());
|
||||
}
|
||||
return schemaNameToUse(schemaName);
|
||||
return schemaNameToUse(schemaName != null ? schemaName : getDefaultSchema());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -401,7 +368,7 @@ public class GenericTableMetaDataProvider implements TableMetaDataProvider {
|
||||
try {
|
||||
tableColumns = databaseMetaData.getColumns(
|
||||
metaDataCatalogName, metaDataSchemaName, metaDataTableName, null);
|
||||
while (tableColumns.next()) {
|
||||
while (tableColumns != null && tableColumns.next()) {
|
||||
String columnName = tableColumns.getString("COLUMN_NAME");
|
||||
int dataType = tableColumns.getInt("DATA_TYPE");
|
||||
if (dataType == Types.DECIMAL) {
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.jdbc.core.metadata;
|
||||
|
||||
import java.sql.DatabaseMetaData;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* The MySQL/MariaDB specific implementation of {@link TableMetaDataProvider}.
|
||||
* Sets {@link #setGeneratedKeysColumnNameArraySupported} to {@code false}.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @since 6.2.12
|
||||
*/
|
||||
public class MySQLTableMetaDataProvider extends GenericTableMetaDataProvider {
|
||||
|
||||
public MySQLTableMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLException {
|
||||
super(databaseMetaData);
|
||||
setGeneratedKeysColumnNameArraySupported(false);
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user