From bce850aa12ee678a98a44b1074c7fc5a2f2f8e96 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Thu, 21 Oct 2021 05:57:00 +0000 Subject: [PATCH 01/37] Next development version (v5.3.13-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index aea3f16f967a..b46dbd487055 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=5.3.12-SNAPSHOT +version=5.3.13-SNAPSHOT org.gradle.jvmargs=-Xmx1536M org.gradle.caching=true org.gradle.parallel=true From b3eb1a2ad725ab4e8c9af7e1fe51840fa01289d0 Mon Sep 17 00:00:00 2001 From: no-brand Date: Thu, 21 Oct 2021 07:19:26 +0000 Subject: [PATCH 02/37] Improve example in Javadoc for HttpEntity Closes gh-27586 --- .../src/main/java/org/springframework/http/HttpEntity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/HttpEntity.java b/spring-web/src/main/java/org/springframework/http/HttpEntity.java index 7b7fe9af6002..d6493c4bed06 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpEntity.java +++ b/spring-web/src/main/java/org/springframework/http/HttpEntity.java @@ -28,7 +28,7 @@ *
  * HttpHeaders headers = new HttpHeaders();
  * headers.setContentType(MediaType.TEXT_PLAIN);
- * HttpEntity<String> entity = new HttpEntity<String>(helloWorld, headers);
+ * HttpEntity<String> entity = new HttpEntity<String>("helloWorld", headers);
  * URI location = template.postForLocation("https://example.com", entity);
  * 
* or From ec3f857bda2ffbba5d96787215cc99565d95c3cf Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Thu, 21 Oct 2021 12:40:10 +0200 Subject: [PATCH 03/37] Polish contribution See gh-27586 --- .../main/java/org/springframework/http/HttpEntity.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/HttpEntity.java b/spring-web/src/main/java/org/springframework/http/HttpEntity.java index d6493c4bed06..9b5be7c625ae 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpEntity.java +++ b/spring-web/src/main/java/org/springframework/http/HttpEntity.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,12 +23,12 @@ /** * Represents an HTTP request or response entity, consisting of headers and body. * - *

Typically used in combination with the {@link org.springframework.web.client.RestTemplate}, + *

Often used in combination with the {@link org.springframework.web.client.RestTemplate}, * like so: *

  * HttpHeaders headers = new HttpHeaders();
  * headers.setContentType(MediaType.TEXT_PLAIN);
- * HttpEntity<String> entity = new HttpEntity<String>("helloWorld", headers);
+ * HttpEntity<String> entity = new HttpEntity<>("Hello World", headers);
  * URI location = template.postForLocation("https://example.com", entity);
  * 
* or @@ -39,11 +39,11 @@ * * Can also be used in Spring MVC, as a return value from a @Controller method: *
- * @RequestMapping("/handle")
+ * @GetMapping("/handle")
  * public HttpEntity<String> handle() {
  *   HttpHeaders responseHeaders = new HttpHeaders();
  *   responseHeaders.set("MyResponseHeader", "MyValue");
- *   return new HttpEntity<String>("Hello World", responseHeaders);
+ *   return new HttpEntity<>("Hello World", responseHeaders);
  * }
  * 
* From 7a54ff2d21ad7e1900ce603ea1a814d8021bacb9 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Thu, 21 Oct 2021 14:07:17 +0200 Subject: [PATCH 04/37] Extract public TypeFilterUtils from ComponentScanAnnotationParser Prior to this commit, third parties using @ComponentScan's @Filter annotation had to implement their own parsing for @Filter AnnotationAttributes as well as instantiation of the corresponding TypeFilters. In such cases the various *Aware callbacks (BeanFactoryAware, EnvironmentAware, etc.) should also be supported. This commit therefore extracts a new public TypeFilterUtils class from ComponentScanAnnotationParser so that third parties can benefit from consistent TypeFilter creation from @ComponentScan @Filter annotations. Closes gh-27553 --- .../ComponentScanAnnotationParser.java | 65 ++-------- .../context/annotation/TypeFilterUtils.java | 119 ++++++++++++++++++ 2 files changed, 127 insertions(+), 57 deletions(-) create mode 100644 spring-context/src/main/java/org/springframework/context/annotation/TypeFilterUtils.java diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java index 90575862c6bd..27e879bfb338 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java @@ -16,13 +16,10 @@ package org.springframework.context.annotation; -import java.lang.annotation.Annotation; -import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; -import java.util.regex.Pattern; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.config.BeanDefinitionHolder; @@ -33,12 +30,7 @@ import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; import org.springframework.core.type.filter.AbstractTypeHierarchyTraversingFilter; -import org.springframework.core.type.filter.AnnotationTypeFilter; -import org.springframework.core.type.filter.AspectJTypeFilter; -import org.springframework.core.type.filter.AssignableTypeFilter; -import org.springframework.core.type.filter.RegexPatternTypeFilter; import org.springframework.core.type.filter.TypeFilter; -import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -93,13 +85,17 @@ public Set parse(AnnotationAttributes componentScan, final scanner.setResourcePattern(componentScan.getString("resourcePattern")); - for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) { - for (TypeFilter typeFilter : typeFiltersFor(filter)) { + for (AnnotationAttributes includeFilterAttributes : componentScan.getAnnotationArray("includeFilters")) { + List typeFilters = TypeFilterUtils.createTypeFiltersFor(includeFilterAttributes, this.environment, + this.resourceLoader, this.registry); + for (TypeFilter typeFilter : typeFilters) { scanner.addIncludeFilter(typeFilter); } } - for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) { - for (TypeFilter typeFilter : typeFiltersFor(filter)) { + for (AnnotationAttributes excludeFilterAttributes : componentScan.getAnnotationArray("excludeFilters")) { + List typeFilters = TypeFilterUtils.createTypeFiltersFor(excludeFilterAttributes, this.environment, + this.resourceLoader, this.registry); + for (TypeFilter typeFilter : typeFilters) { scanner.addExcludeFilter(typeFilter); } } @@ -132,49 +128,4 @@ protected boolean matchClassName(String className) { return scanner.doScan(StringUtils.toStringArray(basePackages)); } - private List typeFiltersFor(AnnotationAttributes filterAttributes) { - List typeFilters = new ArrayList<>(); - FilterType filterType = filterAttributes.getEnum("type"); - - for (Class filterClass : filterAttributes.getClassArray("classes")) { - switch (filterType) { - case ANNOTATION: - Assert.isAssignable(Annotation.class, filterClass, - "@ComponentScan ANNOTATION type filter requires an annotation type"); - @SuppressWarnings("unchecked") - Class annotationType = (Class) filterClass; - typeFilters.add(new AnnotationTypeFilter(annotationType)); - break; - case ASSIGNABLE_TYPE: - typeFilters.add(new AssignableTypeFilter(filterClass)); - break; - case CUSTOM: - Assert.isAssignable(TypeFilter.class, filterClass, - "@ComponentScan CUSTOM type filter requires a TypeFilter implementation"); - - TypeFilter filter = ParserStrategyUtils.instantiateClass(filterClass, TypeFilter.class, - this.environment, this.resourceLoader, this.registry); - typeFilters.add(filter); - break; - default: - throw new IllegalArgumentException("Filter type not supported with Class value: " + filterType); - } - } - - for (String expression : filterAttributes.getStringArray("pattern")) { - switch (filterType) { - case ASPECTJ: - typeFilters.add(new AspectJTypeFilter(expression, this.resourceLoader.getClassLoader())); - break; - case REGEX: - typeFilters.add(new RegexPatternTypeFilter(Pattern.compile(expression))); - break; - default: - throw new IllegalArgumentException("Filter type not supported with String pattern: " + filterType); - } - } - - return typeFilters; - } - } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/TypeFilterUtils.java b/spring-context/src/main/java/org/springframework/context/annotation/TypeFilterUtils.java new file mode 100644 index 000000000000..340c232f60f1 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/TypeFilterUtils.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2021 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.context.annotation; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.core.type.filter.AspectJTypeFilter; +import org.springframework.core.type.filter.AssignableTypeFilter; +import org.springframework.core.type.filter.RegexPatternTypeFilter; +import org.springframework.core.type.filter.TypeFilter; +import org.springframework.util.Assert; + +/** + * Collection of utilities for working with {@link ComponentScan @ComponentScan} + * {@linkplain ComponentScan.Filter type filters}. + * + * @author Chris Beams + * @author Juergen Hoeller + * @author Sam Brannen + * @since 5.3.13 + * @see ComponentScan.Filter + * @see org.springframework.core.type.filter.TypeFilter + */ +public abstract class TypeFilterUtils { + + /** + * Create {@linkplain TypeFilter type filters} from the supplied + * {@link AnnotationAttributes}, such as those sourced from + * {@link ComponentScan#includeFilters()} or {@link ComponentScan#excludeFilters()}. + *

Each {@link TypeFilter} will be instantiated using an appropriate + * constructor, with {@code BeanClassLoaderAware}, {@code BeanFactoryAware}, + * {@code EnvironmentAware}, and {@code ResourceLoaderAware} contracts + * invoked if they are implemented by the type filter. + * @param filterAttributes {@code AnnotationAttributes} for a + * {@link ComponentScan.Filter @Filter} declaration + * @param environment the {@code Environment} to make available to filters + * @param resourceLoader the {@code ResourceLoader} to make available to filters + * @param registry the {@code BeanDefinitionRegistry} to make available to filters + * as a {@link org.springframework.beans.factory.BeanFactory} if applicable + * @return a list of instantiated and configured type filters + * @see TypeFilter + * @see AnnotationTypeFilter + * @see AssignableTypeFilter + * @see AspectJTypeFilter + * @see RegexPatternTypeFilter + * @see org.springframework.beans.factory.BeanClassLoaderAware + * @see org.springframework.beans.factory.BeanFactoryAware + * @see org.springframework.context.EnvironmentAware + * @see org.springframework.context.ResourceLoaderAware + */ + public static List createTypeFiltersFor(AnnotationAttributes filterAttributes, Environment environment, + ResourceLoader resourceLoader, BeanDefinitionRegistry registry) { + + List typeFilters = new ArrayList<>(); + FilterType filterType = filterAttributes.getEnum("type"); + + for (Class filterClass : filterAttributes.getClassArray("classes")) { + switch (filterType) { + case ANNOTATION: + Assert.isAssignable(Annotation.class, filterClass, + "@ComponentScan ANNOTATION type filter requires an annotation type"); + @SuppressWarnings("unchecked") + Class annotationType = (Class) filterClass; + typeFilters.add(new AnnotationTypeFilter(annotationType)); + break; + case ASSIGNABLE_TYPE: + typeFilters.add(new AssignableTypeFilter(filterClass)); + break; + case CUSTOM: + Assert.isAssignable(TypeFilter.class, filterClass, + "@ComponentScan CUSTOM type filter requires a TypeFilter implementation"); + TypeFilter filter = ParserStrategyUtils.instantiateClass(filterClass, TypeFilter.class, + environment, resourceLoader, registry); + typeFilters.add(filter); + break; + default: + throw new IllegalArgumentException("Filter type not supported with Class value: " + filterType); + } + } + + for (String expression : filterAttributes.getStringArray("pattern")) { + switch (filterType) { + case ASPECTJ: + typeFilters.add(new AspectJTypeFilter(expression, resourceLoader.getClassLoader())); + break; + case REGEX: + typeFilters.add(new RegexPatternTypeFilter(Pattern.compile(expression))); + break; + default: + throw new IllegalArgumentException("Filter type not supported with String pattern: " + filterType); + } + } + + return typeFilters; + } + +} From 9af11ad5ce137e8fe71f0ce9e980745e4c840818 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Fri, 22 Oct 2021 11:08:33 +0200 Subject: [PATCH 05/37] Fix Javadoc formatting issues --- .../core/convert/TypeDescriptor.java | 14 +++++++------- .../springframework/expression/TypeConverter.java | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java index 1bc8cb281b1f..f6e25824e004 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java @@ -37,7 +37,7 @@ /** * Contextual descriptor about a type to convert from or to. - * Capable of representing arrays and generic collection types. + *

Capable of representing arrays and generic collection types. * * @author Keith Donald * @author Andy Clement @@ -345,9 +345,9 @@ public TypeDescriptor getElementTypeDescriptor() { * from the provided collection or array element. *

Narrows the {@link #getElementTypeDescriptor() elementType} property to the class * of the provided collection or array element. For example, if this describes a - * {@code java.util.List<java.lang.Number<} and the element argument is an + * {@code java.util.List} and the element argument is a * {@code java.lang.Integer}, the returned TypeDescriptor will be {@code java.lang.Integer}. - * If this describes a {@code java.util.List<?>} and the element argument is an + * If this describes a {@code java.util.List} and the element argument is a * {@code java.lang.Integer}, the returned TypeDescriptor will be {@code java.lang.Integer} * as well. *

Annotation and nested type context will be preserved in the narrowed @@ -388,9 +388,9 @@ public TypeDescriptor getMapKeyTypeDescriptor() { * from the provided map key. *

Narrows the {@link #getMapKeyTypeDescriptor() mapKeyType} property * to the class of the provided map key. For example, if this describes a - * {@code java.util.Map<java.lang.Number, java.lang.String<} and the key + * {@code java.util.Map} and the key * argument is a {@code java.lang.Integer}, the returned TypeDescriptor will be - * {@code java.lang.Integer}. If this describes a {@code java.util.Map<?, ?>} + * {@code java.lang.Integer}. If this describes a {@code java.util.Map} * and the key argument is a {@code java.lang.Integer}, the returned * TypeDescriptor will be {@code java.lang.Integer} as well. *

Annotation and nested type context will be preserved in the narrowed @@ -425,9 +425,9 @@ public TypeDescriptor getMapValueTypeDescriptor() { * from the provided map value. *

Narrows the {@link #getMapValueTypeDescriptor() mapValueType} property * to the class of the provided map value. For example, if this describes a - * {@code java.util.Map<java.lang.String, java.lang.Number<} and the value + * {@code java.util.Map} and the value * argument is a {@code java.lang.Integer}, the returned TypeDescriptor will be - * {@code java.lang.Integer}. If this describes a {@code java.util.Map<?, ?>} + * {@code java.lang.Integer}. If this describes a {@code java.util.Map} * and the value argument is a {@code java.lang.Integer}, the returned * TypeDescriptor will be {@code java.lang.Integer} as well. *

Annotation and nested type context will be preserved in the narrowed diff --git a/spring-expression/src/main/java/org/springframework/expression/TypeConverter.java b/spring-expression/src/main/java/org/springframework/expression/TypeConverter.java index f2f67c0965c8..6974d6b8747c 100644 --- a/spring-expression/src/main/java/org/springframework/expression/TypeConverter.java +++ b/spring-expression/src/main/java/org/springframework/expression/TypeConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2021 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. @@ -44,7 +44,7 @@ public interface TypeConverter { * Convert (or coerce) a value from one type to another, for example from a * {@code boolean} to a {@code String}. *

The {@link TypeDescriptor} parameters enable support for typed collections: - * A caller may prefer a {@code List<Integer>}, for example, rather than + * A caller may prefer a {@code List}, for example, rather than * simply any {@code List}. * @param value the value to be converted * @param sourceType a type descriptor that supplies extra information about the From 9b967775ede2d885746d734347a356abb8b3d1db Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Fri, 22 Oct 2021 10:45:34 +0200 Subject: [PATCH 06/37] Polish Spel's ReflectionHelper.setupArgumentsForVarargsInvocation() --- .../spel/support/ReflectionHelper.java | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java index d8af41170d67..65e3cff83a86 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java @@ -332,8 +332,8 @@ private static boolean isFirstEntryInArray(Object value, @Nullable Object possib } /** - * Package up the arguments so that they correctly match what is expected in parameterTypes. - * For example, if parameterTypes is {@code (int, String[])} because the second parameter + * Package up the arguments so that they correctly match what is expected in requiredParameterTypes. + *

For example, if requiredParameterTypes is {@code (int, String[])} because the second parameter * was declared {@code String...}, then if arguments is {@code [1,"a","b"]} then it must be * repackaged as {@code [1,new String[]{"a","b"}]} in order to match the expected types. * @param requiredParameterTypes the types of the parameters for the invocation @@ -350,23 +350,24 @@ public static Object[] setupArgumentsForVarargsInvocation(Class[] requiredPar requiredParameterTypes[parameterCount - 1] != (args[argumentCount - 1] != null ? args[argumentCount - 1].getClass() : null)) { - int arraySize = 0; // zero size array if nothing to pass as the varargs parameter - if (argumentCount >= parameterCount) { - arraySize = argumentCount - (parameterCount - 1); - } - - // Create an array for the varargs arguments + // Create an array for the leading arguments plus the varargs array argument. Object[] newArgs = new Object[parameterCount]; + // Copy all leading arguments to the new array, omitting the varargs array argument. System.arraycopy(args, 0, newArgs, 0, newArgs.length - 1); // Now sort out the final argument, which is the varargs one. Before entering this method, // the arguments should have been converted to the box form of the required type. + int varargsArraySize = 0; // zero size array if nothing to pass as the varargs parameter + if (argumentCount >= parameterCount) { + varargsArraySize = argumentCount - (parameterCount - 1); + } Class componentType = requiredParameterTypes[parameterCount - 1].getComponentType(); - Object repackagedArgs = Array.newInstance(componentType, arraySize); - for (int i = 0; i < arraySize; i++) { - Array.set(repackagedArgs, i, args[parameterCount - 1 + i]); + Object varargsArray = Array.newInstance(componentType, varargsArraySize); + for (int i = 0; i < varargsArraySize; i++) { + Array.set(varargsArray, i, args[parameterCount - 1 + i]); } - newArgs[newArgs.length - 1] = repackagedArgs; + // Finally, add the varargs array to the new arguments array. + newArgs[newArgs.length - 1] = varargsArray; return newArgs; } return args; From bc657eb4d5babccf5065ef26fca8c9723675a083 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Thu, 21 Oct 2021 14:50:34 +0200 Subject: [PATCH 07/37] Fix SpEL vararg method invocation for strings containing commas Prior to this commit, if a SpEL expression invoked a method or registered function that declares a String varargs argument, there were sometimes issues with converting the input arguments into the varargs array argument. Specifically, if the expression supplied a single String argument containing a comma for the varargs (such as "a,b"), SpEL's ReflectionHelper.convertArguments() method incorrectly converted that single String to an array via the ConversionService, which indirectly converted that String using the StringToArrayConverter, which converts a comma-delimited String to an array. Thus, "a,b" effectively got converted to a two-dimensional array ["a", "b"] instead of simply ["a,b"]. This commit fixes this bug by avoiding use of the TypeConverter and ConversionService for single arguments supplied as varargs when the single argument's type matches the varargs array component type. Closes gh-27582 --- .../spel/support/ReflectionHelper.java | 28 +++++++----- .../spel/MethodInvocationTests.java | 43 ++++++++++++++++--- .../spel/testresources/Inventor.java | 11 ++++- 3 files changed, 63 insertions(+), 19 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java index 65e3cff83a86..821c0b246ae6 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,6 +38,7 @@ * * @author Andy Clement * @author Juergen Hoeller + * @author Sam Brannen * @since 3.0 */ public abstract class ReflectionHelper { @@ -281,25 +282,32 @@ static boolean convertArguments(TypeConverter converter, Object[] arguments, Exe arguments[i] = converter.convertValue(argument, TypeDescriptor.forObject(argument), targetType); conversionOccurred |= (argument != arguments[i]); } + MethodParameter methodParam = MethodParameter.forExecutable(executable, varargsPosition); + + // If the target is varargs and there is just one more argument, then convert it here. if (varargsPosition == arguments.length - 1) { - // If the target is varargs and there is just one more argument - // then convert it here - TypeDescriptor targetType = new TypeDescriptor(methodParam); Object argument = arguments[varargsPosition]; + TypeDescriptor targetType = new TypeDescriptor(methodParam); TypeDescriptor sourceType = TypeDescriptor.forObject(argument); - arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetType); - // Three outcomes of that previous line: - // 1) the input argument was already compatible (ie. array of valid type) and nothing was done - // 2) the input argument was correct type but not in an array so it was made into an array - // 3) the input argument was the wrong type and got converted and put into an array + // If the argument type is equal to the varargs element type, there is no need + // to convert it or wrap it in an array. For example, using StringToArrayConverter + // to convert a String containing a comma would result in the String being split + // and repackaged in an array when it should be used as-is. + if (!sourceType.equals(targetType.getElementTypeDescriptor())) { + arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetType); + } + // Three outcomes of the above if-block: + // 1) the input argument was correct type but not wrapped in an array, and nothing was done. + // 2) the input argument was already compatible (i.e., array of valid type), and nothing was done. + // 3) the input argument was the wrong type and got converted and wrapped in an array. if (argument != arguments[varargsPosition] && !isFirstEntryInArray(argument, arguments[varargsPosition])) { conversionOccurred = true; // case 3 } } + // Otherwise, convert remaining arguments to the varargs element type. else { - // Convert remaining arguments to the varargs element type TypeDescriptor targetType = new TypeDescriptor(methodParam).getElementTypeDescriptor(); Assert.state(targetType != null, "No element type"); for (int i = varargsPosition; i < arguments.length; i++) { diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/MethodInvocationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/MethodInvocationTests.java index 0a025acf723f..b2cde1f10ff3 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/MethodInvocationTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/MethodInvocationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,6 +46,7 @@ * * @author Andy Clement * @author Phillip Webb + * @author Sam Brannen */ public class MethodInvocationTests extends AbstractExpressionTests { @@ -233,26 +234,54 @@ public void testAddingMethodResolvers() { @Test public void testVarargsInvocation01() { - // Calling 'public int aVarargsMethod(String... strings)' - //evaluate("aVarargsMethod('a','b','c')", 3, Integer.class); - //evaluate("aVarargsMethod('a')", 1, Integer.class); + // Calling 'public int aVarargsMethod(String... strings)' - returns number of arguments + evaluate("aVarargsMethod('a','b','c')", 3, Integer.class); + evaluate("aVarargsMethod('a')", 1, Integer.class); evaluate("aVarargsMethod()", 0, Integer.class); evaluate("aVarargsMethod(1,2,3)", 3, Integer.class); // all need converting to strings evaluate("aVarargsMethod(1)", 1, Integer.class); // needs string conversion evaluate("aVarargsMethod(1,'a',3.0d)", 3, Integer.class); // first and last need conversion - // evaluate("aVarargsMethod(new String[]{'a','b','c'})", 3, Integer.class); + evaluate("aVarargsMethod(new String[]{'a','b','c'})", 3, Integer.class); } @Test public void testVarargsInvocation02() { - // Calling 'public int aVarargsMethod2(int i, String... strings)' - returns int+length_of_strings + // Calling 'public int aVarargsMethod2(int i, String... strings)' - returns int + length_of_strings evaluate("aVarargsMethod2(5,'a','b','c')", 8, Integer.class); evaluate("aVarargsMethod2(2,'a')", 3, Integer.class); evaluate("aVarargsMethod2(4)", 4, Integer.class); evaluate("aVarargsMethod2(8,2,3)", 10, Integer.class); evaluate("aVarargsMethod2(9)", 9, Integer.class); evaluate("aVarargsMethod2(2,'a',3.0d)", 4, Integer.class); - // evaluate("aVarargsMethod2(8,new String[]{'a','b','c'})", 11, Integer.class); + evaluate("aVarargsMethod2(8,new String[]{'a','b','c'})", 11, Integer.class); + } + + @Test + public void testVarargsInvocation03() { + // Calling 'public int aVarargsMethod3(String str1, String... strings)' - returns all strings concatenated with "-" + + // No conversion necessary + evaluate("aVarargsMethod3('x')", "x", String.class); + evaluate("aVarargsMethod3('x', 'a')", "x-a", String.class); + evaluate("aVarargsMethod3('x', 'a', 'b', 'c')", "x-a-b-c", String.class); + + // Conversion necessary + evaluate("aVarargsMethod3(9)", "9", String.class); + evaluate("aVarargsMethod3(8,2,3)", "8-2-3", String.class); + evaluate("aVarargsMethod3('2','a',3.0d)", "2-a-3.0", String.class); + evaluate("aVarargsMethod3('8',new String[]{'a','b','c'})", "8-a-b-c", String.class); + + // Individual string contains a comma with multiple varargs arguments + evaluate("aVarargsMethod3('foo', ',', 'baz')", "foo-,-baz", String.class); + evaluate("aVarargsMethod3('foo', 'bar', ',baz')", "foo-bar-,baz", String.class); + evaluate("aVarargsMethod3('foo', 'bar,', 'baz')", "foo-bar,-baz", String.class); + + // Individual string contains a comma with single varargs argument. + // Reproduces https://github.com/spring-projects/spring-framework/issues/27582 + evaluate("aVarargsMethod3('foo', ',')", "foo-,", String.class); + evaluate("aVarargsMethod3('foo', ',bar')", "foo-,bar", String.class); + evaluate("aVarargsMethod3('foo', 'bar,')", "foo-bar,", String.class); + evaluate("aVarargsMethod3('foo', 'bar,baz')", "foo-bar,baz", String.class); } @Test diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Inventor.java b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Inventor.java index bdf6d79c1ddc..34960d982fe3 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Inventor.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Inventor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ ///CLOVER:OFF @SuppressWarnings("unused") public class Inventor { + private String name; public String _name; public String _name_; @@ -202,8 +203,14 @@ public int aVarargsMethod2(int i, String... strings) { return strings.length + i; } - public Inventor(String... strings) { + public String aVarargsMethod3(String str1, String... strings) { + if (ObjectUtils.isEmpty(strings)) { + return str1; + } + return str1 + "-" + String.join("-", strings); + } + public Inventor(String... strings) { } public boolean getSomeProperty() { From b728b4640b70c293c18ca1290ee303dee1f0d413 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Fri, 22 Oct 2021 13:46:11 +0200 Subject: [PATCH 08/37] Add explicit tests for SpEL functions for fix for gh-27582 --- .../expression/spel/VariableAndFunctionTests.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/VariableAndFunctionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/VariableAndFunctionTests.java index 0333e20ff961..3271e45ea6d1 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/VariableAndFunctionTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/VariableAndFunctionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ * Tests the evaluation of expressions that access variables and functions (lambda/java). * * @author Andy Clement + * @author Sam Brannen */ public class VariableAndFunctionTests extends AbstractExpressionTests { @@ -58,12 +59,17 @@ public void testFunctionAccess02() { @Test public void testCallVarargsFunction() { + evaluate("#varargsFunctionReverseStringsAndMerge('a,b')", "a,b", String.class); + evaluate("#varargsFunctionReverseStringsAndMerge('a', 'b,c', 'd')", "db,ca", String.class); evaluate("#varargsFunctionReverseStringsAndMerge('a','b','c')", "cba", String.class); evaluate("#varargsFunctionReverseStringsAndMerge('a')", "a", String.class); evaluate("#varargsFunctionReverseStringsAndMerge()", "", String.class); evaluate("#varargsFunctionReverseStringsAndMerge('b',25)", "25b", String.class); evaluate("#varargsFunctionReverseStringsAndMerge(25)", "25", String.class); + + evaluate("#varargsFunctionReverseStringsAndMerge2(1, 'a,b')", "1a,b", String.class); evaluate("#varargsFunctionReverseStringsAndMerge2(1,'a','b','c')", "1cba", String.class); + evaluate("#varargsFunctionReverseStringsAndMerge2(1, 'a', 'b,c', 'd')", "1db,ca", String.class); evaluate("#varargsFunctionReverseStringsAndMerge2(2,'a')", "2a", String.class); evaluate("#varargsFunctionReverseStringsAndMerge2(3)", "3", String.class); evaluate("#varargsFunctionReverseStringsAndMerge2(4,'b',25)", "425b", String.class); From e5475d698a8a861a03a18c24de16e3e4f0d22f72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=A6=D1=8B=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2?= Date: Fri, 22 Oct 2021 14:38:13 +0300 Subject: [PATCH 09/37] Iterate over Map's entrySet() instead of keySet() in PropertyEditorRegistrySupport Closes gh-27591 --- .../beans/PropertyEditorRegistrySupport.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrySupport.java b/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrySupport.java index d1354e1d89b0..e17a4e52e697 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrySupport.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrySupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -422,10 +422,13 @@ private PropertyEditor getCustomEditor(@Nullable Class requiredType) { } if (editor == null) { // Find editor for superclass or interface. - for (Iterator> it = this.customEditors.keySet().iterator(); it.hasNext() && editor == null;) { - Class key = it.next(); + for (Map.Entry, PropertyEditor> entry : this.customEditors.entrySet()) { + if (editor != null) { + break; + } + Class key = entry.getKey(); if (key.isAssignableFrom(requiredType)) { - editor = this.customEditors.get(key); + editor = entry.getValue(); // Cache editor for search type, to avoid the overhead // of repeated assignable-from checks. if (this.customEditorCache == null) { From a603779f33d47cd9ac87919ccc26f8c2d279fa40 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Fri, 22 Oct 2021 14:57:12 +0200 Subject: [PATCH 10/37] Return previous value in UndertowHeadersAdapter's remove() method Prior to this commit, UndertowHeadersAdapter's remove() method violated the java.util.Map contract by always returning null. This commit fixes this by returning the previous list stored under the specified key, and otherwise returning null if no previous value was present. Closes gh-27592 --- .../http/server/reactive/UndertowHeadersAdapter.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowHeadersAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowHeadersAdapter.java index fa197aa0c208..8b7c6aed14dd 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowHeadersAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowHeadersAdapter.java @@ -17,6 +17,7 @@ package org.springframework.http.server.reactive; import java.util.AbstractSet; +import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; @@ -36,6 +37,7 @@ * {@code MultiValueMap} implementation for wrapping Undertow HTTP headers. * * @author Brian Clozel + * @author Sam Brannen * @since 5.1.1 */ class UndertowHeadersAdapter implements MultiValueMap { @@ -131,7 +133,10 @@ public List put(String key, List value) { @Nullable public List remove(Object key) { if (key instanceof String) { - this.headers.remove((String) key); + Collection removed = this.headers.remove((String) key); + if (removed != null) { + return new ArrayList<>(removed); + } } return null; } From 66826ac960be20aa0df25f22f1a47612c5b91845 Mon Sep 17 00:00:00 2001 From: xxpain Date: Fri, 22 Oct 2021 15:47:13 +0800 Subject: [PATCH 11/37] Fix typo in Javadoc in HeaderAssertions Closes gh-27589 --- .../test/web/reactive/server/HeaderAssertions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java index dfd8ce89c7e4..aa20066960d0 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java @@ -115,7 +115,7 @@ public WebTestClient.ResponseSpec valueMatches(String name, String pattern) { /** * Match all values of the response header with the given regex * patterns which are applied to the values of the header in the - * same order. Note that the number of pattenrs must match the + * same order. Note that the number of patterns must match the * number of actual values. * @param name the header name * @param patterns one or more regex patterns, one per expected value From c58853f5e5891147c43487fac3a249ba5c98de0c Mon Sep 17 00:00:00 2001 From: Daniil Pozdeev Date: Thu, 14 Oct 2021 17:20:41 +0300 Subject: [PATCH 12/37] Use LocalDataSourceJobStore only if one is not set via Quartz properties Prior to this commit, Spring's SchedulerFactoryBean always set the "org.quartz.jobStore.class" property to LocalDataSourceJobStore even if the user had already specified a custom JobStore implementation via the Quartz properties file or Properties object, thereby effectively ignoring the user configuration. This commit addresses this by configuring Quartz to use Spring's LocalDataSourceJobStore only if a JobStore has not already been specified via user configuration. Closes gh-27560 --- .../quartz/SchedulerFactoryBean.java | 2 +- .../scheduling/quartz/QuartzSupportTests.java | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java index 50ef456cfc9a..cb4c0d2a3adb 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java @@ -570,7 +570,7 @@ private void initSchedulerFactory(StdSchedulerFactory schedulerFactory) throws S CollectionUtils.mergePropertiesIntoMap(this.quartzProperties, mergedProps); if (this.dataSource != null) { - mergedProps.setProperty(StdSchedulerFactory.PROP_JOB_STORE_CLASS, LocalDataSourceJobStore.class.getName()); + mergedProps.putIfAbsent(StdSchedulerFactory.PROP_JOB_STORE_CLASS, LocalDataSourceJobStore.class.getName()); } // Determine scheduler name across local settings and Quartz properties... diff --git a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java index 6c7d5b8691c7..a300ce010aeb 100644 --- a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java +++ b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java @@ -18,6 +18,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Properties; import javax.sql.DataSource; @@ -30,6 +31,7 @@ import org.quartz.SchedulerFactory; import org.quartz.impl.JobDetailImpl; import org.quartz.impl.SchedulerRepository; +import org.quartz.impl.jdbcjobstore.JobStoreTX; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; @@ -40,6 +42,8 @@ import org.springframework.core.task.TaskExecutor; import org.springframework.core.testfixture.EnabledForTestGroups; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -396,6 +400,29 @@ public void schedulerWithHsqlDataSource() throws Exception { } } + @Test + public void schedulerFactoryBeanWithCustomJobStore() throws Exception { + StaticApplicationContext context = new StaticApplicationContext(); + + final String dbName = "mydb"; + final EmbeddedDatabase database = new EmbeddedDatabaseBuilder().setName(dbName).build(); + + final Properties properties = new Properties(); + properties.setProperty("org.quartz.jobStore.class", JobStoreTX.class.getName()); + properties.setProperty("org.quartz.jobStore.dataSource", dbName); + + BeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(SchedulerFactoryBean.class) + .addPropertyValue("autoStartup", false) + .addPropertyValue("dataSource", database) + .addPropertyValue("quartzProperties", properties) + .getBeanDefinition(); + context.registerBeanDefinition("scheduler", beanDefinition); + + Scheduler bean = context.getBean("scheduler", Scheduler.class); + + assertThat(bean.getMetaData().getJobStoreClass()).isEqualTo(JobStoreTX.class); + } + private ClassPathXmlApplicationContext context(String path) { return new ClassPathXmlApplicationContext(path, getClass()); } From 2c89ff934ddd5b7efa14f715c4ec6a67bab7fd69 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sat, 23 Oct 2021 15:59:46 +0200 Subject: [PATCH 13/37] Update copyright date See gh-27560 --- .../springframework/scheduling/quartz/SchedulerFactoryBean.java | 2 +- .../springframework/scheduling/quartz/QuartzSupportTests.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java index cb4c0d2a3adb..e0982a2e5ff9 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2021 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. diff --git a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java index a300ce010aeb..71cd93810e52 100644 --- a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java +++ b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. From 995aaa6159957222b189f352672e20e3626ac442 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sat, 23 Oct 2021 16:00:26 +0200 Subject: [PATCH 14/37] Polishing --- .../scheduling/quartz/QuartzSupportTests.java | 64 ++++++++++--------- ...ipleAnonymousMethodInvokingJobDetailFB.xml | 4 +- .../quartz/schedulerAccessorBean.xml | 4 +- .../client/DefaultWebClientTests.java | 3 +- 4 files changed, 41 insertions(+), 34 deletions(-) diff --git a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java index 71cd93810e52..9d461d2e400f 100644 --- a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java +++ b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java @@ -61,10 +61,10 @@ * @author Sam Brannen * @since 20.02.2004 */ -public class QuartzSupportTests { +class QuartzSupportTests { @Test - public void schedulerFactoryBeanWithApplicationContext() throws Exception { + void schedulerFactoryBeanWithApplicationContext() throws Exception { TestBean tb = new TestBean("tb", 99); StaticApplicationContext ac = new StaticApplicationContext(); @@ -101,7 +101,7 @@ protected Scheduler createScheduler(SchedulerFactory schedulerFactory, String sc @Test @EnabledForTestGroups(LONG_RUNNING) - public void schedulerWithTaskExecutor() throws Exception { + void schedulerWithTaskExecutor() throws Exception { CountingTaskExecutor taskExecutor = new CountingTaskExecutor(); DummyJob.count = 0; @@ -134,7 +134,7 @@ public void schedulerWithTaskExecutor() throws Exception { @Test @SuppressWarnings({ "unchecked", "rawtypes" }) - public void jobDetailWithRunnableInsteadOfJob() { + void jobDetailWithRunnableInsteadOfJob() { JobDetailImpl jobDetail = new JobDetailImpl(); assertThatIllegalArgumentException().isThrownBy(() -> jobDetail.setJobClass((Class) DummyRunnable.class)); @@ -142,7 +142,7 @@ public void jobDetailWithRunnableInsteadOfJob() { @Test @EnabledForTestGroups(LONG_RUNNING) - public void schedulerWithQuartzJobBean() throws Exception { + void schedulerWithQuartzJobBean() throws Exception { DummyJob.param = 0; DummyJob.count = 0; @@ -175,7 +175,7 @@ public void schedulerWithQuartzJobBean() throws Exception { @Test @EnabledForTestGroups(LONG_RUNNING) - public void schedulerWithSpringBeanJobFactory() throws Exception { + void schedulerWithSpringBeanJobFactory() throws Exception { DummyJob.param = 0; DummyJob.count = 0; @@ -210,7 +210,7 @@ public void schedulerWithSpringBeanJobFactory() throws Exception { @Test @EnabledForTestGroups(LONG_RUNNING) - public void schedulerWithSpringBeanJobFactoryAndParamMismatchNotIgnored() throws Exception { + void schedulerWithSpringBeanJobFactoryAndParamMismatchNotIgnored() throws Exception { DummyJob.param = 0; DummyJob.count = 0; @@ -246,7 +246,7 @@ public void schedulerWithSpringBeanJobFactoryAndParamMismatchNotIgnored() throws @Test @EnabledForTestGroups(LONG_RUNNING) - public void schedulerWithSpringBeanJobFactoryAndQuartzJobBean() throws Exception { + void schedulerWithSpringBeanJobFactoryAndQuartzJobBean() throws Exception { DummyJobBean.param = 0; DummyJobBean.count = 0; @@ -280,7 +280,7 @@ public void schedulerWithSpringBeanJobFactoryAndQuartzJobBean() throws Exception @Test @EnabledForTestGroups(LONG_RUNNING) - public void schedulerWithSpringBeanJobFactoryAndJobSchedulingData() throws Exception { + void schedulerWithSpringBeanJobFactoryAndJobSchedulingData() throws Exception { DummyJob.param = 0; DummyJob.count = 0; @@ -298,7 +298,7 @@ public void schedulerWithSpringBeanJobFactoryAndJobSchedulingData() throws Excep } @Test // SPR-772 - public void multipleSchedulers() throws Exception { + void multipleSchedulers() throws Exception { try (ClassPathXmlApplicationContext ctx = context("multipleSchedulers.xml")) { Scheduler scheduler1 = (Scheduler) ctx.getBean("scheduler1"); Scheduler scheduler2 = (Scheduler) ctx.getBean("scheduler2"); @@ -309,7 +309,7 @@ public void multipleSchedulers() throws Exception { } @Test // SPR-16884 - public void multipleSchedulersWithQuartzProperties() throws Exception { + void multipleSchedulersWithQuartzProperties() throws Exception { try (ClassPathXmlApplicationContext ctx = context("multipleSchedulersWithQuartzProperties.xml")) { Scheduler scheduler1 = (Scheduler) ctx.getBean("scheduler1"); Scheduler scheduler2 = (Scheduler) ctx.getBean("scheduler2"); @@ -321,12 +321,13 @@ public void multipleSchedulersWithQuartzProperties() throws Exception { @Test @EnabledForTestGroups(LONG_RUNNING) - public void twoAnonymousMethodInvokingJobDetailFactoryBeans() throws Exception { - Thread.sleep(3000); + void twoAnonymousMethodInvokingJobDetailFactoryBeans() throws Exception { try (ClassPathXmlApplicationContext ctx = context("multipleAnonymousMethodInvokingJobDetailFB.xml")) { QuartzTestBean exportService = (QuartzTestBean) ctx.getBean("exportService"); QuartzTestBean importService = (QuartzTestBean) ctx.getBean("importService"); + Thread.sleep(400); + assertThat(exportService.getImportCount()).as("doImport called exportService").isEqualTo(0); assertThat(exportService.getExportCount()).as("doExport not called on exportService").isEqualTo(2); assertThat(importService.getImportCount()).as("doImport not called on importService").isEqualTo(2); @@ -336,12 +337,13 @@ public void twoAnonymousMethodInvokingJobDetailFactoryBeans() throws Exception { @Test @EnabledForTestGroups(LONG_RUNNING) - public void schedulerAccessorBean() throws Exception { - Thread.sleep(3000); + void schedulerAccessorBean() throws Exception { try (ClassPathXmlApplicationContext ctx = context("schedulerAccessorBean.xml")) { QuartzTestBean exportService = (QuartzTestBean) ctx.getBean("exportService"); QuartzTestBean importService = (QuartzTestBean) ctx.getBean("importService"); + Thread.sleep(400); + assertThat(exportService.getImportCount()).as("doImport called exportService").isEqualTo(0); assertThat(exportService.getExportCount()).as("doExport not called on exportService").isEqualTo(2); assertThat(importService.getImportCount()).as("doImport not called on importService").isEqualTo(2); @@ -351,7 +353,7 @@ public void schedulerAccessorBean() throws Exception { @Test @SuppressWarnings("resource") - public void schedulerAutoStartsOnContextRefreshedEventByDefault() throws Exception { + void schedulerAutoStartsOnContextRefreshedEventByDefault() throws Exception { StaticApplicationContext context = new StaticApplicationContext(); context.registerBeanDefinition("scheduler", new RootBeanDefinition(SchedulerFactoryBean.class)); Scheduler bean = context.getBean("scheduler", Scheduler.class); @@ -362,7 +364,7 @@ public void schedulerAutoStartsOnContextRefreshedEventByDefault() throws Excepti @Test @SuppressWarnings("resource") - public void schedulerAutoStartupFalse() throws Exception { + void schedulerAutoStartupFalse() throws Exception { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(SchedulerFactoryBean.class) .addPropertyValue("autoStartup", false).getBeanDefinition(); @@ -374,7 +376,7 @@ public void schedulerAutoStartupFalse() throws Exception { } @Test - public void schedulerRepositoryExposure() throws Exception { + void schedulerRepositoryExposure() throws Exception { try (ClassPathXmlApplicationContext ctx = context("schedulerRepositoryExposure.xml")) { assertThat(ctx.getBean("scheduler")).isSameAs(SchedulerRepository.getInstance().lookup("myScheduler")); } @@ -385,7 +387,7 @@ public void schedulerRepositoryExposure() throws Exception { * TODO: Against Quartz 2.2, this test's job doesn't actually execute anymore... */ @Test - public void schedulerWithHsqlDataSource() throws Exception { + void schedulerWithHsqlDataSource() throws Exception { DummyJob.param = 0; DummyJob.count = 0; @@ -401,13 +403,14 @@ public void schedulerWithHsqlDataSource() throws Exception { } @Test - public void schedulerFactoryBeanWithCustomJobStore() throws Exception { + @SuppressWarnings("resource") + void schedulerFactoryBeanWithCustomJobStore() throws Exception { StaticApplicationContext context = new StaticApplicationContext(); - final String dbName = "mydb"; - final EmbeddedDatabase database = new EmbeddedDatabaseBuilder().setName(dbName).build(); + String dbName = "mydb"; + EmbeddedDatabase database = new EmbeddedDatabaseBuilder().setName(dbName).build(); - final Properties properties = new Properties(); + Properties properties = new Properties(); properties.setProperty("org.quartz.jobStore.class", JobStoreTX.class.getName()); properties.setProperty("org.quartz.jobStore.dataSource", dbName); @@ -418,9 +421,9 @@ public void schedulerFactoryBeanWithCustomJobStore() throws Exception { .getBeanDefinition(); context.registerBeanDefinition("scheduler", beanDefinition); - Scheduler bean = context.getBean("scheduler", Scheduler.class); + Scheduler scheduler = context.getBean(Scheduler.class); - assertThat(bean.getMetaData().getJobStoreClass()).isEqualTo(JobStoreTX.class); + assertThat(scheduler.getMetaData().getJobStoreClass()).isEqualTo(JobStoreTX.class); } private ClassPathXmlApplicationContext context(String path) { @@ -428,7 +431,7 @@ private ClassPathXmlApplicationContext context(String path) { } - public static class CountingTaskExecutor implements TaskExecutor { + private static class CountingTaskExecutor implements TaskExecutor { private int count; @@ -440,12 +443,14 @@ public void execute(Runnable task) { } - public static class DummyJob implements Job { + private static class DummyJob implements Job { private static int param; private static int count; + @SuppressWarnings("unused") + // Must be public public void setParam(int value) { if (param > 0) { throw new IllegalStateException("Param already set"); @@ -460,12 +465,13 @@ public synchronized void execute(JobExecutionContext jobExecutionContext) throws } - public static class DummyJobBean extends QuartzJobBean { + private static class DummyJobBean extends QuartzJobBean { private static int param; private static int count; + @SuppressWarnings("unused") public void setParam(int value) { if (param > 0) { throw new IllegalStateException("Param already set"); @@ -480,7 +486,7 @@ protected synchronized void executeInternal(JobExecutionContext jobExecutionCont } - public static class DummyRunnable implements Runnable { + private static class DummyRunnable implements Runnable { @Override public void run() { diff --git a/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/multipleAnonymousMethodInvokingJobDetailFB.xml b/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/multipleAnonymousMethodInvokingJobDetailFB.xml index e45ccd195feb..58e771f3ad4a 100644 --- a/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/multipleAnonymousMethodInvokingJobDetailFB.xml +++ b/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/multipleAnonymousMethodInvokingJobDetailFB.xml @@ -19,7 +19,7 @@ - + @@ -30,7 +30,7 @@ - + diff --git a/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/schedulerAccessorBean.xml b/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/schedulerAccessorBean.xml index 0ba9c4a57a84..3634002664cc 100644 --- a/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/schedulerAccessorBean.xml +++ b/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/schedulerAccessorBean.xml @@ -21,7 +21,7 @@ - + @@ -32,7 +32,7 @@ - + diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java index 37928d2fb1b7..a33a24c01647 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -133,6 +133,7 @@ public void requestHeaderAndCookie() { } @Test + @SuppressWarnings("deprecation") public void contextFromThreadLocal() { WebClient client = this.builder .filter((request, next) -> From 4ec8ae42f48da6415a3be29507c9e327dec94538 Mon Sep 17 00:00:00 2001 From: Kenzo Spaulding Date: Wed, 26 May 2021 22:33:51 -0700 Subject: [PATCH 15/37] Add test cases to SpEL's MapAccessorTests Closes gh-26981 --- .../context/expression/MapAccessorTests.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/spring-context/src/test/java/org/springframework/context/expression/MapAccessorTests.java b/spring-context/src/test/java/org/springframework/context/expression/MapAccessorTests.java index df73adaa36d1..8fd4d2fb9a99 100644 --- a/spring-context/src/test/java/org/springframework/context/expression/MapAccessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/expression/MapAccessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 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. @@ -67,6 +67,17 @@ public void mapAccessorCompilable() { assertThat(ex.getValue(sec,mapGetter)).isEqualTo("bar"); assertThat(SpelCompiler.compile(ex)).isTrue(); assertThat(ex.getValue(sec,mapGetter)).isEqualTo("bar"); + + // basic isWritable + ex = sep.parseExpression("foo"); + assertThat(ex.isWritable(sec,testMap)).isTrue(); + + // basic write + ex = sep.parseExpression("foo2"); + ex.setValue(sec, testMap, "bar2"); + assertThat(ex.getValue(sec,testMap)).isEqualTo("bar2"); + assertThat(SpelCompiler.compile(ex)).isTrue(); + assertThat(ex.getValue(sec,testMap)).isEqualTo("bar2"); } public static class MapGetter { From 0268e762980f302e251a92743dd6724403660399 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=A6=D1=8B=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2?= Date: Thu, 23 Sep 2021 14:48:04 +0300 Subject: [PATCH 16/37] Improve mapping function in ExtendedEntityManagerCreator.createProxy() Closes gh-27456 --- .../orm/jpa/ExtendedEntityManagerCreator.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/ExtendedEntityManagerCreator.java b/spring-orm/src/main/java/org/springframework/orm/jpa/ExtendedEntityManagerCreator.java index 8e9173a94bfd..55431d805f3e 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/ExtendedEntityManagerCreator.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/ExtendedEntityManagerCreator.java @@ -230,10 +230,10 @@ private static EntityManager createProxy( if (emIfc != null) { interfaces = cachedEntityManagerInterfaces.computeIfAbsent(emIfc, key -> { - Set> ifcs = new LinkedHashSet<>(4); - ifcs.add(key); - ifcs.add(EntityManagerProxy.class); - return ClassUtils.toClassArray(ifcs); + if (EntityManagerProxy.class.equals(key)) { + return new Class[] {key}; + } + return new Class[] {key, EntityManagerProxy.class}; }); } else { From c4c3d59d07bfe7775f0df88f40babebd27975ca2 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 25 Oct 2021 17:02:58 +0200 Subject: [PATCH 17/37] Remove unused code in FormattingConversionServiceTests --- .../FormattingConversionServiceTests.java | 24 +------------------ 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceTests.java b/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceTests.java index eeb6895eddea..8aff305f22fa 100644 --- a/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceTests.java +++ b/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -321,28 +321,6 @@ public Date convert(MyDate source) { TypeDescriptor.valueOf(String.class)); } - @Test - public void registerDefaultValueViaFormatter() { - registerDefaultValue(Date.class, new Date()); - } - - private void registerDefaultValue(Class clazz, final T defaultValue) { - formattingService.addFormatterForFieldType(clazz, new Formatter() { - @Override - public T parse(String text, Locale locale) { - return defaultValue; - } - @Override - public String print(T t, Locale locale) { - return defaultValue.toString(); - } - @Override - public String toString() { - return defaultValue.toString(); - } - }); - } - @Test public void introspectedFormatter() { formattingService.addFormatter(new NumberStyleFormatter("#,#00.0#")); From 0416168d0edfaa8f321e318bd4129df7b425ee79 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 27 Oct 2021 15:07:15 +0200 Subject: [PATCH 18/37] Fix bug in max header calculation in DefaultPartHttpMessageReader This commit fixes a bug in the DefaultPartHttpMessageReader, in the check for exceeding the maximum header size. Before this commit, the entire buffer size was considered, thus triggering an exception even though the max header limit was not exceeded. After this commit, we only consider the size up until the end-of-header mark (CRLFCRLF). Furthermore, this commit increases the default maximum header size to 10k, the same default as Commons File upload. Closes gh-27612 --- .../DefaultPartHttpMessageReader.java | 2 +- .../http/codec/multipart/MultipartParser.java | 71 +++++++++++-------- .../DefaultPartHttpMessageReaderTests.java | 25 +++++++ .../http/codec/multipart/files.multipart | 2 - 4 files changed, 67 insertions(+), 33 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java index d4248c8442d3..6e94678cef11 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java @@ -63,7 +63,7 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements private int maxInMemorySize = 256 * 1024; - private int maxHeadersSize = 8 * 1024; + private int maxHeadersSize = 10 * 1024; private long maxDiskUsagePerPart = -1; diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartParser.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartParser.java index d2057f53d627..d797b99f4b1b 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartParser.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartParser.java @@ -342,33 +342,23 @@ private final class HeadersState implements State { /** * First checks whether the multipart boundary leading to this state - * was the final boundary, or whether {@link #maxHeadersSize} is - * exceeded. Then looks for the header-body boundary - * ({@code CR LF CR LF}) in the given buffer. If found, convert - * all buffers collected so far into a {@link HttpHeaders} object + * was the final boundary. Then looks for the header-body boundary + * ({@code CR LF CR LF}) in the given buffer. If found, checks whether + * the size of all header buffers does not exceed {@link #maxHeadersSize}, + * converts all buffers collected so far into a {@link HttpHeaders} object * and changes to {@link BodyState}, passing the remainder of the - * buffer. If the boundary is not found, the buffer is collected. + * buffer. If the boundary is not found, the buffer is collected if + * its size does not exceed {@link #maxHeadersSize}. */ @Override public void onNext(DataBuffer buf) { - long prevCount = this.byteCount.get(); - long count = this.byteCount.addAndGet(buf.readableByteCount()); - if (prevCount < 2 && count >= 2) { - if (isLastBoundary(buf)) { - if (logger.isTraceEnabled()) { - logger.trace("Last boundary found in " + buf); - } - - if (changeState(this, DisposedState.INSTANCE, buf)) { - emitComplete(); - } - return; + if (isLastBoundary(buf)) { + if (logger.isTraceEnabled()) { + logger.trace("Last boundary found in " + buf); } - } - else if (count > MultipartParser.this.maxHeadersSize) { + if (changeState(this, DisposedState.INSTANCE, buf)) { - emitError(new DataBufferLimitException("Part headers exceeded the memory usage limit of " + - MultipartParser.this.maxHeadersSize + " bytes")); + emitComplete(); } return; } @@ -377,17 +367,23 @@ else if (count > MultipartParser.this.maxHeadersSize) { if (logger.isTraceEnabled()) { logger.trace("End of headers found @" + endIdx + " in " + buf); } - DataBuffer headerBuf = MultipartUtils.sliceTo(buf, endIdx); - this.buffers.add(headerBuf); - DataBuffer bodyBuf = MultipartUtils.sliceFrom(buf, endIdx); - DataBufferUtils.release(buf); - - emitHeaders(parseHeaders()); - changeState(this, new BodyState(), bodyBuf); + long count = this.byteCount.addAndGet(endIdx); + if (belowMaxHeaderSize(count)) { + DataBuffer headerBuf = MultipartUtils.sliceTo(buf, endIdx); + this.buffers.add(headerBuf); + DataBuffer bodyBuf = MultipartUtils.sliceFrom(buf, endIdx); + DataBufferUtils.release(buf); + + emitHeaders(parseHeaders()); + changeState(this, new BodyState(), bodyBuf); + } } else { - this.buffers.add(buf); - requestBuffer(); + long count = this.byteCount.addAndGet(buf.readableByteCount()); + if (belowMaxHeaderSize(count)) { + this.buffers.add(buf); + requestBuffer(); + } } } @@ -407,6 +403,21 @@ private boolean isLastBoundary(DataBuffer buf) { buf.getByte(0) == HYPHEN); } + /** + * Checks whether the given {@code count} is below or equal to {@link #maxHeadersSize} + * and emits a {@link DataBufferLimitException} if not. + */ + private boolean belowMaxHeaderSize(long count) { + if (count <= MultipartParser.this.maxHeadersSize) { + return true; + } + else { + emitError(new DataBufferLimitException("Part headers exceeded the memory usage limit of " + + MultipartParser.this.maxHeadersSize + " bytes")); + return false; + } + } + /** * Parses the list of buffers into a {@link HttpHeaders} instance. * Converts the joined buffers into a string using ISO=8859-1, and parses diff --git a/spring-web/src/test/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReaderTests.java b/spring-web/src/test/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReaderTests.java index 8e812e720fb0..9179a546820f 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReaderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReaderTests.java @@ -270,6 +270,31 @@ public void utf8Headers(String displayName, DefaultPartHttpMessageReader reader) latch.await(); } + // gh-27612 + @Test + public void exceedHeaderLimit() throws InterruptedException { + Flux body = DataBufferUtils + .readByteChannel((new ClassPathResource("files.multipart", getClass()))::readableChannel, bufferFactory, 282); + + MediaType contentType = new MediaType("multipart", "form-data", singletonMap("boundary", "----WebKitFormBoundaryG8fJ50opQOML0oGD")); + MockServerHttpRequest request = MockServerHttpRequest.post("/") + .contentType(contentType) + .body(body); + + DefaultPartHttpMessageReader reader = new DefaultPartHttpMessageReader(); + + reader.setMaxHeadersSize(230); + + Flux result = reader.read(forClass(Part.class), request, emptyMap()); + + CountDownLatch latch = new CountDownLatch(2); + StepVerifier.create(result) + .consumeNextWith(part -> testPart(part, null, LOREM_IPSUM, latch)) + .consumeNextWith(part -> testPart(part, null, MUSPI_MEROL, latch)) + .verifyComplete(); + + latch.await(); + } private void testBrowser(DefaultPartHttpMessageReader reader, Resource resource, String boundary) throws InterruptedException { diff --git a/spring-web/src/test/resources/org/springframework/http/codec/multipart/files.multipart b/spring-web/src/test/resources/org/springframework/http/codec/multipart/files.multipart index 03b41190647e..bd10aea6b0a3 100644 --- a/spring-web/src/test/resources/org/springframework/http/codec/multipart/files.multipart +++ b/spring-web/src/test/resources/org/springframework/http/codec/multipart/files.multipart @@ -3,11 +3,9 @@ Content-Disposition: form-data; name="file2"; filename="a.txt" Content-Type: text/plain Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer iaculis metus id vestibulum nullam. - ------WebKitFormBoundaryG8fJ50opQOML0oGD Content-Disposition: form-data; name="file2"; filename="b.txt" Content-Type: text/plain .mallun mulubitsev di sutem silucai regetnI .tile gnicsipida rutetcesnoc ,tema tis rolod muspi meroL - ------WebKitFormBoundaryG8fJ50opQOML0oGD-- From 40d84c297bd88a2c4b7979b14eb7925e5fce0170 Mon Sep 17 00:00:00 2001 From: Xjzon <1070809335@qq.com> Date: Thu, 28 Oct 2021 00:11:59 +0800 Subject: [PATCH 19/37] Add support for custom expression parsing See gh-27604 --- .../context/expression/CachedExpressionEvaluator.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/spring-context/src/main/java/org/springframework/context/expression/CachedExpressionEvaluator.java b/spring-context/src/main/java/org/springframework/context/expression/CachedExpressionEvaluator.java index 7b868fc9871e..13a04440d542 100644 --- a/spring-context/src/main/java/org/springframework/context/expression/CachedExpressionEvaluator.java +++ b/spring-context/src/main/java/org/springframework/context/expression/CachedExpressionEvaluator.java @@ -86,7 +86,7 @@ protected Expression getExpression(Map cache, ExpressionKey expressionKey = createKey(elementKey, expression); Expression expr = cache.get(expressionKey); if (expr == null) { - expr = getParser().parseExpression(expression); + expr = parseExpression(expression); cache.put(expressionKey, expr); } return expr; @@ -96,6 +96,13 @@ private ExpressionKey createKey(AnnotatedElementKey elementKey, String expressio return new ExpressionKey(elementKey, expression); } + /** + * Parse the expression + * @param expression the expression to parse + */ + protected Expression parseExpression(String expression) { + return getParser().parseExpression(expression); + } /** * An expression key. From f295c8b2fbb8ac61f570c9b35abb45f0837fdd66 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 28 Oct 2021 14:10:21 +0200 Subject: [PATCH 20/37] Polish "Add support for custom expression parsing" See gh-27604 --- .../expression/CachedExpressionEvaluator.java | 14 ++++++++------ .../expression/CachedExpressionEvaluatorTests.java | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/expression/CachedExpressionEvaluator.java b/spring-context/src/main/java/org/springframework/context/expression/CachedExpressionEvaluator.java index 13a04440d542..c2d197a122e0 100644 --- a/spring-context/src/main/java/org/springframework/context/expression/CachedExpressionEvaluator.java +++ b/spring-context/src/main/java/org/springframework/context/expression/CachedExpressionEvaluator.java @@ -75,7 +75,7 @@ protected ParameterNameDiscoverer getParameterNameDiscoverer() { /** * Return the {@link Expression} for the specified SpEL value - *

Parse the expression if it hasn't been already. + *

{@link #parseExpression(String) Parse the expression} if it hasn't been already. * @param cache the cache to use * @param elementKey the element on which the expression is defined * @param expression the expression to parse @@ -92,18 +92,20 @@ protected Expression getExpression(Map cache, return expr; } - private ExpressionKey createKey(AnnotatedElementKey elementKey, String expression) { - return new ExpressionKey(elementKey, expression); - } - /** - * Parse the expression + * Parse the specified {@code expression}. * @param expression the expression to parse + * @since 5.3.13 */ protected Expression parseExpression(String expression) { return getParser().parseExpression(expression); } + private ExpressionKey createKey(AnnotatedElementKey elementKey, String expression) { + return new ExpressionKey(elementKey, expression); + } + + /** * An expression key. */ diff --git a/spring-context/src/test/java/org/springframework/context/expression/CachedExpressionEvaluatorTests.java b/spring-context/src/test/java/org/springframework/context/expression/CachedExpressionEvaluatorTests.java index f81a7945d738..1594137c0411 100644 --- a/spring-context/src/test/java/org/springframework/context/expression/CachedExpressionEvaluatorTests.java +++ b/spring-context/src/test/java/org/springframework/context/expression/CachedExpressionEvaluatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 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. From 694db2273f24be1152914c9f5bb849a8c0ccbb12 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 2 Nov 2021 16:13:35 +0100 Subject: [PATCH 21/37] Add Part::delete method This commit introduces the Part::delete method, that deletes its underlying storage. Closes gh-27612 --- .../http/codec/multipart/Content.java | 129 ++++++++++++++++++ .../http/codec/multipart/DefaultParts.java | 17 ++- .../http/codec/multipart/Part.java | 10 ++ .../http/codec/multipart/PartGenerator.java | 16 +-- .../SynchronossPartHttpMessageReader.java | 35 +++++ 5 files changed, 189 insertions(+), 18 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/http/codec/multipart/Content.java diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/Content.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/Content.java new file mode 100644 index 000000000000..28178c8c8ee4 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/Content.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2021 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.http.codec.multipart; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; + +/** + * Part content abstraction used by {@link DefaultParts}. + * + * @author Arjen Poutsma + * @since 5.3.13 + */ +abstract class Content { + + + protected Content() { + } + + /** + * Return the content. + */ + public abstract Flux content(); + + /** + * Delete this content. Default implementation does nothing. + */ + public Mono delete() { + return Mono.empty(); + } + + /** + * Returns a new {@code Content} based on the given flux of data buffers. + */ + public static Content fromFlux(Flux content) { + return new FluxContent(content); + } + + /** + * Return a new {@code Content} based on the given file path. + */ + public static Content fromFile(Path file, Scheduler scheduler) { + return new FileContent(file, scheduler); + } + + + /** + * {@code Content} implementation based on a flux of data buffers. + */ + private static final class FluxContent extends Content { + + private final Flux content; + + + public FluxContent(Flux content) { + this.content = content; + } + + + @Override + public Flux content() { + return this.content; + } + } + + + /** + * {@code Content} implementation based on a file. + */ + private static final class FileContent extends Content { + + private final Path file; + + private final Scheduler scheduler; + + + public FileContent(Path file, Scheduler scheduler) { + this.file = file; + this.scheduler = scheduler; + } + + + @Override + public Flux content() { + return DataBufferUtils.readByteChannel( + () -> Files.newByteChannel(this.file, StandardOpenOption.READ), + DefaultDataBufferFactory.sharedInstance, 1024) + .subscribeOn(this.scheduler); + } + + @Override + public Mono delete() { + return Mono.fromRunnable(() -> { + try { + Files.delete(this.file); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }) + .subscribeOn(this.scheduler); + } + } +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultParts.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultParts.java index 77044db0a8dc..f18ec157b9ad 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultParts.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultParts.java @@ -57,7 +57,7 @@ public static FormFieldPart formFieldPart(HttpHeaders headers, String value) { * @param content the content of the part * @return {@link Part} or {@link FilePart}, depending on {@link HttpHeaders#getContentDisposition()} */ - public static Part part(HttpHeaders headers, Flux content) { + public static Part part(HttpHeaders headers, Content content) { Assert.notNull(headers, "Headers must not be null"); Assert.notNull(content, "Content must not be null"); @@ -142,16 +142,21 @@ public String toString() { */ private static class DefaultPart extends AbstractPart { - private final Flux content; + private final Content content; - public DefaultPart(HttpHeaders headers, Flux content) { + public DefaultPart(HttpHeaders headers, Content content) { super(headers); this.content = content; } @Override public Flux content() { - return this.content; + return this.content.content(); + } + + @Override + public Mono delete() { + return this.content.delete(); } @Override @@ -171,9 +176,9 @@ public String toString() { /** * Default implementation of {@link FilePart}. */ - private static class DefaultFilePart extends DefaultPart implements FilePart { + private static final class DefaultFilePart extends DefaultPart implements FilePart { - public DefaultFilePart(HttpHeaders headers, Flux content) { + public DefaultFilePart(HttpHeaders headers, Content content) { super(headers, content); } diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/Part.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/Part.java index c611adf22ae6..c39b36ff5faf 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/Part.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/Part.java @@ -17,6 +17,7 @@ package org.springframework.http.codec.multipart; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpHeaders; @@ -57,4 +58,13 @@ public interface Part { */ Flux content(); + /** + * Return a mono that, when subscribed to, deletes the underlying storage + * for this part. + * @since 5.3.13 + */ + default Mono delete() { + return Mono.empty(); + } + } diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java index 32a923d8a7d5..42b43e2eb5ef 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java @@ -160,7 +160,7 @@ else if (!this.streaming) { requestToken(); } }); - emitPart(DefaultParts.part(headers, streamingContent)); + emitPart(DefaultParts.part(headers, Content.fromFlux(streamingContent))); } } @@ -518,7 +518,7 @@ private void emitMemoryPart() { } this.content.clear(); Flux content = Flux.just(DefaultDataBufferFactory.sharedInstance.wrap(bytes)); - emitPart(DefaultParts.part(this.headers, content)); + emitPart(DefaultParts.part(this.headers, Content.fromFlux(content))); } @Override @@ -674,21 +674,13 @@ public void body(DataBuffer dataBuffer) { @Override public void partComplete(boolean finalPart) { MultipartUtils.closeChannel(this.channel); - Flux content = partContent(); - emitPart(DefaultParts.part(this.headers, content)); + emitPart(DefaultParts.part(this.headers, + Content.fromFile(this.file, PartGenerator.this.blockingOperationScheduler))); if (finalPart) { emitComplete(); } } - private Flux partContent() { - return DataBufferUtils - .readByteChannel( - () -> Files.newByteChannel(this.file, StandardOpenOption.READ), - DefaultDataBufferFactory.sharedInstance, 1024) - .subscribeOn(PartGenerator.this.blockingOperationScheduler); - } - @Override public void dispose() { if (this.closeOnDispose) { diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java index 032b787d887b..a35c9d5712de 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java @@ -16,7 +16,9 @@ package org.springframework.http.codec.multipart; +import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; @@ -40,6 +42,7 @@ import org.synchronoss.cloud.nio.multipart.NioMultipartParser; import org.synchronoss.cloud.nio.multipart.NioMultipartParserListener; import org.synchronoss.cloud.nio.multipart.PartBodyStreamStorageFactory; +import org.synchronoss.cloud.nio.stream.storage.NameAwarePurgableFileInputStream; import org.synchronoss.cloud.nio.stream.storage.StreamStorage; import reactor.core.publisher.BaseSubscriber; import reactor.core.publisher.Flux; @@ -497,6 +500,38 @@ public Flux content() { protected StreamStorage getStorage() { return this.storage; } + + @Override + public Mono delete() { + return Mono.fromRunnable(() -> { + File file = getFile(); + if (file != null) { + file.delete(); + } + }); + } + + @Nullable + private File getFile() { + InputStream inputStream = null; + try { + inputStream = getStorage().getInputStream(); + if (inputStream instanceof NameAwarePurgableFileInputStream) { + NameAwarePurgableFileInputStream stream = (NameAwarePurgableFileInputStream) inputStream; + return stream.getFile(); + } + } + finally { + if (inputStream != null) { + try { + inputStream.close(); + } + catch (IOException ignore) { + } + } + } + return null; + } } From 0c7e0002504f728d3ca3e182406d19ed3c4c9973 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 3 Nov 2021 15:02:12 +0100 Subject: [PATCH 22/37] Refactor Contents to DefaultParts This commit moves the Contents abstraction into DefaultParts See gh-27613 --- .../http/codec/multipart/Content.java | 129 ---------------- .../http/codec/multipart/DefaultParts.java | 139 +++++++++++++++++- .../http/codec/multipart/PartGenerator.java | 7 +- 3 files changed, 135 insertions(+), 140 deletions(-) delete mode 100644 spring-web/src/main/java/org/springframework/http/codec/multipart/Content.java diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/Content.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/Content.java deleted file mode 100644 index 28178c8c8ee4..000000000000 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/Content.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2002-2021 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.http.codec.multipart; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; - -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.scheduler.Scheduler; - -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferUtils; -import org.springframework.core.io.buffer.DefaultDataBufferFactory; - -/** - * Part content abstraction used by {@link DefaultParts}. - * - * @author Arjen Poutsma - * @since 5.3.13 - */ -abstract class Content { - - - protected Content() { - } - - /** - * Return the content. - */ - public abstract Flux content(); - - /** - * Delete this content. Default implementation does nothing. - */ - public Mono delete() { - return Mono.empty(); - } - - /** - * Returns a new {@code Content} based on the given flux of data buffers. - */ - public static Content fromFlux(Flux content) { - return new FluxContent(content); - } - - /** - * Return a new {@code Content} based on the given file path. - */ - public static Content fromFile(Path file, Scheduler scheduler) { - return new FileContent(file, scheduler); - } - - - /** - * {@code Content} implementation based on a flux of data buffers. - */ - private static final class FluxContent extends Content { - - private final Flux content; - - - public FluxContent(Flux content) { - this.content = content; - } - - - @Override - public Flux content() { - return this.content; - } - } - - - /** - * {@code Content} implementation based on a file. - */ - private static final class FileContent extends Content { - - private final Path file; - - private final Scheduler scheduler; - - - public FileContent(Path file, Scheduler scheduler) { - this.file = file; - this.scheduler = scheduler; - } - - - @Override - public Flux content() { - return DataBufferUtils.readByteChannel( - () -> Files.newByteChannel(this.file, StandardOpenOption.READ), - DefaultDataBufferFactory.sharedInstance, 1024) - .subscribeOn(this.scheduler); - } - - @Override - public Mono delete() { - return Mono.fromRunnable(() -> { - try { - Files.delete(this.file); - } - catch (IOException ex) { - throw new UncheckedIOException(ex); - } - }) - .subscribeOn(this.scheduler); - } - } -} diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultParts.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultParts.java index f18ec157b9ad..284c82497b96 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultParts.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultParts.java @@ -16,10 +16,15 @@ package org.springframework.http.codec.multipart; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.concurrent.Callable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; @@ -50,17 +55,40 @@ public static FormFieldPart formFieldPart(HttpHeaders headers, String value) { } /** - * Create a new {@link Part} or {@link FilePart} with the given parameters. + * Create a new {@link Part} or {@link FilePart} based on a flux of data + * buffers. Returns {@link FilePart} if the {@code Content-Disposition} of + * the given headers contains a filename, or a "normal" {@link Part} + * otherwise. + * @param headers the part headers + * @param dataBuffers the content of the part + * @return {@link Part} or {@link FilePart}, depending on {@link HttpHeaders#getContentDisposition()} + */ + public static Part part(HttpHeaders headers, Flux dataBuffers) { + Assert.notNull(headers, "Headers must not be null"); + Assert.notNull(dataBuffers, "DataBuffers must not be null"); + + return partInternal(headers, new FluxContent(dataBuffers)); + } + + /** + * Create a new {@link Part} or {@link FilePart} based on the given file. * Returns {@link FilePart} if the {@code Content-Disposition} of the given * headers contains a filename, or a "normal" {@link Part} otherwise * @param headers the part headers - * @param content the content of the part + * @param file the file + * @param scheduler the scheduler used for reading the file * @return {@link Part} or {@link FilePart}, depending on {@link HttpHeaders#getContentDisposition()} */ - public static Part part(HttpHeaders headers, Content content) { + public static Part part(HttpHeaders headers, Path file, Scheduler scheduler) { Assert.notNull(headers, "Headers must not be null"); - Assert.notNull(content, "Content must not be null"); + Assert.notNull(file, "File must not be null"); + Assert.notNull(scheduler, "Scheduler must not be null"); + + return partInternal(headers, new FileContent(file, scheduler)); + } + + private static Part partInternal(HttpHeaders headers, Content content) { String filename = headers.getContentDisposition().getFilename(); if (filename != null) { return new DefaultFilePart(headers, content); @@ -142,7 +170,8 @@ public String toString() { */ private static class DefaultPart extends AbstractPart { - private final Content content; + protected final Content content; + public DefaultPart(HttpHeaders headers, Content content) { super(headers); @@ -191,7 +220,7 @@ public String filename() { @Override public Mono transferTo(Path dest) { - return DataBufferUtils.write(content(), dest); + return this.content.transferTo(dest); } @Override @@ -200,7 +229,7 @@ public String toString() { String name = contentDisposition.getName(); String filename = contentDisposition.getFilename(); if (name != null) { - return "DefaultFilePart{" + name() + " (" + filename + ")}"; + return "DefaultFilePart{" + name + " (" + filename + ")}"; } else { return "DefaultFilePart{(" + filename + ")}"; @@ -209,4 +238,100 @@ public String toString() { } + + /** + * Part content abstraction. + */ + private interface Content { + + Flux content(); + + Mono transferTo(Path dest); + + Mono delete(); + + } + + /** + * {@code Content} implementation based on a flux of data buffers. + */ + private static final class FluxContent implements Content { + + private final Flux content; + + + public FluxContent(Flux content) { + this.content = content; + } + + + @Override + public Flux content() { + return this.content; + } + + @Override + public Mono transferTo(Path dest) { + return DataBufferUtils.write(this.content, dest); + } + + @Override + public Mono delete() { + return Mono.empty(); + } + + } + + + /** + * {@code Content} implementation based on a file. + */ + private static final class FileContent implements Content { + + private final Path file; + + private final Scheduler scheduler; + + + public FileContent(Path file, Scheduler scheduler) { + this.file = file; + this.scheduler = scheduler; + } + + + @Override + public Flux content() { + return DataBufferUtils.readByteChannel( + () -> Files.newByteChannel(this.file, StandardOpenOption.READ), + DefaultDataBufferFactory.sharedInstance, 1024) + .subscribeOn(this.scheduler); + } + + @Override + public Mono transferTo(Path dest) { + return blockingOperation(() -> Files.copy(this.file, dest, StandardCopyOption.REPLACE_EXISTING)); + } + + @Override + public Mono delete() { + return blockingOperation(() -> { + Files.delete(this.file); + return null; + }); + } + + private Mono blockingOperation(Callable callable) { + return Mono.create(sink -> { + try { + callable.call(); + sink.success(); + } + catch (Exception ex) { + sink.error(ex); + } + }) + .subscribeOn(this.scheduler); + } + } + } diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java index 42b43e2eb5ef..88d689d90e9b 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java @@ -160,7 +160,7 @@ else if (!this.streaming) { requestToken(); } }); - emitPart(DefaultParts.part(headers, Content.fromFlux(streamingContent))); + emitPart(DefaultParts.part(headers, streamingContent)); } } @@ -518,7 +518,7 @@ private void emitMemoryPart() { } this.content.clear(); Flux content = Flux.just(DefaultDataBufferFactory.sharedInstance.wrap(bytes)); - emitPart(DefaultParts.part(this.headers, Content.fromFlux(content))); + emitPart(DefaultParts.part(this.headers, content)); } @Override @@ -674,8 +674,7 @@ public void body(DataBuffer dataBuffer) { @Override public void partComplete(boolean finalPart) { MultipartUtils.closeChannel(this.channel); - emitPart(DefaultParts.part(this.headers, - Content.fromFile(this.file, PartGenerator.this.blockingOperationScheduler))); + emitPart(DefaultParts.part(this.headers, this.file, PartGenerator.this.blockingOperationScheduler)); if (finalPart) { emitComplete(); } From 1b4438caacee097238de7e7713a2a50bf73ed216 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 4 Nov 2021 14:47:37 +0100 Subject: [PATCH 23/37] Start building against Reactor 2020.0.13 snapshots See gh-27636 --- build.gradle | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e5aa6b6b7d1d..d954fa16003d 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ configure(allprojects) { project -> imports { mavenBom "com.fasterxml.jackson:jackson-bom:2.12.5" mavenBom "io.netty:netty-bom:4.1.69.Final" - mavenBom "io.projectreactor:reactor-bom:2020.0.12" + mavenBom "io.projectreactor:reactor-bom:2020.0.13-SNAPSHOT" mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR10" mavenBom "io.rsocket:rsocket-bom:1.1.1" mavenBom "org.eclipse.jetty:jetty-bom:9.4.44.v20210927" @@ -292,6 +292,8 @@ configure(allprojects) { project -> repositories { mavenCentral() maven { url "https://repo.spring.io/libs-spring-framework-build" } + maven { url "https://repo.spring.io/snapshot" } // reactor + } } configurations.all { From ff1485fd8d6581edce2ad9bfbe2eb36d2efae8cc Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Fri, 5 Nov 2021 10:31:10 +0100 Subject: [PATCH 24/37] Fix log message formatting in CandidateComponentsIndexLoader --- .../context/index/CandidateComponentsIndexLoader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndexLoader.java b/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndexLoader.java index af068373d8c9..ee95954b10ee 100644 --- a/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndexLoader.java +++ b/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndexLoader.java @@ -107,7 +107,7 @@ private static CandidateComponentsIndex doLoadIndex(ClassLoader classLoader) { result.add(properties); } if (logger.isDebugEnabled()) { - logger.debug("Loaded " + result.size() + "] index(es)"); + logger.debug("Loaded " + result.size() + " index(es)"); } int totalCount = result.stream().mapToInt(Properties::size).sum(); return (totalCount > 0 ? new CandidateComponentsIndex(result) : null); From c5de5c99390a69f23e7682b7748c273b01a63907 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 8 Nov 2021 21:21:54 +0000 Subject: [PATCH 25/37] Update Javadoc of LogFormatUtils Closes gh-27632 --- .../java/org/springframework/core/log/LogFormatUtils.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/log/LogFormatUtils.java b/spring-core/src/main/java/org/springframework/core/log/LogFormatUtils.java index e3ae033e9f22..51ffd98cf0ab 100644 --- a/spring-core/src/main/java/org/springframework/core/log/LogFormatUtils.java +++ b/spring-core/src/main/java/org/springframework/core/log/LogFormatUtils.java @@ -36,8 +36,9 @@ public abstract class LogFormatUtils { /** - * Variant of {@link #formatValue(Object, int, boolean)} and a convenience - * method that truncates at 100 characters when {@code limitLength} is set. + * Convenience variant of {@link #formatValue(Object, int, boolean)} that + * limits the length of a log message to 100 characters and also replaces + * newline characters if {@code limitLength} is set to "true". * @param value the value to format * @param limitLength whether to truncate the value at a length of 100 * @return the formatted value From b5743966d6d926ed9b146ca4c34713b406628038 Mon Sep 17 00:00:00 2001 From: happyWilliam0 Date: Fri, 29 Oct 2021 14:14:05 +0800 Subject: [PATCH 26/37] Improve efficiency of UrlPathHelper#getSanitizedPath See gh-27623 --- .../web/util/UrlPathHelper.java | 19 ++++++++++--------- .../web/util/UrlPathHelperTests.java | 6 ++++++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java index e38fdff9b53c..a4a85e707b8b 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java +++ b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java @@ -405,16 +405,17 @@ else if (index1 == requestUri.length()) { * */ private static String getSanitizedPath(final String path) { - int index = path.indexOf("//"); - if (index >= 0) { - StringBuilder sanitized = new StringBuilder(path); - while (index != -1) { - sanitized.deleteCharAt(index); - index = sanitized.indexOf("//", index); + if (path.length() == 0) { + return path; + } + char[] arr = path.toCharArray(); + int slowIndex = 0; + for (int fastIndex = 1; fastIndex < arr.length; fastIndex++) { + if (arr[fastIndex] != '/' || arr[slowIndex] != '/') { + arr[++slowIndex] = arr[fastIndex]; } - return sanitized.toString(); } - return path; + return new String(arr, 0, slowIndex + 1); } /** @@ -532,7 +533,7 @@ public String getOriginatingServletPath(HttpServletRequest request) { */ public String getOriginatingQueryString(HttpServletRequest request) { if ((request.getAttribute(WebUtils.FORWARD_REQUEST_URI_ATTRIBUTE) != null) || - (request.getAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE) != null)) { + (request.getAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE) != null)) { return (String) request.getAttribute(WebUtils.FORWARD_QUERY_STRING_ATTRIBUTE); } else { diff --git a/spring-web/src/test/java/org/springframework/web/util/UrlPathHelperTests.java b/spring-web/src/test/java/org/springframework/web/util/UrlPathHelperTests.java index e3281e55273f..49d8ed6a079a 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UrlPathHelperTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UrlPathHelperTests.java @@ -246,6 +246,12 @@ void removeDuplicateSlashesInPath() { request.setRequestURI("/SPR-12372/foo/bar//"); assertThat(helper.getLookupPathForRequest(request)).isEqualTo("/foo/bar//"); + + // "enhance" case + request.setServletPath("/foo/bar//"); + request.setRequestURI("/SPR-12372////////////////////////foo//////////////////bar////////////////////"); + + assertThat(helper.getLookupPathForRequest(request)).isEqualTo("/foo/bar//"); } @Test From c6ce65ef564dfb89944ae07dd3a2c25f28fc287f Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 9 Nov 2021 09:45:35 +0000 Subject: [PATCH 27/37] Polishing contribution Closes gh-27623 --- .../springframework/web/util/UrlPathHelper.java | 15 ++++++++------- .../web/util/UrlPathHelperTests.java | 4 ++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java index a4a85e707b8b..685f376b426a 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java +++ b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java @@ -405,17 +405,18 @@ else if (index1 == requestUri.length()) { * */ private static String getSanitizedPath(final String path) { - if (path.length() == 0) { + int start = path.indexOf("//"); + if (start == -1) { return path; } - char[] arr = path.toCharArray(); - int slowIndex = 0; - for (int fastIndex = 1; fastIndex < arr.length; fastIndex++) { - if (arr[fastIndex] != '/' || arr[slowIndex] != '/') { - arr[++slowIndex] = arr[fastIndex]; + char[] content = path.toCharArray(); + int slowIndex = start; + for (int fastIndex = start + 1; fastIndex < content.length; fastIndex++) { + if (content[fastIndex] != '/' || content[slowIndex] != '/') { + content[++slowIndex] = content[fastIndex]; } } - return new String(arr, 0, slowIndex + 1); + return new String(content, 0, slowIndex + 1); } /** diff --git a/spring-web/src/test/java/org/springframework/web/util/UrlPathHelperTests.java b/spring-web/src/test/java/org/springframework/web/util/UrlPathHelperTests.java index 49d8ed6a079a..e7ef05083780 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UrlPathHelperTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UrlPathHelperTests.java @@ -232,12 +232,12 @@ void removeDuplicateSlashesInPath() { request.setContextPath("/SPR-12372"); request.setPathInfo(null); request.setServletPath("/foo/bar/"); - request.setRequestURI("/SPR-12372/foo//bar/"); + request.setRequestURI("/SPR-12372/foo///bar/"); assertThat(helper.getLookupPathForRequest(request)).isEqualTo("/foo/bar/"); request.setServletPath("/foo/bar/"); - request.setRequestURI("/SPR-12372/foo/bar//"); + request.setRequestURI("////SPR-12372/foo/bar//"); assertThat(helper.getLookupPathForRequest(request)).isEqualTo("/foo/bar/"); From 6ac780d595b9a661d6ec357183f0d5bd0028981b Mon Sep 17 00:00:00 2001 From: Abdullah Khan Date: Tue, 9 Nov 2021 17:47:15 +0530 Subject: [PATCH 28/37] Fix grammar in webflux-webclient.adoc Closes gh-27657 --- src/docs/asciidoc/web/webflux-webclient.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/docs/asciidoc/web/webflux-webclient.adoc b/src/docs/asciidoc/web/webflux-webclient.adoc index 22e0d265136e..3099ecb02d26 100644 --- a/src/docs/asciidoc/web/webflux-webclient.adoc +++ b/src/docs/asciidoc/web/webflux-webclient.adoc @@ -89,7 +89,7 @@ modified copy as follows: === MaxInMemorySize Codecs have <> for buffering data in -memory to avoid application memory issues. By the default those are set to 256KB. +memory to avoid application memory issues. By default those are set to 256KB. If that's not enough you'll get the following error: ---- From 39be572a7a70b7db24d0a0c4fd9a74a200ca6e3f Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 9 Nov 2021 19:38:10 +0100 Subject: [PATCH 29/37] Upgrade to Reactor 2020.0.13 Closes gh-27636 --- build.gradle | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index d954fa16003d..98c85c3d008b 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ configure(allprojects) { project -> imports { mavenBom "com.fasterxml.jackson:jackson-bom:2.12.5" mavenBom "io.netty:netty-bom:4.1.69.Final" - mavenBom "io.projectreactor:reactor-bom:2020.0.13-SNAPSHOT" + mavenBom "io.projectreactor:reactor-bom:2020.0.13" mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR10" mavenBom "io.rsocket:rsocket-bom:1.1.1" mavenBom "org.eclipse.jetty:jetty-bom:9.4.44.v20210927" @@ -292,7 +292,6 @@ configure(allprojects) { project -> repositories { mavenCentral() maven { url "https://repo.spring.io/libs-spring-framework-build" } - maven { url "https://repo.spring.io/snapshot" } // reactor } } From 9bc09631d61e2d011972232a5b32c1edc51c6dcf Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 9 Nov 2021 22:29:56 +0100 Subject: [PATCH 30/37] Recommend ObjectProvider as alternative to @Lazy for optional dependencies Closes gh-27649 --- .../org/springframework/context/annotation/Lazy.java | 7 ++++++- src/docs/asciidoc/core/core-beans.adoc | 10 ++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Lazy.java b/spring-context/src/main/java/org/springframework/context/annotation/Lazy.java index 9d04a9df26ee..8369957f5917 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Lazy.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Lazy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,6 +47,11 @@ * or {@link javax.inject.Inject}: In that context, it leads to the creation of a * lazy-resolution proxy for all affected dependencies, as an alternative to using * {@link org.springframework.beans.factory.ObjectFactory} or {@link javax.inject.Provider}. + * Please note that such a lazy-resolution proxy will always be injected; if the target + * dependency does not exist, you will only be able to find out through an exception on + * invocation. As a consequence, such an injection point results in unintuitive behavior + * for optional dependencies. For a programmatic equivalent, allowing for lazy references + * with more sophistication, consider {@link org.springframework.beans.factory.ObjectProvider}. * * @author Chris Beams * @author Juergen Hoeller diff --git a/src/docs/asciidoc/core/core-beans.adoc b/src/docs/asciidoc/core/core-beans.adoc index 5ba741761f28..8d701befcbcc 100644 --- a/src/docs/asciidoc/core/core-beans.adoc +++ b/src/docs/asciidoc/core/core-beans.adoc @@ -3039,7 +3039,7 @@ constructor or setter argument or autowired field) as `ObjectFactory`, which delivers +As an extended variant, you may declare `ObjectProvider` which delivers several additional access variants, including `getIfAvailable` and `getIfUnique`. The JSR-330 variant of this is called `Provider` and is used with a `Provider` @@ -6675,9 +6675,11 @@ factory method and other bean definition properties, such as a qualifier value t the `@Qualifier` annotation. Other method-level annotations that can be specified are `@Scope`, `@Lazy`, and custom qualifier annotations. -TIP: In addition to its role for component initialization, you can also place the `@Lazy` annotation -on injection points marked with `@Autowired` or `@Inject`. In this context, it -leads to the injection of a lazy-resolution proxy. +TIP: In addition to its role for component initialization, you can also place the `@Lazy` +annotation on injection points marked with `@Autowired` or `@Inject`. In this context, +it leads to the injection of a lazy-resolution proxy. However, such a proxy approach +is rather limited. For sophisticated lazy interactions, in particular in combination +with optional dependencies, we recommend `ObjectProvider` instead. Autowired fields and methods are supported, as previously discussed, with additional support for autowiring of `@Bean` methods. The following example shows how to do so: From 8d735e6e605737f561252bb4e629c8fb160b9ddf Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 9 Nov 2021 22:30:44 +0100 Subject: [PATCH 31/37] Note on PersistenceExceptionTranslator sorting and non-singleton retrieval Closes gh-26412 --- .../PersistenceExceptionTranslationPostProcessor.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/spring-tx/src/main/java/org/springframework/dao/annotation/PersistenceExceptionTranslationPostProcessor.java b/spring-tx/src/main/java/org/springframework/dao/annotation/PersistenceExceptionTranslationPostProcessor.java index c975e9ec6b05..a612fe808d09 100644 --- a/spring-tx/src/main/java/org/springframework/dao/annotation/PersistenceExceptionTranslationPostProcessor.java +++ b/spring-tx/src/main/java/org/springframework/dao/annotation/PersistenceExceptionTranslationPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,6 @@ * PersistenceExceptionTranslator} interface, which are subsequently asked to translate * candidate exceptions. * - *

All of Spring's applicable resource factories (e.g. * {@link org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean}) * implement the {@code PersistenceExceptionTranslator} interface out of the box. @@ -47,6 +46,11 @@ * with the {@code @Repository} annotation, along with defining this post-processor * as a bean in the application context. * + *

As of 5.3, {@code PersistenceExceptionTranslator} beans will be sorted according + * to Spring's dependency ordering rules: see {@link org.springframework.core.Ordered} + * and {@link org.springframework.core.annotation.Order}. Note that such beans will + * get retrieved from any scope, not just singleton scope, as of this 5.3 revision. + * * @author Rod Johnson * @author Juergen Hoeller * @since 2.0 From b167e1a93fe299b19f339896d86e65337b05b78f Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 9 Nov 2021 22:35:24 +0100 Subject: [PATCH 32/37] Remove unnecessary final declarations (for consistency) --- .../AutowiredAnnotationBeanPostProcessor.java | 2 +- .../CommonAnnotationBeanPostProcessor.java | 4 ++-- .../annotation/ComponentScanAnnotationParser.java | 4 ++-- .../annotation/ConfigurationClassEnhancer.java | 14 +++++++------- .../PersistenceAnnotationBeanPostProcessor.java | 6 +++--- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java index e7131a562ef7..37f5884e671e 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java @@ -459,7 +459,7 @@ private InjectionMetadata findAutowiringMetadata(String beanName, Class clazz return metadata; } - private InjectionMetadata buildAutowiringMetadata(final Class clazz) { + private InjectionMetadata buildAutowiringMetadata(Class clazz) { if (!AnnotationUtils.isCandidateClass(clazz, this.autowiredAnnotationTypes)) { return InjectionMetadata.EMPTY; } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java index 8a3ca5a9f889..ecfdf8f83a9a 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java @@ -343,7 +343,7 @@ public PropertyValues postProcessPropertyValues( } - private InjectionMetadata findResourceMetadata(String beanName, final Class clazz, @Nullable PropertyValues pvs) { + private InjectionMetadata findResourceMetadata(String beanName, Class clazz, @Nullable PropertyValues pvs) { // Fall back to class name as cache key, for backwards compatibility with custom callers. String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName()); // Quick check on the concurrent map first, with minimal locking. @@ -363,7 +363,7 @@ private InjectionMetadata findResourceMetadata(String beanName, final Class c return metadata; } - private InjectionMetadata buildResourceMetadata(final Class clazz) { + private InjectionMetadata buildResourceMetadata(Class clazz) { if (!AnnotationUtils.isCandidateClass(clazz, resourceAnnotationTypes)) { return InjectionMetadata.EMPTY; } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java index 27e879bfb338..0a3c751ff395 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2021 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. @@ -65,7 +65,7 @@ public ComponentScanAnnotationParser(Environment environment, ResourceLoader res } - public Set parse(AnnotationAttributes componentScan, final String declaringClass) { + public Set parse(AnnotationAttributes componentScan, String declaringClass) { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry, componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader); diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java index 17079522c7f6..1b215e524cc9 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2021 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. @@ -453,8 +453,8 @@ private boolean isCurrentlyInvokedFactoryMethod(Method method) { * instance directly. If a FactoryBean instance is fetched through the container via &-dereferencing, * it will not be proxied. This too is aligned with the way XML configuration works. */ - private Object enhanceFactoryBean(final Object factoryBean, Class exposedType, - final ConfigurableBeanFactory beanFactory, final String beanName) { + private Object enhanceFactoryBean(Object factoryBean, Class exposedType, + ConfigurableBeanFactory beanFactory, String beanName) { try { Class clazz = factoryBean.getClass(); @@ -489,8 +489,8 @@ private Object enhanceFactoryBean(final Object factoryBean, Class exposedType return createCglibProxyForFactoryBean(factoryBean, beanFactory, beanName); } - private Object createInterfaceProxyForFactoryBean(final Object factoryBean, Class interfaceType, - final ConfigurableBeanFactory beanFactory, final String beanName) { + private Object createInterfaceProxyForFactoryBean(Object factoryBean, Class interfaceType, + ConfigurableBeanFactory beanFactory, String beanName) { return Proxy.newProxyInstance( factoryBean.getClass().getClassLoader(), new Class[] {interfaceType}, @@ -502,8 +502,8 @@ private Object createInterfaceProxyForFactoryBean(final Object factoryBean, Clas }); } - private Object createCglibProxyForFactoryBean(final Object factoryBean, - final ConfigurableBeanFactory beanFactory, final String beanName) { + private Object createCglibProxyForFactoryBean(Object factoryBean, + ConfigurableBeanFactory beanFactory, String beanName) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(factoryBean.getClass()); diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.java b/spring-orm/src/main/java/org/springframework/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.java index d9ff7afc1414..61582bee1180 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -373,7 +373,7 @@ public boolean requiresDestruction(Object bean) { } - private InjectionMetadata findPersistenceMetadata(String beanName, final Class clazz, @Nullable PropertyValues pvs) { + private InjectionMetadata findPersistenceMetadata(String beanName, Class clazz, @Nullable PropertyValues pvs) { // Fall back to class name as cache key, for backwards compatibility with custom callers. String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName()); // Quick check on the concurrent map first, with minimal locking. @@ -393,7 +393,7 @@ private InjectionMetadata findPersistenceMetadata(String beanName, final Class clazz) { + private InjectionMetadata buildPersistenceMetadata(Class clazz) { if (!AnnotationUtils.isCandidateClass(clazz, Arrays.asList(PersistenceContext.class, PersistenceUnit.class))) { return InjectionMetadata.EMPTY; } From 11a0df3fd144a8c792cd2f9d5a721d21abe52f2b Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 9 Nov 2021 22:36:11 +0100 Subject: [PATCH 33/37] Upgrade to Tomcat 9.0.54, Netty 4.1.70, HtmlUnit 2.54, XMLUnit 2.8.3 --- build.gradle | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index 98c85c3d008b..6021fa574ddf 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,7 @@ configure(allprojects) { project -> dependencyManagement { imports { mavenBom "com.fasterxml.jackson:jackson-bom:2.12.5" - mavenBom "io.netty:netty-bom:4.1.69.Final" + mavenBom "io.netty:netty-bom:4.1.70.Final" mavenBom "io.projectreactor:reactor-bom:2020.0.13" mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR10" mavenBom "io.rsocket:rsocket-bom:1.1.1" @@ -128,14 +128,14 @@ configure(allprojects) { project -> dependency "org.webjars:webjars-locator-core:0.48" dependency "org.webjars:underscorejs:1.8.3" - dependencySet(group: 'org.apache.tomcat', version: '9.0.53') { + dependencySet(group: 'org.apache.tomcat', version: '9.0.54') { entry 'tomcat-util' entry('tomcat-websocket') { exclude group: "org.apache.tomcat", name: "tomcat-websocket-api" exclude group: "org.apache.tomcat", name: "tomcat-servlet-api" } } - dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.53') { + dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.54') { entry 'tomcat-embed-core' entry 'tomcat-embed-websocket' } @@ -192,7 +192,7 @@ configure(allprojects) { project -> dependency "org.hamcrest:hamcrest:2.1" dependency "org.awaitility:awaitility:3.1.6" dependency "org.assertj:assertj-core:3.21.0" - dependencySet(group: 'org.xmlunit', version: '2.8.2') { + dependencySet(group: 'org.xmlunit', version: '2.8.3') { entry 'xmlunit-assertj' entry('xmlunit-matchers') { exclude group: "org.hamcrest", name: "hamcrest-core" @@ -206,10 +206,10 @@ configure(allprojects) { project -> } dependency "io.mockk:mockk:1.12.0" - dependency("net.sourceforge.htmlunit:htmlunit:2.53.0") { + dependency("net.sourceforge.htmlunit:htmlunit:2.54.0") { exclude group: "commons-logging", name: "commons-logging" } - dependency("org.seleniumhq.selenium:htmlunit-driver:2.53.0") { + dependency("org.seleniumhq.selenium:htmlunit-driver:2.54.0") { exclude group: "commons-logging", name: "commons-logging" } dependency("org.seleniumhq.selenium:selenium-java:3.141.59") { @@ -292,7 +292,6 @@ configure(allprojects) { project -> repositories { mavenCentral() maven { url "https://repo.spring.io/libs-spring-framework-build" } - } } configurations.all { From 2a268701c41aaa1324a6c2fcf0e7e71c40f91ec2 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 10 Nov 2021 14:21:48 +0100 Subject: [PATCH 34/37] Introduce optimizeLocations flag for resource location filtering on startup This flag is off by default since it requires jar files with directory entries. Closes gh-27624 --- .../config/ResourceHandlerRegistration.java | 25 +++- .../reactive/resource/ResourceWebHandler.java | 122 +++++++++++------- .../resource/ResourceWebHandlerTests.java | 4 +- .../ResourceHandlerRegistration.java | 23 +++- .../resource/ResourceHttpRequestHandler.java | 77 +++++++---- .../ResourceHttpRequestHandlerTests.java | 3 +- src/docs/asciidoc/web/webflux.adoc | 7 +- src/docs/asciidoc/web/webmvc.adoc | 7 +- 8 files changed, 188 insertions(+), 80 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/ResourceHandlerRegistration.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/ResourceHandlerRegistration.java index 8ee0e87eafb1..9734347cc665 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/ResourceHandlerRegistration.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/ResourceHandlerRegistration.java @@ -54,6 +54,8 @@ public class ResourceHandlerRegistration { private boolean useLastModified = true; + private boolean optimizeLocations = false; + @Nullable private Map mediaTypes; @@ -105,15 +107,33 @@ public ResourceHandlerRegistration setCacheControl(CacheControl cacheControl) { /** * Set whether the {@link Resource#lastModified()} information should be used to drive HTTP responses. *

This configuration is set to {@code true} by default. - * @param useLastModified whether the "last modified" resource information should be used. + * @param useLastModified whether the "last modified" resource information should be used * @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation * @since 5.3 + * @see ResourceWebHandler#setUseLastModified */ public ResourceHandlerRegistration setUseLastModified(boolean useLastModified) { this.useLastModified = useLastModified; return this; } + /** + * Set whether to optimize the specified locations through an existence check on startup, + * filtering non-existing directories upfront so that they do not have to be checked + * on every resource access. + *

The default is {@code false}, for defensiveness against zip files without directory + * entries which are unable to expose the existence of a directory upfront. Switch this flag to + * {@code true} for optimized access in case of a consistent jar layout with directory entries. + * @param optimizeLocations whether to optimize the locations through an existence check on startup + * @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation + * @since 5.3.13 + * @see ResourceWebHandler#setOptimizeLocations + */ + public ResourceHandlerRegistration setOptimizeLocations(boolean optimizeLocations) { + this.optimizeLocations = optimizeLocations; + return this; + } + /** * Configure a chain of resource resolvers and transformers to use. This * can be useful, for example, to apply a version strategy to resource URLs. @@ -181,8 +201,8 @@ protected String[] getPathPatterns() { */ protected ResourceWebHandler getRequestHandler() { ResourceWebHandler handler = new ResourceWebHandler(); - handler.setLocationValues(this.locationValues); handler.setResourceLoader(this.resourceLoader); + handler.setLocationValues(this.locationValues); if (this.resourceChainRegistration != null) { handler.setResourceResolvers(this.resourceChainRegistration.getResourceResolvers()); handler.setResourceTransformers(this.resourceChainRegistration.getResourceTransformers()); @@ -191,6 +211,7 @@ protected ResourceWebHandler getRequestHandler() { handler.setCacheControl(this.cacheControl); } handler.setUseLastModified(this.useLastModified); + handler.setOptimizeLocations(this.optimizeLocations); if (this.mediaTypes != null) { handler.setMediaTypes(this.mediaTypes); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java index c2ba1b234bf1..e6f0640a7022 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java @@ -72,8 +72,8 @@ *

This request handler may also be configured with a * {@link #setResourceResolvers(List) resourcesResolver} and * {@link #setResourceTransformers(List) resourceTransformer} chains to support - * arbitrary resolution and transformation of resources being served. By default a - * {@link PathResourceResolver} simply finds resources based on the configured + * arbitrary resolution and transformation of resources being served. By default + * a {@link PathResourceResolver} simply finds resources based on the configured * "locations". An application can configure additional resolvers and * transformers such as the {@link VersionResourceResolver} which can resolve * and prepare URLs for resources with a version in the URL. @@ -85,6 +85,7 @@ * * @author Rossen Stoyanchev * @author Brian Clozel + * @author Juergen Hoeller * @since 5.0 */ public class ResourceWebHandler implements WebHandler, InitializingBean { @@ -94,6 +95,9 @@ public class ResourceWebHandler implements WebHandler, InitializingBean { private static final Log logger = LogFactory.getLog(ResourceWebHandler.class); + @Nullable + private ResourceLoader resourceLoader; + private final List locationValues = new ArrayList<>(4); private final List locationResources = new ArrayList<>(4); @@ -119,11 +123,18 @@ public class ResourceWebHandler implements WebHandler, InitializingBean { @Nullable private Map mediaTypes; - @Nullable - private ResourceLoader resourceLoader; - private boolean useLastModified = true; + private boolean optimizeLocations = false; + + + /** + * Provide the ResourceLoader to load {@link #setLocationValues location values} with. + * @since 5.1 + */ + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } /** * Accepts a list of String-based location values to be resolved into @@ -161,9 +172,9 @@ public void setLocations(@Nullable List locations) { *

Note that if {@link #setLocationValues(List) locationValues} are provided, * instead of loaded Resource-based locations, this method will return * empty until after initialization via {@link #afterPropertiesSet()}. - *

Note: As of 5.3.11 the list of locations is filtered - * to exclude those that don't actually exist and therefore the list returned - * from this method may be a subset of all given locations. + *

Note: As of 5.3.11 the list of locations may be filtered to + * exclude those that don't actually exist and therefore the list returned from this + * method may be a subset of all given locations. See {@link #setOptimizeLocations}. * @see #setLocationValues * @see #setLocations */ @@ -212,6 +223,22 @@ public List getResourceTransformers() { return this.resourceTransformers; } + /** + * Configure the {@link ResourceHttpMessageWriter} to use. + *

By default a {@link ResourceHttpMessageWriter} will be configured. + */ + public void setResourceHttpMessageWriter(@Nullable ResourceHttpMessageWriter httpMessageWriter) { + this.resourceHttpMessageWriter = httpMessageWriter; + } + + /** + * Return the configured resource message writer. + */ + @Nullable + public ResourceHttpMessageWriter getResourceHttpMessageWriter() { + return this.resourceHttpMessageWriter; + } + /** * Set the {@link org.springframework.http.CacheControl} instance to build * the Cache-Control HTTP response header. @@ -230,19 +257,48 @@ public CacheControl getCacheControl() { } /** - * Configure the {@link ResourceHttpMessageWriter} to use. - *

By default a {@link ResourceHttpMessageWriter} will be configured. + * Set whether we should look at the {@link Resource#lastModified()} + * when serving resources and use this information to drive {@code "Last-Modified"} + * HTTP response headers. + *

This option is enabled by default and should be turned off if the metadata of + * the static files should be ignored. + * @since 5.3 */ - public void setResourceHttpMessageWriter(@Nullable ResourceHttpMessageWriter httpMessageWriter) { - this.resourceHttpMessageWriter = httpMessageWriter; + public void setUseLastModified(boolean useLastModified) { + this.useLastModified = useLastModified; } /** - * Return the configured resource message writer. + * Return whether the {@link Resource#lastModified()} information is used + * to drive HTTP responses when serving static resources. + * @since 5.3 */ - @Nullable - public ResourceHttpMessageWriter getResourceHttpMessageWriter() { - return this.resourceHttpMessageWriter; + public boolean isUseLastModified() { + return this.useLastModified; + } + + /** + * Set whether to optimize the specified locations through an existence + * check on startup, filtering non-existing directories upfront so that + * they do not have to be checked on every resource access. + *

The default is {@code false}, for defensiveness against zip files + * without directory entries which are unable to expose the existence of + * a directory upfront. Switch this flag to {@code true} for optimized + * access in case of a consistent jar layout with directory entries. + * @since 5.3.13 + */ + public void setOptimizeLocations(boolean optimizeLocations) { + this.optimizeLocations = optimizeLocations; + } + + /** + * Return whether to optimize the specified locations through an existence + * check on startup, filtering non-existing directories upfront so that + * they do not have to be checked on every resource access. + * @since 5.3.13 + */ + public boolean isOptimizeLocations() { + return this.optimizeLocations; } /** @@ -269,36 +325,6 @@ public Map getMediaTypes() { return (this.mediaTypes != null ? this.mediaTypes : Collections.emptyMap()); } - /** - * Provide the ResourceLoader to load {@link #setLocationValues(List) - * location values} with. - * @since 5.1 - */ - public void setResourceLoader(ResourceLoader resourceLoader) { - this.resourceLoader = resourceLoader; - } - - /** - * Return whether the {@link Resource#lastModified()} information is used - * to drive HTTP responses when serving static resources. - * @since 5.3 - */ - public boolean isUseLastModified() { - return this.useLastModified; - } - - /** - * Set whether we should look at the {@link Resource#lastModified()} - * when serving resources and use this information to drive {@code "Last-Modified"} - * HTTP response headers. - *

This option is enabled by default and should be turned off if the metadata of - * the static files should be ignored. - * @param useLastModified whether to use the resource last-modified information. - * @since 5.3 - */ - public void setUseLastModified(boolean useLastModified) { - this.useLastModified = useLastModified; - } @Override public void afterPropertiesSet() throws Exception { @@ -332,7 +358,9 @@ private void resolveResourceLocations() { } } - result = result.stream().filter(Resource::exists).collect(Collectors.toList()); + if (isOptimizeLocations()) { + result = result.stream().filter(Resource::exists).collect(Collectors.toList()); + } this.locationsToUse.clear(); this.locationsToUse.addAll(result); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java index b668172197d6..c213b1672ac4 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java @@ -74,6 +74,7 @@ public class ResourceWebHandlerTests { private ResourceWebHandler handler; + @BeforeEach public void setup() throws Exception { List locations = new ArrayList<>(2); @@ -253,7 +254,7 @@ public void getResourceFromFileSystem() throws Exception { assertResponseBody(exchange, "h1 { color:red; }"); } - @Test // gh-27538 + @Test // gh-27538, gh-27624 public void filterNonExistingLocations() throws Exception { List inputLocations = Arrays.asList( new ClassPathResource("test/", getClass()), @@ -262,6 +263,7 @@ public void filterNonExistingLocations() throws Exception { ResourceWebHandler handler = new ResourceWebHandler(); handler.setLocations(inputLocations); + handler.setOptimizeLocations(true); handler.afterPropertiesSet(); List actual = handler.getLocations(); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java index fd822bedd55b..c173f602455e 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java @@ -55,6 +55,8 @@ public class ResourceHandlerRegistration { private boolean useLastModified = true; + private boolean optimizeLocations = false; + /** * Create a {@link ResourceHandlerRegistration} instance. @@ -130,15 +132,33 @@ public ResourceHandlerRegistration setCacheControl(CacheControl cacheControl) { /** * Set whether the {@link Resource#lastModified()} information should be used to drive HTTP responses. *

This configuration is set to {@code true} by default. - * @param useLastModified whether the "last modified" resource information should be used. + * @param useLastModified whether the "last modified" resource information should be used * @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation * @since 5.3 + * @see ResourceHttpRequestHandler#setUseLastModified */ public ResourceHandlerRegistration setUseLastModified(boolean useLastModified) { this.useLastModified = useLastModified; return this; } + /** + * Set whether to optimize the specified locations through an existence check on startup, + * filtering non-existing directories upfront so that they do not have to be checked + * on every resource access. + *

The default is {@code false}, for defensiveness against zip files without directory + * entries which are unable to expose the existence of a directory upfront. Switch this flag to + * {@code true} for optimized access in case of a consistent jar layout with directory entries. + * @param optimizeLocations whether to optimize the locations through an existence check on startup + * @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation + * @since 5.3.13 + * @see ResourceHttpRequestHandler#setOptimizeLocations + */ + public ResourceHandlerRegistration setOptimizeLocations(boolean optimizeLocations) { + this.optimizeLocations = optimizeLocations; + return this; + } + /** * Configure a chain of resource resolvers and transformers to use. This * can be useful, for example, to apply a version strategy to resource URLs. @@ -204,6 +224,7 @@ else if (this.cachePeriod != null) { handler.setCacheSeconds(this.cachePeriod); } handler.setUseLastModified(this.useLastModified); + handler.setOptimizeLocations(this.optimizeLocations); return handler; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java index 22f06ccf58d7..7855a1171cb3 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java @@ -140,11 +140,13 @@ public class ResourceHttpRequestHandler extends WebContentGenerator @Nullable private UrlPathHelper urlPathHelper; + private boolean useLastModified = true; + + private boolean optimizeLocations = false; + @Nullable private StringValueResolver embeddedValueResolver; - private boolean useLastModified = true; - public ResourceHttpRequestHandler() { super(HttpMethod.GET.name(), HttpMethod.HEAD.name()); @@ -185,13 +187,13 @@ public void setLocations(List locations) { /** * Return the configured {@code List} of {@code Resource} locations including * both String-based locations provided via - * {@link #setLocationValues(List) setLocationValues} and pre-resolved {@code Resource} - * locations provided via {@link #setLocations(List) setLocations}. + * {@link #setLocationValues(List) setLocationValues} and pre-resolved + * {@code Resource} locations provided via {@link #setLocations(List) setLocations}. *

Note that the returned list is fully initialized only after * initialization via {@link #afterPropertiesSet()}. - *

Note: As of 5.3.11 the list of locations is filtered - * to exclude those that don't actually exist and therefore the list returned - * from this method may be a subset of all given locations. + *

Note: As of 5.3.11 the list of locations may be filtered to + * exclude those that don't actually exist and therefore the list returned from this + * method may be a subset of all given locations. See {@link #setOptimizeLocations}. * @see #setLocationValues * @see #setLocations */ @@ -293,7 +295,7 @@ public void setContentNegotiationManager(@Nullable ContentNegotiationManager con /** * Return the configured content negotiation manager. * @since 4.3 - * @deprecated as of 5.2.4. + * @deprecated as of 5.2.4 */ @Nullable @Deprecated @@ -303,7 +305,7 @@ public ContentNegotiationManager getContentNegotiationManager() { /** * Add mappings between file extensions, extracted from the filename of a - * static {@link Resource}, and corresponding media type to set on the + * static {@link Resource}, and corresponding media type to set on the * response. *

Use of this method is typically not necessary since mappings are * otherwise determined via @@ -361,9 +363,16 @@ public UrlPathHelper getUrlPathHelper() { return this.urlPathHelper; } - @Override - public void setEmbeddedValueResolver(StringValueResolver resolver) { - this.embeddedValueResolver = resolver; + /** + * Set whether we should look at the {@link Resource#lastModified()} when + * serving resources and use this information to drive {@code "Last-Modified"} + * HTTP response headers. + *

This option is enabled by default and should be turned off if the metadata + * of the static files should be ignored. + * @since 5.3 + */ + public void setUseLastModified(boolean useLastModified) { + this.useLastModified = useLastModified; } /** @@ -376,18 +385,35 @@ public boolean isUseLastModified() { } /** - * Set whether we should look at the {@link Resource#lastModified()} - * when serving resources and use this information to drive {@code "Last-Modified"} - * HTTP response headers. - *

This option is enabled by default and should be turned off if the metadata of - * the static files should be ignored. - * @param useLastModified whether to use the resource last-modified information. - * @since 5.3 + * Set whether to optimize the specified locations through an existence + * check on startup, filtering non-existing directories upfront so that + * they do not have to be checked on every resource access. + *

The default is {@code false}, for defensiveness against zip files + * without directory entries which are unable to expose the existence of + * a directory upfront. Switch this flag to {@code true} for optimized + * access in case of a consistent jar layout with directory entries. + * @since 5.3.13 */ - public void setUseLastModified(boolean useLastModified) { - this.useLastModified = useLastModified; + public void setOptimizeLocations(boolean optimizeLocations) { + this.optimizeLocations = optimizeLocations; + } + + /** + * Return whether to optimize the specified locations through an existence + * check on startup, filtering non-existing directories upfront so that + * they do not have to be checked on every resource access. + * @since 5.3.13 + */ + public boolean isOptimizeLocations() { + return this.optimizeLocations; } + @Override + public void setEmbeddedValueResolver(StringValueResolver resolver) { + this.embeddedValueResolver = resolver; + } + + @Override public void afterPropertiesSet() throws Exception { resolveResourceLocations(); @@ -449,8 +475,8 @@ private void resolveResourceLocations() { if (location.equals("/") && !(resource instanceof ServletContextResource)) { throw new IllegalStateException( "The String-based location \"/\" should be relative to the web application root " + - "but resolved to a Resource of type: " + resource.getClass() + ". " + - "If this is intentional, please pass it as a pre-configured Resource via setLocations."); + "but resolved to a Resource of type: " + resource.getClass() + ". " + + "If this is intentional, please pass it as a pre-configured Resource via setLocations."); } result.add(resource); if (charset != null) { @@ -463,7 +489,9 @@ private void resolveResourceLocations() { } result.addAll(this.locationResources); - result = result.stream().filter(Resource::exists).collect(Collectors.toList()); + if (isOptimizeLocations()) { + result = result.stream().filter(Resource::exists).collect(Collectors.toList()); + } this.locationsToUse.clear(); this.locationsToUse.addAll(result); @@ -508,6 +536,7 @@ protected org.springframework.web.accept.PathExtensionContentNegotiationStrategy return null; } + /** * Processes a resource request. *

Checks for the existence of the requested resource in the configured list of locations. diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java index 9aefd7f9d07b..834aab2694a6 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java @@ -311,7 +311,7 @@ public String getMimeType(String filePath) { assertThat(this.response.getContentAsString()).isEqualTo("h1 { color:red; }"); } - @Test // gh-27538 + @Test // gh-27538, gh-27624 public void filterNonExistingLocations() throws Exception { List inputLocations = Arrays.asList( new ClassPathResource("test/", getClass()), @@ -321,6 +321,7 @@ public void filterNonExistingLocations() throws Exception { ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler(); handler.setServletContext(new MockServletContext()); handler.setLocations(inputLocations); + handler.setOptimizeLocations(true); handler.afterPropertiesSet(); List actual = handler.getLocations(); diff --git a/src/docs/asciidoc/web/webflux.adoc b/src/docs/asciidoc/web/webflux.adoc index 3b601ea4c885..1cc302102126 100644 --- a/src/docs/asciidoc/web/webflux.adoc +++ b/src/docs/asciidoc/web/webflux.adoc @@ -4170,8 +4170,8 @@ the example: @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/resources/**") - .addResourceLocations("/public", "classpath:/static/") - .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)); + .addResourceLocations("/public", "classpath:/static/") + .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)); } } @@ -4259,6 +4259,9 @@ re-write URLs to include the version of the jar and can also match against incom without versions -- for example, from `/jquery/jquery.min.js` to `/jquery/1.2.0/jquery.min.js`. +TIP: The Java configuration based on `ResourceHandlerRegistry` provides further options +for fine-grained control, e.g. last-modified behavior and optimized resource resolution. + [[webflux-config-path-matching]] diff --git a/src/docs/asciidoc/web/webmvc.adoc b/src/docs/asciidoc/web/webmvc.adoc index b49666303e98..33b49bef56a5 100644 --- a/src/docs/asciidoc/web/webmvc.adoc +++ b/src/docs/asciidoc/web/webmvc.adoc @@ -5738,8 +5738,8 @@ The following listing shows how to do so with Java configuration: @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/resources/**") - .addResourceLocations("/public", "classpath:/static/") - .setCacheControl(CacheControl.maxAge(Duration.ofDays(365))); + .addResourceLocations("/public", "classpath:/static/") + .setCacheControl(CacheControl.maxAge(Duration.ofDays(365))); } } ---- @@ -5846,6 +5846,9 @@ re-write URLs to include the version of the jar and can also match against incom without versions -- for example, from `/jquery/jquery.min.js` to `/jquery/1.2.0/jquery.min.js`. +TIP: The Java configuration based on `ResourceHandlerRegistry` provides further options +for fine-grained control, e.g. last-modified behavior and optimized resource resolution. + [[mvc-default-servlet-handler]] From 15a6373fed1b8acf6dcda7cc2a144de34e32f4cd Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 10 Nov 2021 14:22:06 +0100 Subject: [PATCH 35/37] Avoid nullability warnings --- .../test/web/reactive/server/CookieAssertions.java | 7 ++++--- .../test/web/reactive/server/HeaderAssertions.java | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/CookieAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/CookieAssertions.java index 26b28b486a41..f9804915f72b 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/CookieAssertions.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/CookieAssertions.java @@ -17,6 +17,7 @@ package org.springframework.test.web.reactive.server; import java.time.Duration; +import java.util.Objects; import java.util.function.Consumer; import org.hamcrest.Matcher; @@ -210,10 +211,10 @@ public WebTestClient.ResponseSpec sameSite(String name, String expected) { private ResponseCookie getCookie(String name) { ResponseCookie cookie = this.exchangeResult.getResponseCookies().getFirst(name); if (cookie == null) { - String message = "No cookie with name '" + name + "'"; - this.exchangeResult.assertWithDiagnostics(() -> AssertionErrors.fail(message)); + this.exchangeResult.assertWithDiagnostics(() -> + AssertionErrors.fail("No cookie with name '" + name + "'")); } - return cookie; + return Objects.requireNonNull(cookie); } private String getMessage(String cookie) { diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java index aa20066960d0..c3750d27c3c4 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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,6 +19,7 @@ import java.net.URI; import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.function.Consumer; import org.hamcrest.Matcher; @@ -73,7 +74,7 @@ public WebTestClient.ResponseSpec valueEquals(String headerName, long value) { String actual = getHeaders().getFirst(headerName); this.exchangeResult.assertWithDiagnostics(() -> assertTrue("Response does not contain header '" + headerName + "'", actual != null)); - return assertHeader(headerName, value, Long.parseLong(actual)); + return assertHeader(headerName, value, Long.parseLong(Objects.requireNonNull(actual))); } /** @@ -203,7 +204,7 @@ private List getRequiredValues(String name) { this.exchangeResult.assertWithDiagnostics(() -> AssertionErrors.fail(getMessage(name) + " not found")); } - return values; + return Objects.requireNonNull(values); } /** From 9197f15a306a497b3aa10c6ed48c581b3e938f58 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 10 Nov 2021 16:23:41 +0100 Subject: [PATCH 36/37] Use ByteArrayDecoder in DefaultClientResponse::createException This commit changes DefaultClientResponse::createException to use the ByteArrayDecoder, instead of converting to DataBuffers and turning these into a byte array. Closes gh-27666 --- .../client/DefaultClientResponse.java | 8 +----- .../client/DefaultClientResponseTests.java | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java index 0931bf214072..37f14f34ae0b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java @@ -195,13 +195,7 @@ public Mono>> toEntityList(ParameterizedTypeReference @Override public Mono createException() { - return DataBufferUtils.join(body(BodyExtractors.toDataBuffers())) - .map(dataBuffer -> { - byte[] bytes = new byte[dataBuffer.readableByteCount()]; - dataBuffer.read(bytes); - DataBufferUtils.release(dataBuffer); - return bytes; - }) + return bodyToMono(byte[].class) .defaultIfEmpty(EMPTY) .onErrorReturn(ex -> !(ex instanceof Error), EMPTY) .map(bodyBytes -> { diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseTests.java index 0b21511feab1..f9b626a60c79 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseTests.java @@ -30,6 +30,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.codec.ByteArrayDecoder; import org.springframework.core.codec.StringDecoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DefaultDataBuffer; @@ -48,6 +49,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.entry; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.springframework.web.reactive.function.BodyExtractors.toMono; @@ -328,6 +330,29 @@ public void toEntityListTypeReference() { assertThat(result.getHeaders().getContentType()).isEqualTo(MediaType.TEXT_PLAIN); } + @Test + public void createException() { + byte[] bytes = "foo".getBytes(StandardCharsets.UTF_8); + DefaultDataBuffer dataBuffer = DefaultDataBufferFactory.sharedInstance.wrap(ByteBuffer.wrap(bytes)); + Flux body = Flux.just(dataBuffer); + httpHeaders.setContentType(MediaType.TEXT_PLAIN); + given(mockResponse.getStatusCode()).willReturn(HttpStatus.NOT_FOUND); + given(mockResponse.getRawStatusCode()).willReturn(HttpStatus.NOT_FOUND.value()); + given(mockResponse.getBody()).willReturn(body); + + List> messageReaders = Collections.singletonList( + new DecoderHttpMessageReader<>(new ByteArrayDecoder())); + given(mockExchangeStrategies.messageReaders()).willReturn(messageReaders); + + Mono resultMono = defaultClientResponse.createException(); + WebClientResponseException exception = resultMono.block(); + assertThat(exception.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + assertThat(exception.getMessage()).isEqualTo("404 Not Found"); + assertThat(exception.getHeaders()).containsExactly(entry("Content-Type", + Collections.singletonList("text/plain"))); + assertThat(exception.getResponseBodyAsByteArray()).isEqualTo(bytes); + } + private void mockTextPlainResponse(Flux body) { httpHeaders.setContentType(MediaType.TEXT_PLAIN); From 23bb745ea49ae3d5ae58638e465c54a3595c5f78 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Thu, 11 Nov 2021 07:31:25 +0000 Subject: [PATCH 37/37] Release v5.3.13 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index b46dbd487055..de2ed3c71aba 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=5.3.13-SNAPSHOT +version=5.3.13 org.gradle.jvmargs=-Xmx1536M org.gradle.caching=true org.gradle.parallel=true