mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 34764252dc |
@@ -50,7 +50,7 @@ public class CheckstyleConventions {
|
||||
project.getPlugins().apply(CheckstylePlugin.class);
|
||||
project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g"));
|
||||
CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class);
|
||||
checkstyle.setToolVersion("10.18.1");
|
||||
checkstyle.setToolVersion("10.17.0");
|
||||
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
|
||||
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
|
||||
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
|
||||
|
||||
@@ -13,7 +13,7 @@ content:
|
||||
- url: https://github.com/spring-projects/spring-framework
|
||||
# Refname matching:
|
||||
# https://docs.antora.org/antora/latest/playbook/content-refname-matching/
|
||||
branches: ['main', '{6..9}.+({1..9}).x']
|
||||
branches: ['main', '{6..9}.+({0..9}).x']
|
||||
tags: ['v{6..9}.+({0..9}).+({0..9})?(-{RC,M}*)', '!(v6.0.{0..8})', '!(v6.0.0-{RC,M}{0..9})']
|
||||
start_path: framework-docs
|
||||
asciidoc:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[[context-introduction]]
|
||||
= Additional Capabilities of the `ApplicationContext`
|
||||
|
||||
As discussed in the xref:core/beans/introduction.adoc[chapter introduction], the `org.springframework.beans.factory`
|
||||
As discussed in the xref:web/webmvc-view/mvc-xslt.adoc#mvc-view-xslt-beandefs[chapter introduction], the `org.springframework.beans.factory`
|
||||
package provides basic functionality for managing and manipulating beans, including in a
|
||||
programmatic way. The `org.springframework.context` package adds the
|
||||
{spring-framework-api}/context/ApplicationContext.html[`ApplicationContext`]
|
||||
@@ -644,7 +644,7 @@ Each `SpEL` expression evaluates against a dedicated context. The following tabl
|
||||
items made available to the context so that you can use them for conditional event processing:
|
||||
|
||||
[[context-functionality-events-annotation-tbl]]
|
||||
.Event metadata available in SpEL expressions
|
||||
.Event SpEL available metadata
|
||||
|===
|
||||
| Name| Location| Description| Example
|
||||
|
||||
@@ -660,8 +660,8 @@ items made available to the context so that you can use them for conditional eve
|
||||
|
||||
| __Argument name__
|
||||
| evaluation context
|
||||
| The name of a particular method argument. If the names are not available
|
||||
(for example, because the code was compiled without the `-parameters` flag), individual
|
||||
| The name of any of the method arguments. If, for some reason, the names are not available
|
||||
(for example, because there is no debug information in the compiled byte code), individual
|
||||
arguments are also available using the `#a<#arg>` syntax where `<#arg>` stands for the
|
||||
argument index (starting from 0).
|
||||
| `#blEvent` or `#a0` (you can also use `#p0` or `#p<#arg>` parameter notation as an alias)
|
||||
|
||||
+12
-15
@@ -159,12 +159,10 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
[discrete]
|
||||
[[beans-factory-ctor-arguments-type]]
|
||||
==== Constructor argument type matching
|
||||
|
||||
.[[beans-factory-ctor-arguments-type]]Constructor argument type matching
|
||||
--
|
||||
In the preceding scenario, the container can use type matching with simple types if
|
||||
you explicitly specify the type of the constructor argument via the `type` attribute,
|
||||
you explicitly specify the type of the constructor argument by using the `type` attribute,
|
||||
as the following example shows:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
@@ -174,11 +172,10 @@ as the following example shows:
|
||||
<constructor-arg type="java.lang.String" value="42"/>
|
||||
</bean>
|
||||
----
|
||||
--
|
||||
|
||||
[discrete]
|
||||
[[beans-factory-ctor-arguments-index]]
|
||||
==== Constructor argument index
|
||||
|
||||
.[[beans-factory-ctor-arguments-index]]Constructor argument index
|
||||
--
|
||||
You can use the `index` attribute to specify explicitly the index of constructor arguments,
|
||||
as the following example shows:
|
||||
|
||||
@@ -194,11 +191,10 @@ In addition to resolving the ambiguity of multiple simple values, specifying an
|
||||
resolves ambiguity where a constructor has two arguments of the same type.
|
||||
|
||||
NOTE: The index is 0-based.
|
||||
--
|
||||
|
||||
[discrete]
|
||||
[[beans-factory-ctor-arguments-name]]
|
||||
==== Constructor argument name
|
||||
|
||||
.[[beans-factory-ctor-arguments-name]]Constructor argument name
|
||||
--
|
||||
You can also use the constructor parameter name for value disambiguation, as the following
|
||||
example shows:
|
||||
|
||||
@@ -211,8 +207,8 @@ example shows:
|
||||
----
|
||||
|
||||
Keep in mind that, to make this work out of the box, your code must be compiled with the
|
||||
`-parameters` flag enabled so that Spring can look up the parameter name from the constructor.
|
||||
If you cannot or do not want to compile your code with the `-parameters` flag, you can use the
|
||||
debug flag enabled so that Spring can look up the parameter name from the constructor.
|
||||
If you cannot or do not want to compile your code with the debug flag, you can use the
|
||||
https://download.oracle.com/javase/8/docs/api/java/beans/ConstructorProperties.html[@ConstructorProperties]
|
||||
JDK annotation to explicitly name your constructor arguments. The sample class would
|
||||
then have to look as follows:
|
||||
@@ -248,6 +244,7 @@ Kotlin::
|
||||
constructor(val years: Int, val ultimateAnswer: String)
|
||||
----
|
||||
======
|
||||
--
|
||||
|
||||
|
||||
[[beans-setter-injection]]
|
||||
|
||||
+1
-1
@@ -582,7 +582,7 @@ it needs to be declared in the XML file even though it is not defined in an XSD
|
||||
(it exists inside the Spring core).
|
||||
|
||||
For the rare cases where the constructor argument names are not available (usually if
|
||||
the bytecode was compiled without the `-parameters` flag), you can fall back to the
|
||||
the bytecode was compiled without debugging information), you can use fallback to the
|
||||
argument indexes, as follows:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
|
||||
+32
-55
@@ -116,7 +116,7 @@ the configuration model, in that references to other beans must be valid Java sy
|
||||
Fortunately, solving this problem is simple. As
|
||||
xref:core/beans/java/bean-annotation.adoc#beans-java-dependencies[we already discussed],
|
||||
a `@Bean` method can have an arbitrary number of parameters that describe the bean
|
||||
dependencies. Consider the following more realistic scenario with several `@Configuration`
|
||||
dependencies. Consider the following more real-world scenario with several `@Configuration`
|
||||
classes, each depending on beans declared in the others:
|
||||
|
||||
[tabs]
|
||||
@@ -331,10 +331,8 @@ TIP: Constructor injection in `@Configuration` classes is only supported as of S
|
||||
Framework 4.3. Note also that there is no need to specify `@Autowired` if the target
|
||||
bean defines only one constructor.
|
||||
|
||||
[discrete]
|
||||
[[beans-java-injecting-imported-beans-fq]]
|
||||
==== Fully-qualifying imported beans for ease of navigation
|
||||
|
||||
.[[beans-java-injecting-imported-beans-fq]]Fully-qualifying imported beans for ease of navigation
|
||||
--
|
||||
In the preceding scenario, using `@Autowired` works well and provides the desired
|
||||
modularity, but determining exactly where the autowired bean definitions are declared is
|
||||
still somewhat ambiguous. For example, as a developer looking at `ServiceConfig`, how do
|
||||
@@ -503,6 +501,7 @@ Now `ServiceConfig` is loosely coupled with respect to the concrete
|
||||
get a type hierarchy of `RepositoryConfig` implementations. In this
|
||||
way, navigating `@Configuration` classes and their dependencies becomes no different
|
||||
than the usual process of navigating interface-based code.
|
||||
--
|
||||
|
||||
TIP: If you want to influence the startup creation order of certain beans, consider
|
||||
declaring some of them as `@Lazy` (for creation on first access instead of on startup)
|
||||
@@ -541,7 +540,7 @@ Java::
|
||||
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
|
||||
if (attrs != null) {
|
||||
for (Object value : attrs.get("value")) {
|
||||
if (context.getEnvironment().matchesProfiles((String[]) value)) {
|
||||
if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -560,7 +559,7 @@ Kotlin::
|
||||
val attrs = metadata.getAllAnnotationAttributes(Profile::class.java.name)
|
||||
if (attrs != null) {
|
||||
for (value in attrs["value"]!!) {
|
||||
if (context.environment.matchesProfiles(*value as Array<String>)) {
|
||||
if (context.environment.acceptsProfiles(Profiles.of(*value as Array<String>))) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -595,18 +594,16 @@ that uses Spring XML, it is easier to create `@Configuration` classes on an
|
||||
as-needed basis and include them from the existing XML files. Later in this section, we cover the
|
||||
options for using `@Configuration` classes in this kind of "`XML-centric`" situation.
|
||||
|
||||
[discrete]
|
||||
[[beans-java-combining-xml-centric-declare-as-bean]]
|
||||
==== Declaring `@Configuration` classes as plain Spring `<bean/>` elements
|
||||
|
||||
Remember that `@Configuration` classes are ultimately bean definitions in the container.
|
||||
In this series of examples, we create a `@Configuration` class named `AppConfig` and
|
||||
.[[beans-java-combining-xml-centric-declare-as-bean]]Declaring `@Configuration` classes as plain Spring `<bean/>` elements
|
||||
--
|
||||
Remember that `@Configuration` classes are ultimately bean definitions in the
|
||||
container. In this series examples, we create a `@Configuration` class named `AppConfig` and
|
||||
include it within `system-test-config.xml` as a `<bean/>` definition. Because
|
||||
`<context:annotation-config/>` is switched on, the container recognizes the
|
||||
`@Configuration` annotation and processes the `@Bean` methods declared in `AppConfig`
|
||||
properly.
|
||||
|
||||
The following example shows the `AppConfig` configuration class in Java and Kotlin:
|
||||
The following example shows an ordinary configuration class in Java:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -627,7 +624,7 @@ Java::
|
||||
|
||||
@Bean
|
||||
public TransferService transferService() {
|
||||
return new TransferServiceImpl(accountRepository());
|
||||
return new TransferService(accountRepository());
|
||||
}
|
||||
}
|
||||
----
|
||||
@@ -660,7 +657,6 @@ The following example shows part of a sample `system-test-config.xml` file:
|
||||
<beans>
|
||||
<!-- enable processing of annotations such as @Autowired and @Configuration -->
|
||||
<context:annotation-config/>
|
||||
|
||||
<context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
|
||||
|
||||
<bean class="com.acme.AppConfig"/>
|
||||
@@ -707,20 +703,20 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
NOTE: In the `system-test-config.xml` file, the `AppConfig` `<bean/>` does not declare an `id`
|
||||
attribute. While it would be acceptable to do so, it is unnecessary, given that no other bean
|
||||
|
||||
NOTE: In `system-test-config.xml` file, the `AppConfig` `<bean/>` does not declare an `id`
|
||||
element. While it would be acceptable to do so, it is unnecessary, given that no other bean
|
||||
ever refers to it, and it is unlikely to be explicitly fetched from the container by name.
|
||||
Similarly, the `DataSource` bean is only ever autowired by type, so an explicit bean `id`
|
||||
is not strictly required.
|
||||
--
|
||||
|
||||
[discrete]
|
||||
[[beans-java-combining-xml-centric-component-scan]]
|
||||
==== Using <context:component-scan/> to pick up `@Configuration` classes
|
||||
|
||||
.[[beans-java-combining-xml-centric-component-scan]] Using <context:component-scan/> to pick up `@Configuration` classes
|
||||
--
|
||||
Because `@Configuration` is meta-annotated with `@Component`, `@Configuration`-annotated
|
||||
classes are automatically candidates for component scanning. Using the same scenario as
|
||||
described in the previous example, we can redefine `system-test-config.xml` to take
|
||||
advantage of component-scanning. Note that, in this case, we need not explicitly declare
|
||||
described in the previous example, we can redefine `system-test-config.xml` to take advantage of component-scanning.
|
||||
Note that, in this case, we need not explicitly declare
|
||||
`<context:annotation-config/>`, because `<context:component-scan/>` enables the same
|
||||
functionality.
|
||||
|
||||
@@ -731,7 +727,6 @@ The following example shows the modified `system-test-config.xml` file:
|
||||
<beans>
|
||||
<!-- picks up and registers AppConfig as a bean definition -->
|
||||
<context:component-scan base-package="com.acme"/>
|
||||
|
||||
<context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
|
||||
|
||||
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
|
||||
@@ -741,17 +736,19 @@ The following example shows the modified `system-test-config.xml` file:
|
||||
</bean>
|
||||
</beans>
|
||||
----
|
||||
--
|
||||
|
||||
[[beans-java-combining-java-centric]]
|
||||
=== `@Configuration` Class-centric Use of XML with `@ImportResource`
|
||||
|
||||
In applications where `@Configuration` classes are the primary mechanism for configuring
|
||||
the container, it may still be necessary to use at least some XML. In such scenarios, you
|
||||
can use `@ImportResource` and define only as much XML as you need. Doing so achieves a
|
||||
"`Java-centric`" approach to configuring the container and keeps XML to a bare minimum.
|
||||
The following example (which includes a configuration class, an XML file that defines a
|
||||
bean, a properties file, and the `main()` method) shows how to use the `@ImportResource`
|
||||
annotation to achieve "`Java-centric`" configuration that uses XML as needed:
|
||||
the container, it is still likely necessary to use at least some XML. In these
|
||||
scenarios, you can use `@ImportResource` and define only as much XML as you need. Doing
|
||||
so achieves a "`Java-centric`" approach to configuring the container and keeps XML to a
|
||||
bare minimum. The following example (which includes a configuration class, an XML file
|
||||
that defines a bean, a properties file, and the `main` class) shows how to use
|
||||
the `@ImportResource` annotation to achieve "`Java-centric`" configuration that uses XML
|
||||
as needed:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -776,17 +773,6 @@ Java::
|
||||
public DataSource dataSource() {
|
||||
return new DriverManagerDataSource(url, username, password);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AccountRepository accountRepository(DataSource dataSource) {
|
||||
return new JdbcAccountRepository(dataSource);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TransferService transferService(AccountRepository accountRepository) {
|
||||
return new TransferServiceImpl(accountRepository);
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
@@ -811,32 +797,21 @@ Kotlin::
|
||||
fun dataSource(): DataSource {
|
||||
return DriverManagerDataSource(url, username, password)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun accountRepository(dataSource: DataSource): AccountRepository {
|
||||
return JdbcAccountRepository(dataSource)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun transferService(accountRepository: AccountRepository): TransferService {
|
||||
return TransferServiceImpl(accountRepository)
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
.properties-config.xml
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
properties-config.xml
|
||||
<beans>
|
||||
<context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
|
||||
</beans>
|
||||
----
|
||||
|
||||
.jdbc.properties
|
||||
[literal,subs="verbatim,quotes"]
|
||||
----
|
||||
jdbc.properties
|
||||
jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
|
||||
jdbc.username=sa
|
||||
jdbc.password=
|
||||
@@ -869,3 +844,5 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -222,38 +222,29 @@ Kotlin::
|
||||
[[expressions-evaluation-context]]
|
||||
== Understanding `EvaluationContext`
|
||||
|
||||
The `EvaluationContext` API is used when evaluating an expression to resolve properties,
|
||||
methods, or fields and to help perform type conversion. Spring provides two
|
||||
The `EvaluationContext` interface is used when evaluating an expression to resolve
|
||||
properties, methods, or fields and to help perform type conversion. Spring provides two
|
||||
implementations.
|
||||
|
||||
`SimpleEvaluationContext`::
|
||||
Exposes a subset of essential SpEL language features and configuration options, for
|
||||
categories of expressions that do not require the full extent of the SpEL language
|
||||
syntax and should be meaningfully restricted. Examples include but are not limited to
|
||||
data binding expressions and property-based filters.
|
||||
* `SimpleEvaluationContext`: Exposes a subset of essential SpEL language features and
|
||||
configuration options, for categories of expressions that do not require the full extent
|
||||
of the SpEL language syntax and should be meaningfully restricted. Examples include but
|
||||
are not limited to data binding expressions and property-based filters.
|
||||
|
||||
`StandardEvaluationContext`::
|
||||
Exposes the full set of SpEL language features and configuration options. You can use
|
||||
it to specify a default root object and to configure every available evaluation-related
|
||||
strategy.
|
||||
* `StandardEvaluationContext`: Exposes the full set of SpEL language features and
|
||||
configuration options. You can use it to specify a default root object and to configure
|
||||
every available evaluation-related strategy.
|
||||
|
||||
`SimpleEvaluationContext` is designed to support only a subset of the SpEL language
|
||||
syntax. For example, it excludes Java type references, constructors, and bean references.
|
||||
It also requires you to explicitly choose the level of support for properties and methods
|
||||
in expressions. When creating a `SimpleEvaluationContext` you need to choose the level of
|
||||
support that you need for data binding in SpEL expressions:
|
||||
`SimpleEvaluationContext` is designed to support only a subset of the SpEL language syntax.
|
||||
It excludes Java type references, constructors, and bean references. It also requires
|
||||
you to explicitly choose the level of support for properties and methods in expressions.
|
||||
By default, the `create()` static factory method enables only read access to properties.
|
||||
You can also obtain a builder to configure the exact level of support needed, targeting
|
||||
one or some combination of the following.
|
||||
|
||||
* Data binding for read-only access
|
||||
* Data binding for read and write access
|
||||
* A custom `PropertyAccessor` (typically not reflection-based), potentially combined with
|
||||
a `DataBindingPropertyAccessor`
|
||||
|
||||
Conveniently, `SimpleEvaluationContext.forReadOnlyDataBinding()` enables read-only access
|
||||
to properties via `DataBindingPropertyAccessor`. Similarly,
|
||||
`SimpleEvaluationContext.forReadWriteDataBinding()` enables read and write access to
|
||||
properties. Alternatively, configure custom accessors via
|
||||
`SimpleEvaluationContext.forPropertyAccessors(...)`, potentially disable assignment, and
|
||||
optionally activate method resolution and/or a type converter through the builder.
|
||||
* Custom `PropertyAccessor` only (no reflection)
|
||||
* Data binding properties for read-only access
|
||||
* Data binding properties for read and write
|
||||
|
||||
|
||||
[[expressions-type-conversion]]
|
||||
@@ -323,17 +314,17 @@ Kotlin::
|
||||
It is possible to configure the SpEL expression parser by using a parser configuration
|
||||
object (`org.springframework.expression.spel.SpelParserConfiguration`). The configuration
|
||||
object controls the behavior of some of the expression components. For example, if you
|
||||
index into a collection and the element at the specified index is `null`, SpEL can
|
||||
automatically create the element. This is useful when using expressions made up of a
|
||||
chain of property references. Similarly, if you index into a collection and specify an
|
||||
index that is greater than the current size of the collection, SpEL can automatically
|
||||
grow the collection to accommodate that index. In order to add an element at the
|
||||
index into an array or collection and the element at the specified index is `null`, SpEL
|
||||
can automatically create the element. This is useful when using expressions made up of a
|
||||
chain of property references. If you index into an array or list and specify an index
|
||||
that is beyond the end of the current size of the array or list, SpEL can automatically
|
||||
grow the array or list to accommodate that index. In order to add an element at the
|
||||
specified index, SpEL will try to create the element using the element type's default
|
||||
constructor before setting the specified value. If the element type does not have a
|
||||
default constructor, `null` will be added to the collection. If there is no built-in
|
||||
converter or custom converter that knows how to set the value, `null` will remain in the
|
||||
collection at the specified index. The following example demonstrates how to
|
||||
automatically grow a `List`.
|
||||
default constructor, `null` will be added to the array or list. If there is no built-in
|
||||
or custom converter that knows how to set the value, `null` will remain in the array or
|
||||
list at the specified index. The following example demonstrates how to automatically grow
|
||||
the list.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
|
||||
@@ -332,7 +332,7 @@ metadata, such as the argument names. The following table describes the items ma
|
||||
available to the context so that you can use them for key and conditional computations:
|
||||
|
||||
[[cache-spel-context-tbl]]
|
||||
.Cache metadata available in SpEL expressions
|
||||
.Cache SpEL available metadata
|
||||
|===
|
||||
| Name| Location| Description| Example
|
||||
|
||||
@@ -358,7 +358,7 @@ available to the context so that you can use them for key and conditional comput
|
||||
|
||||
| `args`
|
||||
| Root object
|
||||
| The arguments (as an object array) used for invoking the target
|
||||
| The arguments (as array) used for invoking the target
|
||||
| `#root.args[0]`
|
||||
|
||||
| `caches`
|
||||
@@ -368,10 +368,9 @@ available to the context so that you can use them for key and conditional comput
|
||||
|
||||
| Argument name
|
||||
| Evaluation context
|
||||
| The name of a particular method argument. If the names are not available
|
||||
(for example, because the code was compiled without the `-parameters` flag), individual
|
||||
arguments are also available using the `#a<#arg>` syntax where `<#arg>` stands for the
|
||||
argument index (starting from 0).
|
||||
| Name of any of the method arguments. If the names are not available
|
||||
(perhaps due to having no debug information), the argument names are also available under the `#a<#arg>`
|
||||
where `#arg` stands for the argument index (starting from `0`).
|
||||
| `#iban` or `#a0` (you can also use `#p0` or `#p<#arg>` notation as an alias).
|
||||
|
||||
| `result`
|
||||
|
||||
@@ -65,7 +65,7 @@ a "shared objects file" source, as shown in the following example:
|
||||
If CDS can't be enabled or if you have a large number of classes that are not loaded from the cache, make sure that
|
||||
the following conditions are fulfilled when creating and using the archive:
|
||||
|
||||
- The very same JVM must be used.
|
||||
- The very same JVM must used.
|
||||
- The classpath must be specified as a list of JARs, and avoid the usage of directories and `*` wildcard characters.
|
||||
- The timestamps of the JARs must be preserved.
|
||||
- When using the archive, the classpath must be the same than the one used to create the archive, in the same order.
|
||||
|
||||
@@ -19,8 +19,6 @@ A checkpoint can be created on demand, for example using a command like `jcmd ap
|
||||
|
||||
WARNING: Leveraging checkpoint/restore of a running application typically requires additional lifecycle management to gracefully stop and start using resources like files or sockets and stop active threads.
|
||||
|
||||
WARNING: Be aware that when defining scheduling tasks at a fixed rate, for example with an annotation like `@Scheduled(fixedRate = 5000)`, all missed executions between checkpoint and restore will be performed when the JVM is restored with on-demand checkpoint/restore. If this is not the behavior you want, it is recommended to schedule tasks at a fixed delay (for example with `@Scheduled(fixedDelay = 5000)`) or with a cron expression as those are calculated after every task execution.
|
||||
|
||||
NOTE: If the checkpoint is created on a warmed-up JVM, the restored JVM will be equally warmed-up, allowing potentially peak performance immediately. This method typically requires access to remote services, and thus requires some level of platform integration.
|
||||
|
||||
== Automatic checkpoint/restore at startup
|
||||
|
||||
-4
@@ -34,10 +34,6 @@ Kotlin::
|
||||
You can use the xref:web/webmvc/mvc-config/message-converters.adoc[Message Converters] option of the xref:web/webmvc/mvc-config.adoc[MVC Config] to
|
||||
configure or customize message conversion.
|
||||
|
||||
NOTE: Form data should be read using xref:web/webmvc/mvc-controller/ann-methods/requestparam.adoc[`@RequestParam`],
|
||||
not with `@RequestBody` which can't always be used reliably since in the Servlet API, request parameter
|
||||
access causes the request body to be parsed, and it can't be read again.
|
||||
|
||||
You can use `@RequestBody` in combination with `jakarta.validation.Valid` or Spring's
|
||||
`@Validated` annotation, both of which cause Standard Bean Validation to be applied.
|
||||
By default, validation errors cause a `MethodArgumentNotValidException`, which is turned
|
||||
|
||||
+1
-43
@@ -61,7 +61,7 @@ Kotlin::
|
||||
|
||||
By default, method parameters that use this annotation are required, but you can specify that
|
||||
a method parameter is optional by setting the `@RequestParam` annotation's `required` flag to
|
||||
`false` or by declaring the argument with a `java.util.Optional` wrapper.
|
||||
`false` or by declaring the argument with an `java.util.Optional` wrapper.
|
||||
|
||||
Type conversion is automatically applied if the target method parameter type is not
|
||||
`String`. See xref:web/webmvc/mvc-controller/ann-methods/typeconversion.adoc[Type Conversion].
|
||||
@@ -72,48 +72,6 @@ values for the same parameter name.
|
||||
When an `@RequestParam` annotation is declared as a `Map<String, String>` or
|
||||
`MultiValueMap<String, String>`, without a parameter name specified in the annotation,
|
||||
then the map is populated with the request parameter values for each given parameter name.
|
||||
The following example shows how to do so with form data processing:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@Controller
|
||||
@RequestMapping("/pets")
|
||||
class EditPetForm {
|
||||
|
||||
// ...
|
||||
|
||||
@PostMapping(path = "/process", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
public String processForm(@RequestParam MultiValueMap<String, String> params) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
----
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@Controller
|
||||
@RequestMapping("/pets")
|
||||
class EditPetForm {
|
||||
|
||||
// ...
|
||||
|
||||
@PostMapping("/process", consumes = [MediaType.APPLICATION_FORM_URLENCODED_VALUE])
|
||||
fun processForm(@RequestParam params: MultiValueMap<String, String>): String {
|
||||
// ...
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
Note that use of `@RequestParam` is optional (for example, to set its attributes).
|
||||
By default, any argument that is a simple value type (as determined by
|
||||
|
||||
+2
-2
@@ -263,10 +263,10 @@ JavaPoet 1.13.0 is licensed under the Apache License, version 2.0, the text of
|
||||
which is included above.
|
||||
|
||||
|
||||
>>> Objenesis 3.4 (org.objenesis:objenesis:3.4):
|
||||
>>> Objenesis 3.2 (org.objenesis:objenesis:3.2):
|
||||
|
||||
Per the LICENSE file in the Objenesis ZIP distribution downloaded from
|
||||
http://objenesis.org/download.html, Objenesis 3.4 is licensed under the
|
||||
http://objenesis.org/download.html, Objenesis 3.2 is licensed under the
|
||||
Apache License, version 2.0, the text of which is included above.
|
||||
|
||||
Per the NOTICE file in the Objenesis ZIP distribution downloaded from
|
||||
|
||||
@@ -8,16 +8,16 @@ javaPlatform {
|
||||
|
||||
dependencies {
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.15.4"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.12.10"))
|
||||
api(platform("io.netty:netty-bom:4.1.113.Final"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.12.9"))
|
||||
api(platform("io.netty:netty-bom:4.1.112.Final"))
|
||||
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
|
||||
api(platform("io.projectreactor:reactor-bom:2023.0.10"))
|
||||
api(platform("io.projectreactor:reactor-bom:2023.0.9"))
|
||||
api(platform("io.rsocket:rsocket-bom:1.1.3"))
|
||||
api(platform("org.apache.groovy:groovy-bom:4.0.22"))
|
||||
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
|
||||
api(platform("org.assertj:assertj-bom:3.26.3"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.0.13"))
|
||||
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.13"))
|
||||
api(platform("org.assertj:assertj-bom:3.26.0"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.0.12"))
|
||||
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.12"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3"))
|
||||
api(platform("org.junit:junit-bom:5.10.3"))
|
||||
@@ -56,9 +56,9 @@ dependencies {
|
||||
api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE")
|
||||
api("io.reactivex.rxjava3:rxjava:3.1.8")
|
||||
api("io.smallrye.reactive:mutiny:1.10.0")
|
||||
api("io.undertow:undertow-core:2.3.17.Final")
|
||||
api("io.undertow:undertow-servlet:2.3.17.Final")
|
||||
api("io.undertow:undertow-websockets-jsr:2.3.17.Final")
|
||||
api("io.undertow:undertow-core:2.3.15.Final")
|
||||
api("io.undertow:undertow-servlet:2.3.15.Final")
|
||||
api("io.undertow:undertow-websockets-jsr:2.3.15.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")
|
||||
@@ -116,7 +116,7 @@ dependencies {
|
||||
api("org.codehaus.jettison:jettison:1.5.4")
|
||||
api("org.crac:crac:1.4.0")
|
||||
api("org.dom4j:dom4j:2.1.4")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.7")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.5")
|
||||
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")
|
||||
@@ -140,7 +140,7 @@ dependencies {
|
||||
api("org.seleniumhq.selenium:htmlunit-driver:2.70.0")
|
||||
api("org.seleniumhq.selenium:selenium-java:3.141.59")
|
||||
api("org.skyscreamer:jsonassert:1.5.3")
|
||||
api("org.slf4j:slf4j-api:2.0.16")
|
||||
api("org.slf4j:slf4j-api:2.0.13")
|
||||
api("org.testng:testng:7.9.0")
|
||||
api("org.webjars:underscorejs:1.8.3")
|
||||
api("org.webjars:webjars-locator-core:0.55")
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
version=6.1.13
|
||||
version=6.1.12
|
||||
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
org.gradle.parallel=true
|
||||
|
||||
kotlinVersion=1.9.25
|
||||
kotlinVersion=1.9.24
|
||||
|
||||
kotlin.jvm.target.validation.mode=ignore
|
||||
kotlin.stdlib.default.dependency=false
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -19,10 +19,10 @@ package org.springframework.beans.factory.aot;
|
||||
/**
|
||||
* Record class holding key information for beans registered in a bean factory.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 6.0.8
|
||||
* @param beanName the name of the registered bean
|
||||
* @param beanClass the type of the registered bean
|
||||
* @author Brian Clozel
|
||||
* @since 6.0.8
|
||||
*/
|
||||
record BeanRegistrationKey(String beanName, Class<?> beanClass) {
|
||||
}
|
||||
|
||||
@@ -83,7 +83,6 @@ public class CodeWarnings {
|
||||
* specified {@link ResolvableType}.
|
||||
* @param resolvableType a type signature
|
||||
* @return {@code this} instance
|
||||
* @since 6.1.8
|
||||
*/
|
||||
public CodeWarnings detectDeprecation(ResolvableType resolvableType) {
|
||||
if (ResolvableType.NONE.equals(resolvableType)) {
|
||||
|
||||
+2
-2
@@ -25,11 +25,11 @@ import org.springframework.util.ClassUtils;
|
||||
* reference to the method's {@linkplain #declaringClass declaring class},
|
||||
* {@linkplain #methodName name}, and {@linkplain #parameterTypes parameter types}.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @since 6.0.11
|
||||
* @param declaringClass the method's declaring class
|
||||
* @param methodName the name of the method
|
||||
* @param parameterTypes the types of parameters accepted by the method
|
||||
* @author Sam Brannen
|
||||
* @since 6.0.11
|
||||
*/
|
||||
record MethodDescriptor(Class<?> declaringClass, String methodName, Class<?>... parameterTypes) {
|
||||
|
||||
|
||||
+1
-1
@@ -267,10 +267,10 @@ public final class RegisteredBean {
|
||||
* is usually the declaring class of the {@code executable} (in case of a constructor
|
||||
* or a locally declared factory method), there are cases where retaining the actual
|
||||
* concrete class is necessary (e.g. for an inherited factory method).
|
||||
* @since 6.1.7
|
||||
* @param executable the {@link Executable} ({@link java.lang.reflect.Constructor}
|
||||
* or {@link java.lang.reflect.Method}) to invoke
|
||||
* @param targetClass the target {@link Class} of the executable
|
||||
* @since 6.1.7
|
||||
*/
|
||||
public record InstantiationDescriptor(Executable executable, Class<?> targetClass) {
|
||||
|
||||
|
||||
-3
@@ -371,9 +371,6 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
|
||||
if (logger.isWarnEnabled()) {
|
||||
logger.warn("Failed to stop bean '" + beanName + "'", ex);
|
||||
}
|
||||
if (bean instanceof SmartLifecycle) {
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
-21
@@ -126,27 +126,6 @@ public @interface Scheduled {
|
||||
*/
|
||||
String zone() default "";
|
||||
|
||||
/**
|
||||
* Execute the annotated method with a fixed period between invocations.
|
||||
* <p>The time unit is milliseconds by default but can be overridden via
|
||||
* {@link #timeUnit}.
|
||||
* @return the period
|
||||
*/
|
||||
long fixedRate() default -1;
|
||||
|
||||
/**
|
||||
* Execute the annotated method with a fixed period between invocations.
|
||||
* <p>The time unit is milliseconds by default but can be overridden via
|
||||
* {@link #timeUnit}.
|
||||
* <p>This attribute variant supports Spring-style "${...}" placeholders
|
||||
* as well as SpEL expressions.
|
||||
* @return the period as a String value — for example, a placeholder
|
||||
* or a {@link java.time.Duration#parse java.time.Duration} compliant value
|
||||
* @since 3.2.2
|
||||
* @see #fixedRate()
|
||||
*/
|
||||
String fixedRateString() default "";
|
||||
|
||||
/**
|
||||
* Execute the annotated method with a fixed period between the end of the
|
||||
* last invocation and the start of the next.
|
||||
@@ -176,6 +155,27 @@ public @interface Scheduled {
|
||||
*/
|
||||
String fixedDelayString() default "";
|
||||
|
||||
/**
|
||||
* Execute the annotated method with a fixed period between invocations.
|
||||
* <p>The time unit is milliseconds by default but can be overridden via
|
||||
* {@link #timeUnit}.
|
||||
* @return the period
|
||||
*/
|
||||
long fixedRate() default -1;
|
||||
|
||||
/**
|
||||
* Execute the annotated method with a fixed period between invocations.
|
||||
* <p>The time unit is milliseconds by default but can be overridden via
|
||||
* {@link #timeUnit}.
|
||||
* <p>This attribute variant supports Spring-style "${...}" placeholders
|
||||
* as well as SpEL expressions.
|
||||
* @return the period as a String value — for example, a placeholder
|
||||
* or a {@link java.time.Duration#parse java.time.Duration} compliant value
|
||||
* @since 3.2.2
|
||||
* @see #fixedRate()
|
||||
*/
|
||||
String fixedRateString() default "";
|
||||
|
||||
/**
|
||||
* Number of units of time to delay before the first execution of a
|
||||
* {@link #fixedRate} or {@link #fixedDelay} task.
|
||||
|
||||
+1
-1
@@ -609,7 +609,7 @@ abstract class AbstractProxyTargetClassConfig {
|
||||
@Aspect
|
||||
static class SupplierAdvice {
|
||||
|
||||
@Around("execution(* java.util.function.Supplier+.get())")
|
||||
@Around("execution(public * org.springframework.aop.aspectj.autoproxy..*.*(..))")
|
||||
Object aroundSupplier(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
return "advised: " + joinPoint.proceed();
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ multiRelease {
|
||||
}
|
||||
|
||||
def javapoetVersion = "1.13.0"
|
||||
def objenesisVersion = "3.4"
|
||||
def objenesisVersion = "3.3"
|
||||
|
||||
configurations {
|
||||
java21Api.extendsFrom(api)
|
||||
|
||||
+1
-2
@@ -107,8 +107,7 @@ public class BindingReflectionHintsRegistrar {
|
||||
registerPropertyHints(hints, seen, method, 0);
|
||||
}
|
||||
else if ((methodName.startsWith("get") && method.getParameterCount() == 0 && method.getReturnType() != void.class) ||
|
||||
(methodName.startsWith("is") && method.getParameterCount() == 0
|
||||
&& ClassUtils.resolvePrimitiveIfNecessary(method.getReturnType()) == Boolean.class)) {
|
||||
(methodName.startsWith("is") && method.getParameterCount() == 0 && method.getReturnType() == boolean.class)) {
|
||||
registerPropertyHints(hints, seen, method, -1);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -34,7 +34,6 @@ import java.net.URLClassLoader;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.file.FileSystemNotFoundException;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.FileVisitOption;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
@@ -872,7 +871,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
.formatted(rootPath.toAbsolutePath(), subPattern));
|
||||
}
|
||||
|
||||
try (Stream<Path> files = Files.walk(rootPath, FileVisitOption.FOLLOW_LINKS)) {
|
||||
try (Stream<Path> files = Files.walk(rootPath)) {
|
||||
files.filter(isMatchingFile).sorted().map(FileSystemResource::new).forEach(result::add);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Spring's repackaging of
|
||||
* <a href="http://objenesis.org">Objenesis 3.4</a>
|
||||
* <a href="http://objenesis.org">Objenesis 3.2</a>
|
||||
* (with SpringObjenesis entry point; for internal use only).
|
||||
*
|
||||
* <p>This repackaging technique avoids any potential conflicts with
|
||||
|
||||
+3
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -68,10 +68,8 @@ class BindingReflectionHintsRegistrarKotlinTests {
|
||||
assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleDataClass::class.java, "component1")).accepts(hints)
|
||||
assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleDataClass::class.java, "copy")).accepts(hints)
|
||||
assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleDataClass::class.java, "getName")).accepts(hints)
|
||||
assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleDataClass::class.java, "isNonNullable")).accepts(hints)
|
||||
assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleDataClass::class.java, "isNullable")).accepts(hints)
|
||||
val copyDefault: Method = SampleDataClass::class.java.getMethod("copy\$default", SampleDataClass::class.java,
|
||||
String::class.java, Boolean::class.javaPrimitiveType, Boolean::class.javaObjectType, Int::class.java, Object::class.java)
|
||||
String::class.java , Int::class.java, Object::class.java)
|
||||
assertThat(RuntimeHintsPredicates.reflection().onMethod(copyDefault)).accepts(hints)
|
||||
}
|
||||
|
||||
@@ -86,6 +84,6 @@ class BindingReflectionHintsRegistrarKotlinTests {
|
||||
@kotlinx.serialization.Serializable
|
||||
class SampleSerializableClass(val name: String)
|
||||
|
||||
data class SampleDataClass(val name: String, val isNonNullable: Boolean, val isNullable: Boolean?)
|
||||
data class SampleDataClass(val name: String)
|
||||
|
||||
class SampleClass(val name: String)
|
||||
|
||||
@@ -44,8 +44,6 @@ import java.util.TimeZone;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.servlet.AsyncContext;
|
||||
import jakarta.servlet.AsyncEvent;
|
||||
import jakarta.servlet.AsyncListener;
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.RequestDispatcher;
|
||||
import jakarta.servlet.ServletConnection;
|
||||
@@ -922,19 +920,7 @@ public class MockHttpServletRequest implements HttpServletRequest {
|
||||
public AsyncContext startAsync(ServletRequest request, @Nullable ServletResponse response) {
|
||||
Assert.state(this.asyncSupported, "Async not supported");
|
||||
this.asyncStarted = true;
|
||||
MockAsyncContext newAsyncContext = new MockAsyncContext(request, response);
|
||||
if (this.asyncContext != null) {
|
||||
try {
|
||||
AsyncEvent startEvent = new AsyncEvent(newAsyncContext);
|
||||
for (AsyncListener asyncListener : this.asyncContext.getListeners()) {
|
||||
asyncListener.onStartAsync(startEvent);
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
// ignore failures
|
||||
}
|
||||
}
|
||||
this.asyncContext = newAsyncContext;
|
||||
this.asyncContext = new MockAsyncContext(request, response);
|
||||
return this.asyncContext;
|
||||
}
|
||||
|
||||
|
||||
+2
-10
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -66,7 +66,6 @@ import org.springframework.web.util.UriBuilderFactory;
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Sam Brannen
|
||||
* @author Michał Rowicki
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.0
|
||||
*/
|
||||
class DefaultWebTestClient implements WebTestClient {
|
||||
@@ -491,14 +490,7 @@ class DefaultWebTestClient implements WebTestClient {
|
||||
|
||||
@Override
|
||||
public <T> FluxExchangeResult<T> returnResult(ParameterizedTypeReference<T> elementTypeRef) {
|
||||
Flux<T> body;
|
||||
if (elementTypeRef.getType().equals(Void.class)) {
|
||||
this.response.releaseBody().block();
|
||||
body = Flux.empty();
|
||||
}
|
||||
else {
|
||||
body = this.response.bodyToFlux(elementTypeRef);
|
||||
}
|
||||
Flux<T> body = this.response.bodyToFlux(elementTypeRef);
|
||||
return new FluxExchangeResult<>(this.exchangeResult, body);
|
||||
}
|
||||
|
||||
|
||||
+1
-11
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -176,16 +176,6 @@ public class ExchangeResult {
|
||||
return this.response.getStatusCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the HTTP status code as an integer.
|
||||
* @since 5.1.10
|
||||
* @deprecated in favor of {@link #getStatus()}, for removal in 7.0
|
||||
*/
|
||||
@Deprecated(since = "6.0", forRemoval = true)
|
||||
public int getRawStatusCode() {
|
||||
return getStatus().value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the response headers received from the server.
|
||||
*/
|
||||
|
||||
-68
@@ -30,9 +30,6 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.servlet.AsyncContext;
|
||||
import jakarta.servlet.AsyncEvent;
|
||||
import jakarta.servlet.AsyncListener;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -666,44 +663,6 @@ class MockHttpServletRequestTests {
|
||||
request.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectAsyncStartsIfUnsupported() {
|
||||
assertThat(request.isAsyncStarted()).isFalse();
|
||||
assertThatIllegalStateException().isThrownBy(request::startAsync);
|
||||
}
|
||||
|
||||
@Test
|
||||
void startAsyncShouldUpdateRequestState() {
|
||||
assertThat(request.isAsyncStarted()).isFalse();
|
||||
request.setAsyncSupported(true);
|
||||
AsyncContext asyncContext = request.startAsync();
|
||||
assertThat(request.isAsyncStarted()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotifyAsyncListeners() {
|
||||
request.setAsyncSupported(true);
|
||||
AsyncContext asyncContext = request.startAsync();
|
||||
TestAsyncListener testAsyncListener = new TestAsyncListener();
|
||||
asyncContext.addListener(testAsyncListener);
|
||||
asyncContext.complete();
|
||||
assertThat(testAsyncListener.events).hasSize(1);
|
||||
assertThat(testAsyncListener.events.get(0)).extracting("name").isEqualTo("onComplete");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotifyAsyncListenersWhenNewAsyncStarted() {
|
||||
request.setAsyncSupported(true);
|
||||
AsyncContext asyncContext = request.startAsync();
|
||||
TestAsyncListener testAsyncListener = new TestAsyncListener();
|
||||
asyncContext.addListener(testAsyncListener);
|
||||
AsyncContext newAsyncContext = request.startAsync();
|
||||
assertThat(testAsyncListener.events).hasSize(1);
|
||||
ListenerEvent listenerEvent = testAsyncListener.events.get(0);
|
||||
assertThat(listenerEvent).extracting("name").isEqualTo("onStartAsync");
|
||||
assertThat(listenerEvent.event.getAsyncContext()).isEqualTo(newAsyncContext);
|
||||
}
|
||||
|
||||
private void assertEqualEnumerations(Enumeration<?> enum1, Enumeration<?> enum2) {
|
||||
int count = 0;
|
||||
while (enum1.hasMoreElements()) {
|
||||
@@ -713,31 +672,4 @@ class MockHttpServletRequestTests {
|
||||
}
|
||||
}
|
||||
|
||||
static class TestAsyncListener implements AsyncListener {
|
||||
|
||||
List<ListenerEvent> events = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void onComplete(AsyncEvent asyncEvent) throws IOException {
|
||||
this.events.add(new ListenerEvent("onComplete", asyncEvent));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimeout(AsyncEvent asyncEvent) throws IOException {
|
||||
this.events.add(new ListenerEvent("onTimeout", asyncEvent));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(AsyncEvent asyncEvent) throws IOException {
|
||||
this.events.add(new ListenerEvent("onError", asyncEvent));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartAsync(AsyncEvent asyncEvent) throws IOException {
|
||||
this.events.add(new ListenerEvent("onStartAsync", asyncEvent));
|
||||
}
|
||||
}
|
||||
|
||||
record ListenerEvent(String name, AsyncEvent event) {}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,10 +27,10 @@ import org.springframework.util.StringUtils;
|
||||
/**
|
||||
* Represents an ETag for HTTP conditional requests.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.3.38
|
||||
* @param tag the unquoted tag value
|
||||
* @param weak whether the entity tag is for weak or strong validation
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.3.38
|
||||
* @see <a href="https://datatracker.ietf.org/doc/html/rfc7232">RFC 7232</a>
|
||||
*/
|
||||
public record ETag(String tag, boolean weak) {
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -25,7 +25,7 @@ import org.springframework.http.HttpStatusCode;
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 3.1.1
|
||||
* @deprecated with no direct replacement; for removal in 6.2
|
||||
* @deprecated as of 6.0, with no direct replacement; scheduled for removal in 6.2
|
||||
*/
|
||||
@Deprecated(since = "6.0", forRemoval = true)
|
||||
public abstract class AbstractClientHttpResponse implements ClientHttpResponse {
|
||||
|
||||
@@ -48,7 +48,8 @@ public interface ClientHttpResponse extends HttpInputMessage, Closeable {
|
||||
* @throws IOException in case of I/O errors
|
||||
* @since 3.1.1
|
||||
* @see #getStatusCode()
|
||||
* @deprecated in favor of {@link #getStatusCode()}, for removal in 7.0
|
||||
* @deprecated as of 6.0, in favor of {@link #getStatusCode()}; scheduled for
|
||||
* removal in 6.2
|
||||
*/
|
||||
@Deprecated(since = "6.0", forRemoval = true)
|
||||
default int getRawStatusCode() throws IOException {
|
||||
|
||||
+1
-13
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -46,18 +46,6 @@ public interface ClientHttpResponse extends ReactiveHttpInputMessage {
|
||||
*/
|
||||
HttpStatusCode getStatusCode();
|
||||
|
||||
/**
|
||||
* Return the HTTP status code as an integer.
|
||||
* @return the HTTP status as an integer value
|
||||
* @since 5.0.6
|
||||
* @see #getStatusCode()
|
||||
* @deprecated in favor of {@link #getStatusCode()}, for removal in 7.0
|
||||
*/
|
||||
@Deprecated(since = "6.0", forRemoval = true)
|
||||
default int getRawStatusCode() {
|
||||
return getStatusCode().value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a read-only map of response cookies received from the server.
|
||||
*/
|
||||
|
||||
+8
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -115,6 +115,13 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse {
|
||||
return setStatusCode(statusCode != null ? HttpStatusCode.valueOf(statusCode) : null);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
@Nullable
|
||||
public Integer getRawStatusCode() {
|
||||
return (this.statusCode != null ? this.statusCode.value() : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders getHeaders() {
|
||||
if (this.readOnlyHeaders != null) {
|
||||
|
||||
-1
@@ -78,7 +78,6 @@ class ReactorNetty2ServerHttpResponse extends AbstractServerHttpResponse impleme
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@SuppressWarnings("removal")
|
||||
public Integer getRawStatusCode() {
|
||||
Integer status = super.getRawStatusCode();
|
||||
return (status != null ? status : this.response.status().code());
|
||||
|
||||
-1
@@ -77,7 +77,6 @@ class ReactorServerHttpResponse extends AbstractServerHttpResponse implements Ze
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@SuppressWarnings("removal")
|
||||
public Integer getRawStatusCode() {
|
||||
Integer status = super.getRawStatusCode();
|
||||
return (status != null ? status : this.response.status().code());
|
||||
|
||||
+2
-2
@@ -65,9 +65,9 @@ public interface ServerHttpResponse extends ReactiveHttpOutputMessage {
|
||||
* status of the response from the underlying server. The return value may
|
||||
* be {@code null} if there is no default value from the underlying server.
|
||||
* @since 5.2.4
|
||||
* @deprecated in favor of {@link #getStatusCode()}, for removal in 7.0
|
||||
* @deprecated as of 6.0, in favor of {@link #getStatusCode()}
|
||||
*/
|
||||
@Deprecated(since = "6.0", forRemoval = true)
|
||||
@Deprecated(since = "6.0")
|
||||
@Nullable
|
||||
default Integer getRawStatusCode() {
|
||||
HttpStatusCode httpStatus = getStatusCode();
|
||||
|
||||
+1
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -74,7 +74,6 @@ public class ServerHttpResponseDecorator implements ServerHttpResponse {
|
||||
@Override
|
||||
@Nullable
|
||||
@Deprecated
|
||||
@SuppressWarnings("removal")
|
||||
public Integer getRawStatusCode() {
|
||||
return getDelegate().getRawStatusCode();
|
||||
}
|
||||
|
||||
-1
@@ -109,7 +109,6 @@ class ServletServerHttpResponse extends AbstractListenerServerHttpResponse {
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@SuppressWarnings("removal")
|
||||
public Integer getRawStatusCode() {
|
||||
Integer status = super.getRawStatusCode();
|
||||
return (status != null ? status : this.response.getStatus());
|
||||
|
||||
-1
@@ -89,7 +89,6 @@ class UndertowServerHttpResponse extends AbstractListenerServerHttpResponse impl
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@SuppressWarnings("removal")
|
||||
public Integer getRawStatusCode() {
|
||||
Integer status = super.getRawStatusCode();
|
||||
return (status != null ? status : this.exchange.getStatusCode());
|
||||
|
||||
-7
@@ -25,13 +25,11 @@ import org.springframework.aot.hint.BindingReflectionHintsRegistrar;
|
||||
import org.springframework.aot.hint.ExecutableMode;
|
||||
import org.springframework.aot.hint.ReflectionHints;
|
||||
import org.springframework.aot.hint.annotation.ReflectiveProcessor;
|
||||
import org.springframework.core.KotlinDetector;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* {@link ReflectiveProcessor} implementation for {@link Controller} and
|
||||
@@ -73,11 +71,6 @@ class ControllerMappingReflectiveProcessor implements ReflectiveProcessor {
|
||||
|
||||
protected void registerMethodHints(ReflectionHints hints, Method method) {
|
||||
hints.registerMethod(method, ExecutableMode.INVOKE);
|
||||
Class<?> declaringClass = method.getDeclaringClass();
|
||||
if (KotlinDetector.isKotlinType(declaringClass)) {
|
||||
ReflectionUtils.doWithMethods(declaringClass, m -> hints.registerMethod(m, ExecutableMode.INVOKE),
|
||||
m -> m.getName().equals(method.getName() + "$default"));
|
||||
}
|
||||
for (Parameter parameter : method.getParameters()) {
|
||||
registerParameterTypeHints(hints, MethodParameter.forParameter(parameter));
|
||||
}
|
||||
|
||||
@@ -468,7 +468,6 @@ final class DefaultRestClient implements RestClient {
|
||||
|
||||
ClientHttpResponse clientResponse = null;
|
||||
Observation observation = null;
|
||||
Observation.Scope observationScope = null;
|
||||
URI uri = null;
|
||||
try {
|
||||
if (DefaultRestClient.this.defaultRequest != null) {
|
||||
@@ -482,7 +481,6 @@ final class DefaultRestClient implements RestClient {
|
||||
observationContext.setUriTemplate(this.uriTemplate);
|
||||
observation = ClientHttpObservationDocumentation.HTTP_CLIENT_EXCHANGES.observation(observationConvention,
|
||||
DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, observationRegistry).start();
|
||||
observationScope = observation.openScope();
|
||||
if (this.body != null) {
|
||||
this.body.writeTo(clientRequest);
|
||||
}
|
||||
@@ -491,14 +489,11 @@ final class DefaultRestClient implements RestClient {
|
||||
}
|
||||
clientResponse = clientRequest.execute();
|
||||
observationContext.setResponse(clientResponse);
|
||||
ConvertibleClientHttpResponse convertibleWrapper = new DefaultConvertibleClientHttpResponse(clientResponse, observation, observationScope);
|
||||
ConvertibleClientHttpResponse convertibleWrapper = new DefaultConvertibleClientHttpResponse(clientResponse, observation);
|
||||
return exchangeFunction.exchange(clientRequest, convertibleWrapper);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
ResourceAccessException resourceAccessException = createResourceAccessException(uri, this.httpMethod, ex);
|
||||
if (observationScope != null) {
|
||||
observationScope.close();
|
||||
}
|
||||
if (observation != null) {
|
||||
observation.error(resourceAccessException);
|
||||
observation.stop();
|
||||
@@ -506,9 +501,6 @@ final class DefaultRestClient implements RestClient {
|
||||
throw resourceAccessException;
|
||||
}
|
||||
catch (Throwable error) {
|
||||
if (observationScope != null) {
|
||||
observationScope.close();
|
||||
}
|
||||
if (observation != null) {
|
||||
observation.error(error);
|
||||
observation.stop();
|
||||
@@ -518,9 +510,6 @@ final class DefaultRestClient implements RestClient {
|
||||
finally {
|
||||
if (close && clientResponse != null) {
|
||||
clientResponse.close();
|
||||
if (observationScope != null) {
|
||||
observationScope.close();
|
||||
}
|
||||
if (observation != null) {
|
||||
observation.stop();
|
||||
}
|
||||
@@ -731,12 +720,10 @@ final class DefaultRestClient implements RestClient {
|
||||
|
||||
private final Observation observation;
|
||||
|
||||
private final Observation.Scope observationScope;
|
||||
|
||||
public DefaultConvertibleClientHttpResponse(ClientHttpResponse delegate, Observation observation, Observation.Scope observationScope) {
|
||||
public DefaultConvertibleClientHttpResponse(ClientHttpResponse delegate, Observation observation) {
|
||||
this.delegate = delegate;
|
||||
this.observation = observation;
|
||||
this.observationScope = observationScope;
|
||||
}
|
||||
|
||||
|
||||
@@ -777,7 +764,6 @@ final class DefaultRestClient implements RestClient {
|
||||
@Override
|
||||
public void close() {
|
||||
this.delegate.close();
|
||||
this.observationScope.close();
|
||||
this.observation.stop();
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -125,7 +125,7 @@ public class RestClientResponseException extends RestClientException {
|
||||
|
||||
/**
|
||||
* Return the raw HTTP status code value.
|
||||
* @deprecated in favor of {@link #getStatusCode()}, for removal in 7.0
|
||||
* @deprecated as of 6.0, in favor of {@link #getStatusCode()}
|
||||
*/
|
||||
@Deprecated(since = "6.0")
|
||||
public int getRawStatusCode() {
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -113,7 +113,7 @@ public class UnknownContentTypeException extends RestClientException {
|
||||
|
||||
/**
|
||||
* Return the raw HTTP status code value.
|
||||
* @deprecated in favor of {@link #getStatusCode()}, for removal in 7.0
|
||||
* @deprecated as of 6.0, in favor of {@link #getStatusCode()}
|
||||
*/
|
||||
@Deprecated(since = "6.0")
|
||||
public int getRawStatusCode() {
|
||||
|
||||
+45
-52
@@ -21,7 +21,6 @@ import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -212,38 +211,6 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return 0 when there is no need to obtain a lock (no async handling in
|
||||
* progress), 1 if lock was acquired, and -1 if lock is not acquired because
|
||||
* request is no longer usable.
|
||||
*/
|
||||
private int tryObtainLock() {
|
||||
|
||||
if (this.state == State.NEW) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Do not wait indefinitely, stop if we moved on from ASYNC state (e.g. to ERROR),
|
||||
// helps to avoid ABBA deadlock with onError callback
|
||||
|
||||
while (this.state == State.ASYNC) {
|
||||
try {
|
||||
if (this.stateLock.tryLock(500, TimeUnit.MILLISECONDS)) {
|
||||
if (this.state == State.ASYNC) {
|
||||
return 1;
|
||||
}
|
||||
this.stateLock.unlock();
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Package private access for testing only.
|
||||
*/
|
||||
@@ -277,7 +244,7 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
|
||||
@Override
|
||||
public ServletOutputStream getOutputStream() throws IOException {
|
||||
int level = obtainLockOrRaiseException();
|
||||
int level = obtainLockAndCheckState();
|
||||
try {
|
||||
if (this.outputStream == null) {
|
||||
Assert.notNull(this.asyncWebRequest, "Not initialized");
|
||||
@@ -296,7 +263,7 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
|
||||
@Override
|
||||
public PrintWriter getWriter() throws IOException {
|
||||
int level = obtainLockOrRaiseException();
|
||||
int level = obtainLockAndCheckState();
|
||||
try {
|
||||
if (this.writer == null) {
|
||||
Assert.notNull(this.asyncWebRequest, "Not initialized");
|
||||
@@ -314,7 +281,7 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
|
||||
@Override
|
||||
public void flushBuffer() throws IOException {
|
||||
int level = obtainLockOrRaiseException();
|
||||
int level = obtainLockAndCheckState();
|
||||
try {
|
||||
getResponse().flushBuffer();
|
||||
}
|
||||
@@ -326,15 +293,25 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
}
|
||||
}
|
||||
|
||||
private int obtainLockOrRaiseException() throws AsyncRequestNotUsableException {
|
||||
/**
|
||||
* Return 0 if checks passed and lock is not needed, 1 if checks passed
|
||||
* and lock is held, or raise AsyncRequestNotUsableException.
|
||||
*/
|
||||
private int obtainLockAndCheckState() throws AsyncRequestNotUsableException {
|
||||
Assert.notNull(this.asyncWebRequest, "Not initialized");
|
||||
int result = this.asyncWebRequest.tryObtainLock();
|
||||
if (result == -1) {
|
||||
throw new AsyncRequestNotUsableException("Response not usable after " +
|
||||
(this.asyncWebRequest.state == State.COMPLETED ?
|
||||
"async request completion" : "response errors") + ".");
|
||||
if (this.asyncWebRequest.state == State.NEW) {
|
||||
return 0;
|
||||
}
|
||||
return result;
|
||||
|
||||
this.asyncWebRequest.stateLock.lock();
|
||||
if (this.asyncWebRequest.state == State.ASYNC) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
this.asyncWebRequest.stateLock.unlock();
|
||||
throw new AsyncRequestNotUsableException("Response not usable after " +
|
||||
(this.asyncWebRequest.state == State.COMPLETED ?
|
||||
"async request completion" : "response errors") + ".");
|
||||
}
|
||||
|
||||
void handleIOException(IOException ex, String msg) throws AsyncRequestNotUsableException {
|
||||
@@ -380,7 +357,7 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
int level = this.response.obtainLockOrRaiseException();
|
||||
int level = this.response.obtainLockAndCheckState();
|
||||
try {
|
||||
this.delegate.write(b);
|
||||
}
|
||||
@@ -393,7 +370,7 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
}
|
||||
|
||||
public void write(byte[] buf, int offset, int len) throws IOException {
|
||||
int level = this.response.obtainLockOrRaiseException();
|
||||
int level = this.response.obtainLockAndCheckState();
|
||||
try {
|
||||
this.delegate.write(buf, offset, len);
|
||||
}
|
||||
@@ -407,7 +384,7 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
int level = this.response.obtainLockOrRaiseException();
|
||||
int level = this.response.obtainLockAndCheckState();
|
||||
try {
|
||||
this.delegate.flush();
|
||||
}
|
||||
@@ -421,7 +398,7 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
int level = this.response.obtainLockOrRaiseException();
|
||||
int level = this.response.obtainLockAndCheckState();
|
||||
try {
|
||||
this.delegate.close();
|
||||
}
|
||||
@@ -455,7 +432,7 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
int level = this.asyncWebRequest.tryObtainLock();
|
||||
int level = tryObtainLockAndCheckState();
|
||||
if (level > -1) {
|
||||
try {
|
||||
this.delegate.flush();
|
||||
@@ -468,7 +445,7 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
int level = this.asyncWebRequest.tryObtainLock();
|
||||
int level = tryObtainLockAndCheckState();
|
||||
if (level > -1) {
|
||||
try {
|
||||
this.delegate.close();
|
||||
@@ -486,7 +463,7 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
|
||||
@Override
|
||||
public void write(int c) {
|
||||
int level = this.asyncWebRequest.tryObtainLock();
|
||||
int level = tryObtainLockAndCheckState();
|
||||
if (level > -1) {
|
||||
try {
|
||||
this.delegate.write(c);
|
||||
@@ -499,7 +476,7 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
|
||||
@Override
|
||||
public void write(char[] buf, int off, int len) {
|
||||
int level = this.asyncWebRequest.tryObtainLock();
|
||||
int level = tryObtainLockAndCheckState();
|
||||
if (level > -1) {
|
||||
try {
|
||||
this.delegate.write(buf, off, len);
|
||||
@@ -517,7 +494,7 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
|
||||
@Override
|
||||
public void write(String s, int off, int len) {
|
||||
int level = this.asyncWebRequest.tryObtainLock();
|
||||
int level = tryObtainLockAndCheckState();
|
||||
if (level > -1) {
|
||||
try {
|
||||
this.delegate.write(s, off, len);
|
||||
@@ -533,6 +510,22 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
this.delegate.write(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return 0 if checks passed and lock is not needed, 1 if checks passed
|
||||
* and lock is held, and -1 if checks did not pass.
|
||||
*/
|
||||
private int tryObtainLockAndCheckState() {
|
||||
if (this.asyncWebRequest.state == State.NEW) {
|
||||
return 0;
|
||||
}
|
||||
this.asyncWebRequest.stateLock.lock();
|
||||
if (this.asyncWebRequest.state == State.ASYNC) {
|
||||
return 1;
|
||||
}
|
||||
this.asyncWebRequest.stateLock.unlock();
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void releaseLock(int level) {
|
||||
if (level > 0) {
|
||||
this.asyncWebRequest.stateLock.unlock();
|
||||
|
||||
+4
-3
@@ -387,15 +387,16 @@ public final class WebAsyncManager {
|
||||
synchronized (WebAsyncManager.this) {
|
||||
if (!this.state.compareAndSet(State.ASYNC_PROCESSING, State.RESULT_SET)) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Async result already set: [" + this.state.get() +
|
||||
"], ignored result for " + formatUri(this.asyncWebRequest));
|
||||
logger.debug("Async result already set: " +
|
||||
"[" + this.state.get() + "], ignored result: " + result +
|
||||
" for " + formatUri(this.asyncWebRequest));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.concurrentResult = result;
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Async result set for " + formatUri(this.asyncWebRequest));
|
||||
logger.debug("Async result set to: " + result + " for " + formatUri(this.asyncWebRequest));
|
||||
}
|
||||
|
||||
if (this.asyncWebRequest.isAsyncComplete()) {
|
||||
|
||||
+3
-4
@@ -118,13 +118,13 @@ public class ServerHttpObservationFilter extends OncePerRequestFilter {
|
||||
throw ex;
|
||||
}
|
||||
finally {
|
||||
// If async is started during the first dispatch, register a listener for completion notification.
|
||||
if (request.isAsyncStarted() && request.getDispatcherType() == DispatcherType.REQUEST) {
|
||||
// If async is started, register a listener for completion notification.
|
||||
if (request.isAsyncStarted()) {
|
||||
request.getAsyncContext().addListener(new ObservationAsyncListener(observation));
|
||||
}
|
||||
// scope is opened for ASYNC dispatches, but the observation will be closed
|
||||
// by the async listener.
|
||||
else if (!isAsyncDispatch(request)) {
|
||||
else if (request.getDispatcherType() != DispatcherType.ASYNC){
|
||||
Throwable error = fetchException(request);
|
||||
if (error != null) {
|
||||
observation.error(error);
|
||||
@@ -168,7 +168,6 @@ public class ServerHttpObservationFilter extends OncePerRequestFilter {
|
||||
|
||||
@Override
|
||||
public void onStartAsync(AsyncEvent event) {
|
||||
event.getAsyncContext().addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+2
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -53,7 +53,6 @@ import org.springframework.web.util.UriComponents;
|
||||
* in which case it removes but does not use the headers.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.1
|
||||
* @see <a href="https://tools.ietf.org/html/rfc7239">https://tools.ietf.org/html/rfc7239</a>
|
||||
* @see <a href="https://docs.spring.io/spring-framework/reference/web/webflux/reactive-spring.html#webflux-forwarded-headers">Forwarded Headers</a>
|
||||
@@ -166,7 +165,7 @@ public class ForwardedHeaderTransformer implements Function<ServerHttpRequest, S
|
||||
String[] rawPrefixes = StringUtils.tokenizeToStringArray(header, ",");
|
||||
for (String rawPrefix : rawPrefixes) {
|
||||
int endIndex = rawPrefix.length();
|
||||
while (endIndex > 0 && rawPrefix.charAt(endIndex - 1) == '/') {
|
||||
while (endIndex > 1 && rawPrefix.charAt(endIndex - 1) == '/') {
|
||||
endIndex--;
|
||||
}
|
||||
prefix.append((endIndex != rawPrefix.length() ? rawPrefix.substring(0, endIndex) : rawPrefix));
|
||||
|
||||
+2
-2
@@ -132,9 +132,9 @@ public class ResponseStatusExceptionHandler implements WebExceptionHandler {
|
||||
* @param ex the exception to check
|
||||
* @return the associated HTTP status code, or -1 if it can't be derived.
|
||||
* @since 5.3
|
||||
* @deprecated in favor of {@link #determineStatus(Throwable)}, for removal in 7.0
|
||||
* @deprecated as of 6.0, in favor of {@link #determineStatus(Throwable)}
|
||||
*/
|
||||
@Deprecated(since = "6.0", forRemoval = true)
|
||||
@Deprecated(since = "6.0")
|
||||
protected int determineRawStatusCode(Throwable ex) {
|
||||
if (ex instanceof ResponseStatusException responseStatusException) {
|
||||
return responseStatusException.getStatusCode().value();
|
||||
|
||||
+4
-61
@@ -27,19 +27,15 @@ import io.micrometer.observation.Observation;
|
||||
import io.micrometer.observation.ObservationHandler;
|
||||
import io.micrometer.observation.tck.TestObservationRegistry;
|
||||
import io.micrometer.observation.tck.TestObservationRegistryAssert;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpRequest;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.ClientHttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestExecution;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.http.client.observation.ClientRequestObservationContext;
|
||||
import org.springframework.http.client.observation.ClientRequestObservationConvention;
|
||||
@@ -77,15 +73,12 @@ class RestClientObservationTests {
|
||||
|
||||
@BeforeEach
|
||||
void setupEach() {
|
||||
this.client = createBuilder().build();
|
||||
this.observationRegistry.observationConfig().observationHandler(new ContextAssertionObservationHandler());
|
||||
}
|
||||
|
||||
RestClient.Builder createBuilder() {
|
||||
return RestClient.builder()
|
||||
this.client = RestClient.builder()
|
||||
.messageConverters(converters -> converters.add(0, this.converter))
|
||||
.requestFactory(this.requestFactory)
|
||||
.observationRegistry(this.observationRegistry);
|
||||
.observationRegistry(this.observationRegistry)
|
||||
.build();
|
||||
this.observationRegistry.observationConfig().observationHandler(new ContextAssertionObservationHandler());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -245,22 +238,6 @@ class RestClientObservationTests {
|
||||
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS");
|
||||
}
|
||||
|
||||
@Test
|
||||
void openScopeWithObservation() throws Exception {
|
||||
this.client = createBuilder().requestInterceptor(new ObservationContextInterceptor(this.observationRegistry))
|
||||
.defaultStatusHandler(new ObservationErrorHandler(this.observationRegistry)).build();
|
||||
mockSentRequest(GET, "https://example.org");
|
||||
mockResponseStatus(HttpStatus.OK);
|
||||
mockResponseBody("Hello World", MediaType.TEXT_PLAIN);
|
||||
|
||||
client.get().uri("https://example.org").retrieve().toBodilessEntity();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void checkAfter() {
|
||||
assertThat(this.observationRegistry.getCurrentObservationScope()).isNull();
|
||||
}
|
||||
|
||||
|
||||
private void mockSentRequest(HttpMethod method, String uri) throws Exception {
|
||||
mockSentRequest(method, uri, new HttpHeaders());
|
||||
@@ -311,38 +288,4 @@ class RestClientObservationTests {
|
||||
|
||||
}
|
||||
|
||||
static class ObservationContextInterceptor implements ClientHttpRequestInterceptor {
|
||||
|
||||
private final TestObservationRegistry observationRegistry;
|
||||
|
||||
public ObservationContextInterceptor(TestObservationRegistry observationRegistry) {
|
||||
this.observationRegistry = observationRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
|
||||
assertThat(this.observationRegistry.getCurrentObservationScope()).isNotNull();
|
||||
return execution.execute(request, body);
|
||||
}
|
||||
}
|
||||
|
||||
static class ObservationErrorHandler implements ResponseErrorHandler {
|
||||
|
||||
final TestObservationRegistry observationRegistry;
|
||||
|
||||
ObservationErrorHandler(TestObservationRegistry observationRegistry) {
|
||||
this.observationRegistry = observationRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasError(ClientHttpResponse response) throws IOException {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleError(ClientHttpResponse response) throws IOException {
|
||||
assertThat(this.observationRegistry.getCurrentObservationScope()).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-10
@@ -48,7 +48,6 @@ import static org.mockito.Mockito.mock;
|
||||
* @author Eddú Meléndez
|
||||
* @author Rob Winch
|
||||
* @author Brian Clozel
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
class ForwardedHeaderFilterTests {
|
||||
|
||||
@@ -443,15 +442,6 @@ class ForwardedHeaderFilterTests {
|
||||
assertThat(actual.getRequestURL().toString()).isEqualTo("http://localhost/first/second/mvc-showcase");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRemoveSingleTrailingSlash() throws Exception {
|
||||
request.addHeader(X_FORWARDED_PREFIX, "/prefix,/");
|
||||
request.setRequestURI("/mvc-showcase");
|
||||
|
||||
HttpServletRequest actual = filterAndGetWrappedRequest();
|
||||
assertThat(actual.getRequestURL().toString()).isEqualTo("http://localhost/prefix/mvc-showcase");
|
||||
}
|
||||
|
||||
@Test
|
||||
void requestURLNewStringBuffer() throws Exception {
|
||||
request.addHeader(X_FORWARDED_PREFIX, "/prefix/");
|
||||
|
||||
-42
@@ -21,16 +21,11 @@ import java.io.IOException;
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import io.micrometer.observation.tck.TestObservationRegistry;
|
||||
import io.micrometer.observation.tck.TestObservationRegistryAssert;
|
||||
import jakarta.servlet.AsyncContext;
|
||||
import jakarta.servlet.AsyncEvent;
|
||||
import jakarta.servlet.AsyncListener;
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.RequestDispatcher;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@@ -144,21 +139,6 @@ class ServerHttpObservationFilterTests {
|
||||
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS").hasBeenStopped();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRegisterListenerForAsyncStarts() throws Exception {
|
||||
CustomAsyncFilter customAsyncFilter = new CustomAsyncFilter();
|
||||
this.mockFilterChain = new MockFilterChain(new NoOpServlet(), customAsyncFilter);
|
||||
this.request.setAsyncSupported(true);
|
||||
this.request.setDispatcherType(DispatcherType.REQUEST);
|
||||
this.filter.doFilter(this.request, this.response, this.mockFilterChain);
|
||||
customAsyncFilter.asyncContext.dispatch();
|
||||
this.request.setDispatcherType(DispatcherType.ASYNC);
|
||||
AsyncContext newAsyncContext = this.request.startAsync();
|
||||
newAsyncContext.complete();
|
||||
|
||||
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS").hasBeenStopped();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCloseObservationAfterAsyncError() throws Exception {
|
||||
this.request.setAsyncSupported(true);
|
||||
@@ -207,26 +187,4 @@ class ServerHttpObservationFilterTests {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
static class NoOpServlet extends HttpServlet {
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class CustomAsyncFilter implements Filter {
|
||||
|
||||
AsyncContext asyncContext;
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
|
||||
this.asyncContext = servletRequest.startAsync();
|
||||
filterChain.doFilter(servletRequest, servletResponse);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-12
@@ -32,7 +32,6 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
* Tests for {@link ForwardedHeaderTransformer}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
class ForwardedHeaderTransformerTests {
|
||||
|
||||
@@ -171,17 +170,6 @@ class ForwardedHeaderTransformerTests {
|
||||
assertForwardedHeadersRemoved(request);
|
||||
}
|
||||
|
||||
@Test // gh-33465
|
||||
void shouldRemoveSingleTrailingSlash() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add("X-Forwarded-Prefix", "/prefix,/");
|
||||
ServerHttpRequest request = this.requestMutator.apply(getRequest(headers));
|
||||
|
||||
assertThat(request.getURI()).isEqualTo(URI.create("https://example.com/prefix/path"));
|
||||
assertThat(request.getPath().value()).isEqualTo("/prefix/path");
|
||||
assertForwardedHeadersRemoved(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedForNotPresent() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.bind.annotation
|
||||
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.aot.hint.*
|
||||
|
||||
/**
|
||||
* Kotlin tests for {@link ControllerMappingReflectiveProcessor}.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
class ControllerMappingReflectiveProcessorKotlinTests {
|
||||
|
||||
private val processor = ControllerMappingReflectiveProcessor()
|
||||
|
||||
private val hints = ReflectionHints()
|
||||
|
||||
@Test
|
||||
fun registerReflectiveHintsForFunctionWithDefaultArgumentValue() {
|
||||
val method = SampleController::class.java.getDeclaredMethod("defaultValue", Boolean::class.javaObjectType)
|
||||
processor.registerReflectionHints(hints, method)
|
||||
Assertions.assertThat(hints.typeHints()).satisfiesExactlyInAnyOrder(
|
||||
{
|
||||
Assertions.assertThat(it.type).isEqualTo(TypeReference.of(SampleController::class.java))
|
||||
Assertions.assertThat(it.methods()).extracting<String> { executableHint: ExecutableHint -> executableHint.name }
|
||||
.containsExactlyInAnyOrder("defaultValue", "defaultValue\$default")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
class SampleController {
|
||||
|
||||
@GetMapping("/defaultValue")
|
||||
fun defaultValue(@RequestParam(required = false) argument: Boolean? = false) = argument
|
||||
}
|
||||
|
||||
}
|
||||
+1
-15
@@ -44,8 +44,6 @@ import java.util.TimeZone;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.servlet.AsyncContext;
|
||||
import jakarta.servlet.AsyncEvent;
|
||||
import jakarta.servlet.AsyncListener;
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.RequestDispatcher;
|
||||
import jakarta.servlet.ServletConnection;
|
||||
@@ -923,19 +921,7 @@ public class MockHttpServletRequest implements HttpServletRequest {
|
||||
public AsyncContext startAsync(ServletRequest request, @Nullable ServletResponse response) {
|
||||
Assert.state(this.asyncSupported, "Async not supported");
|
||||
this.asyncStarted = true;
|
||||
MockAsyncContext newAsyncContext = new MockAsyncContext(request, response);
|
||||
if (this.asyncContext != null) {
|
||||
try {
|
||||
AsyncEvent startEvent = new AsyncEvent(newAsyncContext);
|
||||
for (AsyncListener asyncListener : this.asyncContext.getListeners()) {
|
||||
asyncListener.onStartAsync(startEvent);
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
// ignore failures
|
||||
}
|
||||
}
|
||||
this.asyncContext = newAsyncContext;
|
||||
this.asyncContext = new MockAsyncContext(request, response);
|
||||
return this.asyncContext;
|
||||
}
|
||||
|
||||
|
||||
+1
-12
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -57,17 +57,6 @@ public interface ClientResponse {
|
||||
*/
|
||||
HttpStatusCode statusCode();
|
||||
|
||||
/**
|
||||
* Return the raw status code of this response.
|
||||
* @return the HTTP status as an integer value
|
||||
* @since 5.1
|
||||
* @deprecated in favor of {@link #statusCode()}, for removal in 7.0
|
||||
*/
|
||||
@Deprecated(since = "6.0", forRemoval = true)
|
||||
default int rawStatusCode() {
|
||||
return statusCode().value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the headers of this response.
|
||||
*/
|
||||
|
||||
+1
-1
@@ -795,7 +795,7 @@ public interface WebClient {
|
||||
* .retrieve()
|
||||
* .bodyToMono(Account.class)
|
||||
* .onErrorResume(WebClientResponseException.class,
|
||||
* ex -> ex.getStatusCode().value() == 404 ? Mono.empty() : Mono.error(ex));
|
||||
* ex -> ex.getRawStatusCode() == 404 ? Mono.empty() : Mono.error(ex));
|
||||
* </pre>
|
||||
* @param statusPredicate to match responses with
|
||||
* @param exceptionFunction to map the response to an error signal
|
||||
|
||||
+1
-1
@@ -172,7 +172,7 @@ public class WebClientResponseException extends WebClientException {
|
||||
|
||||
/**
|
||||
* Return the raw HTTP status code value.
|
||||
* @deprecated in favor of {@link #getStatusCode()}, for removal in 7.0
|
||||
* @deprecated as of 6.0, in favor of {@link #getStatusCode()}
|
||||
*/
|
||||
@Deprecated(since = "6.0")
|
||||
public int getRawStatusCode() {
|
||||
|
||||
-1
@@ -323,7 +323,6 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder {
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@SuppressWarnings("removal")
|
||||
public int rawStatusCode() {
|
||||
return this.statusCode.value();
|
||||
}
|
||||
|
||||
+14
-93
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,7 +18,6 @@ package org.springframework.web.reactive.function.server;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.function.Function;
|
||||
|
||||
@@ -31,7 +30,6 @@ import org.springframework.http.server.PathContainer;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ResourceUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.UriUtils;
|
||||
import org.springframework.web.util.pattern.PathPattern;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
@@ -65,17 +63,13 @@ class PathResourceLookupFunction implements Function<ServerRequest, Mono<Resourc
|
||||
|
||||
pathContainer = this.pattern.extractPathWithinPattern(pathContainer);
|
||||
String path = processPath(pathContainer.value());
|
||||
if (!StringUtils.hasText(path) || isInvalidPath(path)) {
|
||||
return Mono.empty();
|
||||
if (path.contains("%")) {
|
||||
path = StringUtils.uriDecode(path, StandardCharsets.UTF_8);
|
||||
}
|
||||
if (isInvalidEncodedInputPath(path)) {
|
||||
if (!StringUtils.hasLength(path) || isInvalidPath(path)) {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
if (!(this.location instanceof UrlResource)) {
|
||||
path = UriUtils.decode(path, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
try {
|
||||
Resource resource = this.location.createRelative(path);
|
||||
if (resource.isReadable() && isResourceUnderLocation(resource)) {
|
||||
@@ -90,47 +84,7 @@ class PathResourceLookupFunction implements Function<ServerRequest, Mono<Resourc
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the given resource path.
|
||||
* <p>The default implementation replaces:
|
||||
* <ul>
|
||||
* <li>Backslash with forward slash.
|
||||
* <li>Duplicate occurrences of slash with a single slash.
|
||||
* <li>Any combination of leading slash and control characters (00-1F and 7F)
|
||||
* with a single "/" or "". For example {@code " / // foo/bar"}
|
||||
* becomes {@code "/foo/bar"}.
|
||||
* </ul>
|
||||
*/
|
||||
protected String processPath(String path) {
|
||||
path = StringUtils.replace(path, "\\", "/");
|
||||
path = cleanDuplicateSlashes(path);
|
||||
return cleanLeadingSlash(path);
|
||||
}
|
||||
|
||||
private String cleanDuplicateSlashes(String path) {
|
||||
StringBuilder sb = null;
|
||||
char prev = 0;
|
||||
for (int i = 0; i < path.length(); i++) {
|
||||
char curr = path.charAt(i);
|
||||
try {
|
||||
if (curr == '/' && prev == '/') {
|
||||
if (sb == null) {
|
||||
sb = new StringBuilder(path.substring(0, i));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (sb != null) {
|
||||
sb.append(path.charAt(i));
|
||||
}
|
||||
}
|
||||
finally {
|
||||
prev = curr;
|
||||
}
|
||||
}
|
||||
return (sb != null ? sb.toString() : path);
|
||||
}
|
||||
|
||||
private String cleanLeadingSlash(String path) {
|
||||
private String processPath(String path) {
|
||||
boolean slash = false;
|
||||
for (int i = 0; i < path.length(); i++) {
|
||||
if (path.charAt(i) == '/') {
|
||||
@@ -140,7 +94,8 @@ class PathResourceLookupFunction implements Function<ServerRequest, Mono<Resourc
|
||||
if (i == 0 || (i == 1 && slash)) {
|
||||
return path;
|
||||
}
|
||||
return (slash ? "/" + path.substring(i) : path.substring(i));
|
||||
path = slash ? "/" + path.substring(i) : path.substring(i);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
return (slash ? "/" : "");
|
||||
@@ -162,31 +117,6 @@ class PathResourceLookupFunction implements Function<ServerRequest, Mono<Resourc
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given path contains invalid escape sequences.
|
||||
* @param path the path to validate
|
||||
* @return {@code true} if the path is invalid, {@code false} otherwise
|
||||
*/
|
||||
private boolean isInvalidEncodedInputPath(String path) {
|
||||
if (path.contains("%")) {
|
||||
try {
|
||||
// Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars
|
||||
String decodedPath = URLDecoder.decode(path, StandardCharsets.UTF_8);
|
||||
if (isInvalidPath(decodedPath)) {
|
||||
return true;
|
||||
}
|
||||
decodedPath = processPath(decodedPath);
|
||||
if (isInvalidPath(decodedPath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
// May not be possible to decode...
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isResourceUnderLocation(Resource resource) throws IOException {
|
||||
if (resource.getClass() != this.location.getClass()) {
|
||||
return false;
|
||||
@@ -212,24 +142,15 @@ class PathResourceLookupFunction implements Function<ServerRequest, Mono<Resourc
|
||||
return true;
|
||||
}
|
||||
locationPath = (locationPath.endsWith("/") || locationPath.isEmpty() ? locationPath : locationPath + "/");
|
||||
return (resourcePath.startsWith(locationPath) && !isInvalidEncodedInputPath(resourcePath));
|
||||
if (!resourcePath.startsWith(locationPath)) {
|
||||
return false;
|
||||
}
|
||||
if (resourcePath.contains("%") && StringUtils.uriDecode(resourcePath, StandardCharsets.UTF_8).contains("../")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isInvalidEncodedResourcePath(String resourcePath) {
|
||||
if (resourcePath.contains("%")) {
|
||||
// Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars...
|
||||
try {
|
||||
String decodedPath = URLDecoder.decode(resourcePath, StandardCharsets.UTF_8);
|
||||
if (decodedPath.contains("../") || decodedPath.contains("..\\")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
// May not be possible to decode...
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -70,9 +70,9 @@ public interface ServerResponse {
|
||||
* Return the status code of this response as integer.
|
||||
* @return the status as an integer
|
||||
* @since 5.2
|
||||
* @deprecated in favor of {@link #statusCode()}, for removal in 7.0
|
||||
* @deprecated as of 6.0, in favor of {@link #statusCode()}
|
||||
*/
|
||||
@Deprecated(since = "6.0", forRemoval = true)
|
||||
@Deprecated(since = "6.0")
|
||||
int rawStatusCode();
|
||||
|
||||
/**
|
||||
|
||||
+2
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2017 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.
|
||||
@@ -84,12 +84,7 @@ class DefaultRenderingBuilder implements Rendering.RedirectBuilder {
|
||||
|
||||
@Override
|
||||
public DefaultRenderingBuilder status(HttpStatusCode status) {
|
||||
if (this.view instanceof RedirectView redirectView) {
|
||||
redirectView.setStatusCode(status);
|
||||
}
|
||||
else {
|
||||
this.status = status;
|
||||
}
|
||||
this.status = status;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -84,7 +84,7 @@ class DefaultEntityResponseBuilderTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("removal")
|
||||
@SuppressWarnings("deprecation")
|
||||
void status() {
|
||||
String body = "foo";
|
||||
Mono<EntityResponse<String>> result = EntityResponse.fromObject(body).status(HttpStatus.CREATED).build();
|
||||
|
||||
+1
-1
@@ -80,7 +80,7 @@ class DefaultServerResponseBuilderTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("removal")
|
||||
@SuppressWarnings("deprecation")
|
||||
void status() {
|
||||
Mono<ServerResponse> result = ServerResponse.status(HttpStatus.CREATED).build();
|
||||
StepVerifier.create(result)
|
||||
|
||||
+2
-2
@@ -223,7 +223,7 @@ class RouterFunctionsTests {
|
||||
public HttpStatus statusCode() {
|
||||
return HttpStatus.OK;
|
||||
}
|
||||
@SuppressWarnings("removal")
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public int rawStatusCode() {
|
||||
return 200;
|
||||
@@ -262,7 +262,7 @@ class RouterFunctionsTests {
|
||||
public HttpStatus statusCode() {
|
||||
return HttpStatus.OK;
|
||||
}
|
||||
@SuppressWarnings("removal")
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public int rawStatusCode() {
|
||||
return 200;
|
||||
|
||||
+7
-19
@@ -23,7 +23,6 @@ import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -49,17 +48,16 @@ class DefaultRenderingBuilderTests {
|
||||
Rendering rendering = Rendering.redirectTo("abc").build();
|
||||
|
||||
Object view = rendering.view();
|
||||
assertThat(view).isExactlyInstanceOf(RedirectView.class);
|
||||
RedirectView redirectView = (RedirectView) view;
|
||||
assertThat(redirectView.getUrl()).isEqualTo("abc");
|
||||
assertThat(redirectView.isContextRelative()).isTrue();
|
||||
assertThat(redirectView.isPropagateQuery()).isFalse();
|
||||
assertThat(view.getClass()).isEqualTo(RedirectView.class);
|
||||
assertThat(((RedirectView) view).getUrl()).isEqualTo("abc");
|
||||
assertThat(((RedirectView) view).isContextRelative()).isTrue();
|
||||
assertThat(((RedirectView) view).isPropagateQuery()).isFalse();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void viewName() {
|
||||
Rendering rendering = Rendering.view("foo").build();
|
||||
|
||||
assertThat(rendering.view()).isEqualTo("foo");
|
||||
}
|
||||
|
||||
@@ -115,7 +113,7 @@ class DefaultRenderingBuilderTests {
|
||||
Rendering rendering = Rendering.redirectTo("foo").contextRelative(false).build();
|
||||
|
||||
Object view = rendering.view();
|
||||
assertThat(view).isExactlyInstanceOf(RedirectView.class);
|
||||
assertThat(view.getClass()).isEqualTo(RedirectView.class);
|
||||
assertThat(((RedirectView) view).isContextRelative()).isFalse();
|
||||
}
|
||||
|
||||
@@ -124,20 +122,10 @@ class DefaultRenderingBuilderTests {
|
||||
Rendering rendering = Rendering.redirectTo("foo").propagateQuery(true).build();
|
||||
|
||||
Object view = rendering.view();
|
||||
assertThat(view).isExactlyInstanceOf(RedirectView.class);
|
||||
assertThat(view.getClass()).isEqualTo(RedirectView.class);
|
||||
assertThat(((RedirectView) view).isPropagateQuery()).isTrue();
|
||||
}
|
||||
|
||||
@Test // gh-33498
|
||||
void redirectWithCustomStatus() {
|
||||
HttpStatus status = HttpStatus.MOVED_PERMANENTLY;
|
||||
Rendering rendering = Rendering.redirectTo("foo").status(status).build();
|
||||
|
||||
Object view = rendering.view();
|
||||
assertThat(view).isExactlyInstanceOf(RedirectView.class);
|
||||
assertThat(((RedirectView) view).getStatusCode()).isEqualTo(status);
|
||||
}
|
||||
|
||||
|
||||
private static class Foo {}
|
||||
|
||||
|
||||
-15
@@ -16,7 +16,6 @@
|
||||
|
||||
package org.springframework.web.reactive.result.view;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
@@ -197,20 +196,6 @@ class ViewResolutionResultHandlerTests {
|
||||
assertThat(exchange.getResponse().getHeaders().getFirst("h")).isEqualTo("h1");
|
||||
}
|
||||
|
||||
@Test // gh-33498
|
||||
void handleRedirect() {
|
||||
HttpStatus status = HttpStatus.MOVED_PERMANENTLY;
|
||||
Rendering returnValue = Rendering.redirectTo("foo").status(status).build();
|
||||
MethodParameter returnType = on(Handler.class).resolveReturnType(Rendering.class);
|
||||
HandlerResult result = new HandlerResult(new Object(), returnValue, returnType, this.bindingContext);
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(get("/path"));
|
||||
resultHandler(new UrlBasedViewResolver()).handleResult(exchange, result).block(Duration.ofSeconds(5));
|
||||
|
||||
assertThat(exchange.getResponse().getStatusCode()).isEqualTo(status);
|
||||
assertThat(exchange.getResponse().getHeaders().getLocation()).isEqualTo(URI.create("foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void handleWithMultipleResolvers() {
|
||||
testHandle("/account",
|
||||
|
||||
-1
@@ -67,7 +67,6 @@ abstract class AbstractServerResponse extends ErrorHandlingServerResponse {
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@SuppressWarnings("removal")
|
||||
public int rawStatusCode() {
|
||||
return this.statusCode.value();
|
||||
}
|
||||
|
||||
-1
@@ -89,7 +89,6 @@ final class DefaultAsyncServerResponse extends ErrorHandlingServerResponse imple
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@SuppressWarnings("removal")
|
||||
public int rawStatusCode() {
|
||||
return delegate(ServerResponse::rawStatusCode);
|
||||
}
|
||||
|
||||
+11
-93
@@ -18,7 +18,6 @@ package org.springframework.web.servlet.function;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
@@ -30,8 +29,6 @@ import org.springframework.http.server.PathContainer;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ResourceUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.context.support.ServletContextResource;
|
||||
import org.springframework.web.util.UriUtils;
|
||||
import org.springframework.web.util.pattern.PathPattern;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
@@ -39,7 +36,6 @@ import org.springframework.web.util.pattern.PathPatternParser;
|
||||
* Lookup function used by {@link RouterFunctions#resources(String, Resource)}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.2
|
||||
*/
|
||||
class PathResourceLookupFunction implements Function<ServerRequest, Optional<Resource>> {
|
||||
@@ -66,17 +62,13 @@ class PathResourceLookupFunction implements Function<ServerRequest, Optional<Res
|
||||
|
||||
pathContainer = this.pattern.extractPathWithinPattern(pathContainer);
|
||||
String path = processPath(pathContainer.value());
|
||||
if (!StringUtils.hasText(path) || isInvalidPath(path)) {
|
||||
return Optional.empty();
|
||||
if (path.contains("%")) {
|
||||
path = StringUtils.uriDecode(path, StandardCharsets.UTF_8);
|
||||
}
|
||||
if (isInvalidEncodedInputPath(path)) {
|
||||
if (!StringUtils.hasLength(path) || isInvalidPath(path)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (!(this.location instanceof UrlResource)) {
|
||||
path = UriUtils.decode(path, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
try {
|
||||
Resource resource = this.location.createRelative(path);
|
||||
if (resource.isReadable() && isResourceUnderLocation(resource)) {
|
||||
@@ -91,47 +83,7 @@ class PathResourceLookupFunction implements Function<ServerRequest, Optional<Res
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the given resource path.
|
||||
* <p>The default implementation replaces:
|
||||
* <ul>
|
||||
* <li>Backslash with forward slash.
|
||||
* <li>Duplicate occurrences of slash with a single slash.
|
||||
* <li>Any combination of leading slash and control characters (00-1F and 7F)
|
||||
* with a single "/" or "". For example {@code " / // foo/bar"}
|
||||
* becomes {@code "/foo/bar"}.
|
||||
* </ul>
|
||||
*/
|
||||
protected String processPath(String path) {
|
||||
path = StringUtils.replace(path, "\\", "/");
|
||||
path = cleanDuplicateSlashes(path);
|
||||
return cleanLeadingSlash(path);
|
||||
}
|
||||
|
||||
private String cleanDuplicateSlashes(String path) {
|
||||
StringBuilder sb = null;
|
||||
char prev = 0;
|
||||
for (int i = 0; i < path.length(); i++) {
|
||||
char curr = path.charAt(i);
|
||||
try {
|
||||
if ((curr == '/') && (prev == '/')) {
|
||||
if (sb == null) {
|
||||
sb = new StringBuilder(path.substring(0, i));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (sb != null) {
|
||||
sb.append(path.charAt(i));
|
||||
}
|
||||
}
|
||||
finally {
|
||||
prev = curr;
|
||||
}
|
||||
}
|
||||
return sb != null ? sb.toString() : path;
|
||||
}
|
||||
|
||||
private String cleanLeadingSlash(String path) {
|
||||
private String processPath(String path) {
|
||||
boolean slash = false;
|
||||
for (int i = 0; i < path.length(); i++) {
|
||||
if (path.charAt(i) == '/') {
|
||||
@@ -141,7 +93,8 @@ class PathResourceLookupFunction implements Function<ServerRequest, Optional<Res
|
||||
if (i == 0 || (i == 1 && slash)) {
|
||||
return path;
|
||||
}
|
||||
return (slash ? "/" + path.substring(i) : path.substring(i));
|
||||
path = slash ? "/" + path.substring(i) : path.substring(i);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
return (slash ? "/" : "");
|
||||
@@ -160,26 +113,6 @@ class PathResourceLookupFunction implements Function<ServerRequest, Optional<Res
|
||||
return path.contains("..") && StringUtils.cleanPath(path).contains("../");
|
||||
}
|
||||
|
||||
private boolean isInvalidEncodedInputPath(String path) {
|
||||
if (path.contains("%")) {
|
||||
try {
|
||||
// Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars
|
||||
String decodedPath = URLDecoder.decode(path, StandardCharsets.UTF_8);
|
||||
if (isInvalidPath(decodedPath)) {
|
||||
return true;
|
||||
}
|
||||
decodedPath = processPath(decodedPath);
|
||||
if (isInvalidPath(decodedPath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
// May not be possible to decode...
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isResourceUnderLocation(Resource resource) throws IOException {
|
||||
if (resource.getClass() != this.location.getClass()) {
|
||||
return false;
|
||||
@@ -196,10 +129,6 @@ class PathResourceLookupFunction implements Function<ServerRequest, Optional<Res
|
||||
resourcePath = classPathResource.getPath();
|
||||
locationPath = StringUtils.cleanPath(((ClassPathResource) this.location).getPath());
|
||||
}
|
||||
else if (resource instanceof ServletContextResource servletContextResource) {
|
||||
resourcePath = servletContextResource.getPath();
|
||||
locationPath = StringUtils.cleanPath(((ServletContextResource) this.location).getPath());
|
||||
}
|
||||
else {
|
||||
resourcePath = resource.getURL().getPath();
|
||||
locationPath = StringUtils.cleanPath(this.location.getURL().getPath());
|
||||
@@ -209,24 +138,13 @@ class PathResourceLookupFunction implements Function<ServerRequest, Optional<Res
|
||||
return true;
|
||||
}
|
||||
locationPath = (locationPath.endsWith("/") || locationPath.isEmpty() ? locationPath : locationPath + "/");
|
||||
return (resourcePath.startsWith(locationPath) && !isInvalidEncodedResourcePath(resourcePath));
|
||||
if (!resourcePath.startsWith(locationPath)) {
|
||||
return false;
|
||||
}
|
||||
return !resourcePath.contains("%") ||
|
||||
!StringUtils.uriDecode(resourcePath, StandardCharsets.UTF_8).contains("../");
|
||||
}
|
||||
|
||||
private boolean isInvalidEncodedResourcePath(String resourcePath) {
|
||||
if (resourcePath.contains("%")) {
|
||||
// Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars...
|
||||
try {
|
||||
String decodedPath = URLDecoder.decode(resourcePath, StandardCharsets.UTF_8);
|
||||
if (decodedPath.contains("../") || decodedPath.contains("..\\")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
// May not be possible to decode...
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
+16
-14
@@ -160,7 +160,6 @@ public abstract class RouterFunctions {
|
||||
*/
|
||||
public static RouterFunction<ServerResponse> resource(RequestPredicate predicate, Resource resource,
|
||||
BiConsumer<Resource, HttpHeaders> headersConsumer) {
|
||||
|
||||
return resources(new PredicateResourceLookupFunction(predicate, resource), headersConsumer);
|
||||
}
|
||||
|
||||
@@ -198,7 +197,6 @@ public abstract class RouterFunctions {
|
||||
*/
|
||||
public static RouterFunction<ServerResponse> resources(String pattern, Resource location,
|
||||
BiConsumer<Resource, HttpHeaders> headersConsumer) {
|
||||
|
||||
return resources(resourceLookupFunction(pattern, location), headersConsumer);
|
||||
}
|
||||
|
||||
@@ -242,9 +240,7 @@ public abstract class RouterFunctions {
|
||||
* @return a router function that routes to resources
|
||||
* @since 6.1
|
||||
*/
|
||||
public static RouterFunction<ServerResponse> resources(Function<ServerRequest, Optional<Resource>> lookupFunction,
|
||||
BiConsumer<Resource, HttpHeaders> headersConsumer) {
|
||||
|
||||
public static RouterFunction<ServerResponse> resources(Function<ServerRequest, Optional<Resource>> lookupFunction, BiConsumer<Resource, HttpHeaders> headersConsumer) {
|
||||
return new ResourcesRouterFunction(lookupFunction, headersConsumer);
|
||||
}
|
||||
|
||||
@@ -254,12 +250,12 @@ public abstract class RouterFunctions {
|
||||
* can be used to change the {@code PathPatternParser} properties from the defaults, for instance to change
|
||||
* {@linkplain PathPatternParser#setCaseSensitive(boolean) case sensitivity}.
|
||||
* @param routerFunction the router function to change the parser in
|
||||
* @param parser the parser to change to
|
||||
* @param parser the parser to change to.
|
||||
* @param <T> the type of response returned by the handler function
|
||||
* @return the change router function
|
||||
*/
|
||||
public static <T extends ServerResponse> RouterFunction<T> changeParser(
|
||||
RouterFunction<T> routerFunction, PathPatternParser parser) {
|
||||
public static <T extends ServerResponse> RouterFunction<T> changeParser(RouterFunction<T> routerFunction,
|
||||
PathPatternParser parser) {
|
||||
|
||||
Assert.notNull(routerFunction, "RouterFunction must not be null");
|
||||
Assert.notNull(parser, "Parser must not be null");
|
||||
@@ -1155,6 +1151,7 @@ public abstract class RouterFunctions {
|
||||
public void accept(Visitor visitor) {
|
||||
visitor.route(this.predicate, this.handlerFunction);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1176,10 +1173,13 @@ public abstract class RouterFunctions {
|
||||
return this.predicate.nest(serverRequest)
|
||||
.map(nestedRequest -> {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace(String.format("Nested predicate \"%s\" matches against \"%s\"",
|
||||
this.predicate, serverRequest));
|
||||
logger.trace(
|
||||
String.format(
|
||||
"Nested predicate \"%s\" matches against \"%s\"",
|
||||
this.predicate, serverRequest));
|
||||
}
|
||||
Optional<HandlerFunction<T>> result = this.routerFunction.route(nestedRequest);
|
||||
Optional<HandlerFunction<T>> result =
|
||||
this.routerFunction.route(nestedRequest);
|
||||
if (result.isPresent() && nestedRequest != serverRequest) {
|
||||
serverRequest.attributes().clear();
|
||||
serverRequest.attributes().putAll(nestedRequest.attributes());
|
||||
@@ -1197,6 +1197,7 @@ public abstract class RouterFunctions {
|
||||
this.routerFunction.accept(visitor);
|
||||
visitor.endNested(this.predicate);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1206,11 +1207,11 @@ public abstract class RouterFunctions {
|
||||
|
||||
private final BiConsumer<Resource, HttpHeaders> headersConsumer;
|
||||
|
||||
|
||||
public ResourcesRouterFunction(Function<ServerRequest, Optional<Resource>> lookupFunction,
|
||||
BiConsumer<Resource, HttpHeaders> headersConsumer) {
|
||||
|
||||
Assert.notNull(lookupFunction, "Lookup function must not be null");
|
||||
Assert.notNull(headersConsumer, "Headers consumer must not be null");
|
||||
Assert.notNull(lookupFunction, "Function must not be null");
|
||||
Assert.notNull(headersConsumer, "HeadersConsumer must not be null");
|
||||
this.lookupFunction = lookupFunction;
|
||||
this.headersConsumer = headersConsumer;
|
||||
}
|
||||
@@ -1278,4 +1279,5 @@ public abstract class RouterFunctions {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -68,9 +68,9 @@ public interface ServerResponse {
|
||||
/**
|
||||
* Return the status code of this response as integer.
|
||||
* @return the status as an integer
|
||||
* @deprecated in favor of {@link #statusCode()}, for removal in 7.0
|
||||
* @deprecated as of 6.0, in favor of {@link #statusCode()}
|
||||
*/
|
||||
@Deprecated(since = "6.0", forRemoval = true)
|
||||
@Deprecated(since = "6.0")
|
||||
int rawStatusCode();
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ class DefaultEntityResponseBuilderTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("removal")
|
||||
@SuppressWarnings("deprecation")
|
||||
void status() {
|
||||
String body = "foo";
|
||||
EntityResponse<String> result =
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ class DefaultServerResponseBuilderTests {
|
||||
static final ServerResponse.Context EMPTY_CONTEXT = Collections::emptyList;
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("removal")
|
||||
@SuppressWarnings("deprecation")
|
||||
void status() {
|
||||
ServerResponse response = ServerResponse.status(HttpStatus.CREATED).build();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED);
|
||||
|
||||
-10
@@ -20,7 +20,6 @@ import java.lang.reflect.Method;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -30,12 +29,10 @@ import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import jakarta.validation.executable.ExecutableValidator;
|
||||
import jakarta.validation.metadata.BeanDescriptor;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.context.MessageSourceResolvable;
|
||||
import org.springframework.context.i18n.LocaleContextHolder;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
@@ -95,8 +92,6 @@ class MethodValidationTests {
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws Exception {
|
||||
LocaleContextHolder.setDefaultLocale(Locale.UK);
|
||||
|
||||
LocalValidatorFactoryBean validatorBean = new LocalValidatorFactoryBean();
|
||||
validatorBean.afterPropertiesSet();
|
||||
this.jakartaValidator = new InvocationCountingValidator(validatorBean);
|
||||
@@ -126,11 +121,6 @@ class MethodValidationTests {
|
||||
return handlerAdapter;
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void reset() {
|
||||
LocaleContextHolder.setDefaultLocale(null);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void modelAttribute() {
|
||||
|
||||
@@ -22,15 +22,14 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.servlet.jsp.tagext.Tag;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.context.i18n.LocaleContextHolder;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.core.env.MapPropertySource;
|
||||
import org.springframework.format.annotation.NumberFormat;
|
||||
import org.springframework.format.annotation.NumberFormat.Style;
|
||||
import org.springframework.format.number.PercentStyleFormatter;
|
||||
import org.springframework.format.support.FormattingConversionServiceFactoryBean;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
|
||||
@@ -50,8 +49,6 @@ class EvalTagTests extends AbstractTagTests {
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
LocaleContextHolder.setDefaultLocale(Locale.UK);
|
||||
|
||||
context = createPageContext();
|
||||
FormattingConversionServiceFactoryBean factory = new FormattingConversionServiceFactoryBean();
|
||||
factory.afterPropertiesSet();
|
||||
@@ -61,11 +58,6 @@ class EvalTagTests extends AbstractTagTests {
|
||||
tag.setPageContext(context);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void reset() {
|
||||
LocaleContextHolder.setDefaultLocale(null);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void printScopedAttributeResult() throws Exception {
|
||||
@@ -89,12 +81,13 @@ class EvalTagTests extends AbstractTagTests {
|
||||
|
||||
@Test
|
||||
void printFormattedScopedAttributeResult() throws Exception {
|
||||
PercentStyleFormatter formatter = new PercentStyleFormatter();
|
||||
tag.setExpression("bean.formattable");
|
||||
int action = tag.doStartTag();
|
||||
assertThat(action).isEqualTo(Tag.EVAL_BODY_INCLUDE);
|
||||
action = tag.doEndTag();
|
||||
assertThat(action).isEqualTo(Tag.EVAL_PAGE);
|
||||
assertThat(((MockHttpServletResponse) context.getResponse()).getContentAsString()).isEqualTo("25%");
|
||||
assertThat(((MockHttpServletResponse) context.getResponse()).getContentAsString()).isEqualTo(formatter.print(new BigDecimal(".25"), Locale.getDefault()));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -179,7 +179,7 @@
|
||||
<property name="offset" value="0"/>
|
||||
</module>
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.javadoc.AtclauseOrderCheck">
|
||||
<property name="target" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, RECORD_DEF"/>
|
||||
<property name="target" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF"/>
|
||||
<property name="tagOrder" value="@author, @since, @param, @see, @version, @serial, @deprecated"/>
|
||||
</module>
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.javadoc.AtclauseOrderCheck">
|
||||
|
||||
Reference in New Issue
Block a user