diff --git a/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java b/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java index 21c35afd473..e9a918cfc6d 100644 --- a/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java @@ -409,6 +409,9 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor if (logger.isWarnEnabled()) { logger.warn("Failed to stop bean '" + beanName + "'", ex); } + if (bean instanceof SmartLifecycle) { + latch.countDown(); + } } } } diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java index 2b19d68f752..e79c7195d76 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java @@ -126,6 +126,35 @@ public @interface Scheduled { */ String zone() default ""; + /** + * Execute the annotated method with a fixed period between invocations. + *

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. + *

The duration String can be in several formats: + *

+ * @return the period as a String value — for example a placeholder, + * or a {@link org.springframework.format.annotation.DurationFormat.Style#ISO8601 java.time.Duration} compliant value + * or a {@link org.springframework.format.annotation.DurationFormat.Style#SIMPLE simple format} 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. @@ -143,13 +172,13 @@ public @interface Scheduled { * last invocation and the start of the next. *

The duration String can be in several formats: *

*

NOTE: With virtual threads, fixed rates and cron triggers are recommended * over fixed delays. Fixed-delay tasks operate on a single scheduler thread @@ -162,35 +191,6 @@ public @interface Scheduled { */ String fixedDelayString() default ""; - /** - * Execute the annotated method with a fixed period between invocations. - *

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. - *

The duration String can be in several formats: - *

- * @return the period as a String value — for example a placeholder, - * or a {@link org.springframework.format.annotation.DurationFormat.Style#ISO8601 java.time.Duration} compliant value - * or a {@link org.springframework.format.annotation.DurationFormat.Style#SIMPLE simple format} 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. diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java index bf93682634c..8b921c1bda3 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java @@ -34,6 +34,7 @@ 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; @@ -979,7 +980,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol .formatted(rootPath.toAbsolutePath(), subPattern)); } - try (Stream files = Files.walk(rootPath)) { + try (Stream files = Files.walk(rootPath, FileVisitOption.FOLLOW_LINKS)) { files.filter(isMatchingFile).sorted().map(FileSystemResource::new).forEach(result::add); } catch (Exception ex) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java index 4a10d726127..db0c724f7b5 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java @@ -160,6 +160,7 @@ public abstract class RouterFunctions { */ public static RouterFunction resource(RequestPredicate predicate, Resource resource, BiConsumer headersConsumer) { + return resources(new PredicateResourceLookupFunction(predicate, resource), headersConsumer); } @@ -197,6 +198,7 @@ public abstract class RouterFunctions { */ public static RouterFunction resources(String pattern, Resource location, BiConsumer headersConsumer) { + return resources(resourceLookupFunction(pattern, location), headersConsumer); } @@ -240,7 +242,9 @@ public abstract class RouterFunctions { * @return a router function that routes to resources * @since 6.1 */ - public static RouterFunction resources(Function> lookupFunction, BiConsumer headersConsumer) { + public static RouterFunction resources(Function> lookupFunction, + BiConsumer headersConsumer) { + return new ResourcesRouterFunction(lookupFunction, headersConsumer); } @@ -250,12 +254,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 the type of response returned by the handler function * @return the change router function */ - public static RouterFunction changeParser(RouterFunction routerFunction, - PathPatternParser parser) { + public static RouterFunction changeParser( + RouterFunction routerFunction, PathPatternParser parser) { Assert.notNull(routerFunction, "RouterFunction must not be null"); Assert.notNull(parser, "Parser must not be null"); @@ -1151,7 +1155,6 @@ public abstract class RouterFunctions { public void accept(Visitor visitor) { visitor.route(this.predicate, this.handlerFunction); } - } @@ -1173,13 +1176,10 @@ 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> result = - this.routerFunction.route(nestedRequest); + Optional> result = this.routerFunction.route(nestedRequest); if (result.isPresent() && nestedRequest != serverRequest) { // new attributes map from nestedRequest.attributes() can be composed of the old attributes, // which means that clearing the old attributes will remove those values from new attributes as well @@ -1202,7 +1202,6 @@ public abstract class RouterFunctions { this.routerFunction.accept(visitor); visitor.endNested(this.predicate); } - } @@ -1212,11 +1211,11 @@ public abstract class RouterFunctions { private final BiConsumer headersConsumer; - public ResourcesRouterFunction(Function> lookupFunction, BiConsumer headersConsumer) { - Assert.notNull(lookupFunction, "Function must not be null"); - Assert.notNull(headersConsumer, "HeadersConsumer must not be null"); + + Assert.notNull(lookupFunction, "Lookup function must not be null"); + Assert.notNull(headersConsumer, "Headers consumer must not be null"); this.lookupFunction = lookupFunction; this.headersConsumer = headersConsumer; } @@ -1284,5 +1283,4 @@ public abstract class RouterFunctions { } } - } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java index 3831e99dbac..ca7e06a3563 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java @@ -19,6 +19,7 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.lang.reflect.Method; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Set; import java.util.function.Consumer; @@ -28,10 +29,12 @@ 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; @@ -91,6 +94,8 @@ class MethodValidationTests { @BeforeEach void setup() throws Exception { + LocaleContextHolder.setDefaultLocale(Locale.UK); + LocalValidatorFactoryBean validatorBean = new LocalValidatorFactoryBean(); validatorBean.afterPropertiesSet(); this.jakartaValidator = new InvocationCountingValidator(validatorBean); @@ -120,6 +125,11 @@ class MethodValidationTests { return handlerAdapter; } + @AfterEach + void reset() { + LocaleContextHolder.setDefaultLocale(null); + } + @Test void modelAttribute() { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/EvalTagTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/EvalTagTests.java index 400f92abcfa..ad5608f043a 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/EvalTagTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/EvalTagTests.java @@ -22,14 +22,15 @@ 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; @@ -49,6 +50,8 @@ class EvalTagTests extends AbstractTagTests { @BeforeEach void setup() { + LocaleContextHolder.setDefaultLocale(Locale.UK); + context = createPageContext(); FormattingConversionServiceFactoryBean factory = new FormattingConversionServiceFactoryBean(); factory.afterPropertiesSet(); @@ -58,6 +61,11 @@ class EvalTagTests extends AbstractTagTests { tag.setPageContext(context); } + @AfterEach + void reset() { + LocaleContextHolder.setDefaultLocale(null); + } + @Test void printScopedAttributeResult() throws Exception { @@ -81,13 +89,12 @@ 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(formatter.print(new BigDecimal(".25"), Locale.getDefault())); + assertThat(((MockHttpServletResponse) context.getResponse()).getContentAsString()).isEqualTo("25%"); } @Test