From 60337738d8eef90973f070ecce371b6149b7265a Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Thu, 13 Jul 2023 07:54:12 +0000 Subject: [PATCH 01/47] Next development version (v5.3.30-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 6089d4d958d3..61922c8fd00f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=5.3.29-SNAPSHOT +version=5.3.30-SNAPSHOT org.gradle.jvmargs=-Xmx2048m org.gradle.caching=true org.gradle.parallel=true From 355fa258bd2d98fb34e41450baaa657ef49fe0e9 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 14 Jul 2023 14:52:15 +0200 Subject: [PATCH 02/47] Polishing --- .../scheduling/annotation/Scheduled.java | 10 ++++++++-- .../support/GenericConversionService.java | 5 ++--- .../springframework/jdbc/core/JdbcTemplate.java | 14 ++++---------- .../AbstractFallbackSQLExceptionTranslator.java | 9 ++++++--- .../SQLErrorCodeSQLExceptionTranslator.java | 17 +++++++---------- 5 files changed, 27 insertions(+), 28 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java index 983d341e7823..319165d2bec0 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java @@ -28,8 +28,8 @@ /** * Annotation that marks a method to be scheduled. Exactly one of the - * {@link #cron}, {@link #fixedDelay}, or {@link #fixedRate} attributes must be - * specified. + * {@link #cron}, {@link #fixedDelay}, or {@link #fixedRate} attributes + * must be specified. * *

The annotated method must expect no arguments. It will typically have * a {@code void} return type; if not, the returned value will be ignored @@ -40,6 +40,12 @@ * done manually or, more conveniently, through the {@code } * XML element or {@link EnableScheduling @EnableScheduling} annotation. * + *

This annotation can be used as a {@linkplain Repeatable repeatable} + * annotation. If several scheduled declarations are found on the same method, + * each of them will be processed independently, with a separate trigger firing + * for each of them. As a consequence, such co-located schedules may overlap + * and execute multiple times in parallel or in immediate succession. + * *

This annotation may be used as a meta-annotation to create custom * composed annotations with attribute overrides. * diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java b/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java index 6065d4685247..5cff48af2101 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -205,8 +205,7 @@ public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceTy * @param targetType the target type * @return the converted value * @throws ConversionException if a conversion exception occurred - * @throws IllegalArgumentException if targetType is {@code null}, - * or sourceType is {@code null} but source is not {@code null} + * @throws IllegalArgumentException if targetType is {@code null} */ @Nullable public Object convert(@Nullable Object source, TypeDescriptor targetType) { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index 225ab2b12235..eb0fb2475bca 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -445,9 +445,7 @@ public T query(final String sql, final ResultSetExtractor rse) throws Dat logger.debug("Executing SQL query [" + sql + "]"); } - /** - * Callback to execute the query. - */ + // Callback to execute the query. class QueryStatementCallback implements StatementCallback, SqlProvider { @Override @Nullable @@ -542,9 +540,7 @@ public int update(final String sql) throws DataAccessException { logger.debug("Executing SQL update [" + sql + "]"); } - /** - * Callback to execute the update statement. - */ + // Callback to execute the update statement. class UpdateStatementCallback implements StatementCallback, SqlProvider { @Override public Integer doInStatement(Statement stmt) throws SQLException { @@ -570,9 +566,7 @@ public int[] batchUpdate(final String... sql) throws DataAccessException { logger.debug("Executing SQL batch update of " + sql.length + " statements"); } - /** - * Callback to execute the batch update. - */ + // Callback to execute the batch update. class BatchUpdateStatementCallback implements StatementCallback, SqlProvider { @Nullable @@ -1376,7 +1370,7 @@ protected Map extractOutputParameters(CallableStatement cs, List } } } - if (!(param.isResultsParameter())) { + if (!param.isResultsParameter()) { sqlColIndex++; } } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java index 44b8838b2101..f1546734ac16 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,8 @@ * * @author Juergen Hoeller * @since 2.5.6 + * @see #doTranslate + * @see #setFallbackTranslator */ public abstract class AbstractFallbackSQLExceptionTranslator implements SQLExceptionTranslator { @@ -42,8 +44,8 @@ public abstract class AbstractFallbackSQLExceptionTranslator implements SQLExcep /** - * Override the default SQL state fallback translator - * (typically a {@link SQLStateSQLExceptionTranslator}). + * Set the fallback translator to use when this translator cannot find a + * specific match itself. */ public void setFallbackTranslator(@Nullable SQLExceptionTranslator fallback) { this.fallbackTranslator = fallback; @@ -51,6 +53,7 @@ public void setFallbackTranslator(@Nullable SQLExceptionTranslator fallback) { /** * Return the fallback exception translator, if any. + * @see #setFallbackTranslator */ @Nullable public SQLExceptionTranslator getFallbackTranslator() { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java index 80f89796049d..c4ba08dbfec3 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -75,8 +75,6 @@ public class SQLErrorCodeSQLExceptionTranslator extends AbstractFallbackSQLExcep private static final int MESSAGE_SQL_THROWABLE_CONSTRUCTOR = 4; private static final int MESSAGE_SQL_SQLEX_CONSTRUCTOR = 5; - - /** Error codes used by this translator. */ @Nullable private SingletonSupplier sqlErrorCodes; @@ -194,9 +192,9 @@ protected DataAccessException doTranslate(String task, @Nullable String sql, SQL if (sqlErrorCodes != null) { SQLExceptionTranslator customTranslator = sqlErrorCodes.getCustomSqlExceptionTranslator(); if (customTranslator != null) { - DataAccessException customDex = customTranslator.translate(task, sql, sqlEx); - if (customDex != null) { - return customDex; + dae = customTranslator.translate(task, sql, sqlEx); + if (dae != null) { + return dae; } } } @@ -224,11 +222,10 @@ protected DataAccessException doTranslate(String task, @Nullable String sql, SQL for (CustomSQLErrorCodesTranslation customTranslation : customTranslations) { if (Arrays.binarySearch(customTranslation.getErrorCodes(), errorCode) >= 0 && customTranslation.getExceptionClass() != null) { - DataAccessException customException = createCustomException( - task, sql, sqlEx, customTranslation.getExceptionClass()); - if (customException != null) { + dae = createCustomException(task, sql, sqlEx, customTranslation.getExceptionClass()); + if (dae != null) { logTranslation(task, sql, sqlEx, true); - return customException; + return dae; } } } From 6879be7508c778252e4f50ec1e02b057f44d4fac Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 14 Jul 2023 16:33:22 +0200 Subject: [PATCH 03/47] Explicit hints for @PostConstruct methods (preventing deadlocks) Closes gh-25074 --- src/docs/asciidoc/core/core-beans.adoc | 70 ++++++++++++++++++-------- 1 file changed, 50 insertions(+), 20 deletions(-) diff --git a/src/docs/asciidoc/core/core-beans.adoc b/src/docs/asciidoc/core/core-beans.adoc index 4df6bbf5210a..5ba8c60b9451 100644 --- a/src/docs/asciidoc/core/core-beans.adoc +++ b/src/docs/asciidoc/core/core-beans.adoc @@ -3495,6 +3495,29 @@ The preceding example has almost exactly the same effect as the following exampl However, the first of the two preceding examples does not couple the code to Spring. +[NOTE] +==== +Be aware that `@PostConstruct` and initialization methods in general are executed +within the container's singleton creation lock. The bean instance is only considered +as fully initialized and ready to be published to others after returning from the +`@PostConstruct` method. Such individual initialization methods are only meant +for validating the configuration state and possibly preparing some data structures +based on the given configuration but no further activity with external bean access. +Otherwise there is a risk for an initialization deadlock. + +For a scenario where expensive post-initialization activity is to be triggered, +e.g. asynchronous database preparation steps, your bean should either implement +`SmartInitializingSingleton.afterSingletonsInstantiated()` or rely on the context +refresh event: implementing `ApplicationListener` or +declaring its annotation equivalent `@EventListener(ContextRefreshedEvent.class)`. +Those variants come after all regular singleton initialization and therefore +outside of any singleton creation lock. + +Alternatively, you may implement the `(Smart)Lifecycle` interface and integrate with +the container's overall lifecycle management, including an auto-startup mechanism, +a pre-destroy stop step, and potential stop/restart callbacks (see below). +==== + [[beans-factory-lifecycle-disposablebean]] ==== Destruction Callbacks @@ -3574,28 +3597,37 @@ The preceding definition has almost exactly the same effect as the following def However, the first of the two preceding definitions does not couple the code to Spring. TIP: You can assign the `destroy-method` attribute of a `` element a special -`(inferred)` value, which instructs Spring to automatically detect a public `close` or -`shutdown` method on the specific bean class. (Any class that implements -`java.lang.AutoCloseable` or `java.io.Closeable` would therefore match.) You can also set -this special `(inferred)` value on the `default-destroy-method` attribute of a +`(inferred)` value, which instructs Spring to automatically detect a public `close` +or `shutdown` method on the specific bean class. (Any class that implements +`java.lang.AutoCloseable` or `java.io.Closeable` would therefore match.) You can also +set this special `(inferred)` value on the `default-destroy-method` attribute of a `` element to apply this behavior to an entire set of beans (see <>). Note that this is the -default behavior with Java configuration. +default behavior for `@Bean` methods in Java configuration classes. + +[NOTE] +==== +For extended shutdown phases, you may implement the `Lifecycle` interface and receive +an early stop signal before the destroy methods of any singleton beans are called. +You may also implement `SmartLifecycle` for a time-bound stop step where the container +will wait for all such stop processing to complete before moving on to destroy methods. +==== + [[beans-factory-lifecycle-default-init-destroy-methods]] ==== Default Initialization and Destroy Methods When you write initialization and destroy method callbacks that do not use the Spring-specific `InitializingBean` and `DisposableBean` callback interfaces, you -typically write methods with names such as `init()`, `initialize()`, `dispose()`, and so -on. Ideally, the names of such lifecycle callback methods are standardized across a -project so that all developers use the same method names and ensure consistency. +typically write methods with names such as `init()`, `initialize()`, `dispose()`, +and so on. Ideally, the names of such lifecycle callback methods are standardized across +a project so that all developers use the same method names and ensure consistency. You can configure the Spring container to "`look`" for named initialization and destroy -callback method names on every bean. This means that you, as an application -developer, can write your application classes and use an initialization callback called -`init()`, without having to configure an `init-method="init"` attribute with each bean -definition. The Spring IoC container calls that method when the bean is created (and in +callback method names on every bean. This means that you, as an application developer, +can write your application classes and use an initialization callback called `init()`, +without having to configure an `init-method="init"` attribute with each bean definition. +The Spring IoC container calls that method when the bean is created (and in accordance with the standard lifecycle callback contract <>). This feature also enforces a consistent naming convention for initialization and destroy method callbacks. @@ -3677,7 +3709,6 @@ target bean to its proxy or interceptors and leave strange semantics when your c interacts directly with the raw target bean. - [[beans-factory-lifecycle-combined-effects]] ==== Combining Lifecycle Mechanisms @@ -3710,7 +3741,6 @@ Destroy methods are called in the same order: . A custom configured `destroy()` method - [[beans-factory-lifecycle-processor]] ==== Startup and Shutdown Callbacks @@ -3752,14 +3782,15 @@ and closed. [TIP] ==== Note that the regular `org.springframework.context.Lifecycle` interface is a plain -contract for explicit start and stop notifications and does not imply auto-startup at context -refresh time. For fine-grained control over auto-startup of a specific bean (including startup phases), -consider implementing `org.springframework.context.SmartLifecycle` instead. +contract for explicit start and stop notifications and does not imply auto-startup +at context refresh time. For fine-grained control over auto-startup and for graceful +stopping of a specific bean (including startup and stop phases), consider implementing +the extended `org.springframework.context.SmartLifecycle` interface instead. Also, please note that stop notifications are not guaranteed to come before destruction. On regular shutdown, all `Lifecycle` beans first receive a stop notification before -the general destruction callbacks are being propagated. However, on hot refresh during a -context's lifetime or on stopped refresh attempts, only destroy methods are called. +the general destruction callbacks are being propagated. However, on hot refresh during +a context's lifetime or on stopped refresh attempts, only destroy methods are called. ==== The order of startup and shutdown invocations can be important. If a "`depends-on`" @@ -3833,7 +3864,6 @@ automatically for a standard context implementation). The `phase` value and any "`depends-on`" relationships determine the startup order as described earlier. - [[beans-factory-shutdown]] ==== Shutting Down the Spring IoC Container Gracefully in Non-Web Applications From 0b4b313baeddb4edee26d0678f040f0696c4873e Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sat, 15 Jul 2023 14:17:52 +0200 Subject: [PATCH 04/47] Cache DependencyDescriptor per autowired constructor argument Aligned with shortcut handling in AutowiredAnnotationBeanPostProcessor. Includes minor MethodInvoker optimization for pre-resolved targetClass. Closes gh-30883 (cherry picked from commit 6183f0684684912802021556dce916ba26228c26) --- .../AutowiredAnnotationBeanPostProcessor.java | 24 ++- .../AbstractAutowireCapableBeanFactory.java | 4 +- .../support/BeanDefinitionValueResolver.java | 4 +- .../factory/support/ConstructorResolver.java | 159 +++++++++++++----- ...wiredAnnotationBeanPostProcessorTests.java | 150 ++++++++++++++++- .../springframework/util/MethodInvoker.java | 12 +- 6 files changed, 283 insertions(+), 70 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 d77880c012c6..4e0f365bc85c 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 @@ -651,7 +651,7 @@ protected void inject(Object bean, @Nullable String beanName, @Nullable Property private Object resolveFieldValue(Field field, Object bean, @Nullable String beanName) { DependencyDescriptor desc = new DependencyDescriptor(field, this.required); desc.setContainingClass(bean.getClass()); - Set autowiredBeanNames = new LinkedHashSet<>(1); + Set autowiredBeanNames = new LinkedHashSet<>(2); Assert.state(beanFactory != null, "No BeanFactory available"); TypeConverter typeConverter = beanFactory.getTypeConverter(); Object value; @@ -670,8 +670,7 @@ private Object resolveFieldValue(Field field, Object bean, @Nullable String bean String autowiredBeanName = autowiredBeanNames.iterator().next(); if (beanFactory.containsBean(autowiredBeanName) && beanFactory.isTypeMatch(autowiredBeanName, field.getType())) { - cachedFieldValue = new ShortcutDependencyDescriptor( - desc, autowiredBeanName, field.getType()); + cachedFieldValue = new ShortcutDependencyDescriptor(desc, autowiredBeanName); } } this.cachedFieldValue = cachedFieldValue; @@ -754,7 +753,7 @@ private Object[] resolveMethodArguments(Method method, Object bean, @Nullable St int argumentCount = method.getParameterCount(); Object[] arguments = new Object[argumentCount]; DependencyDescriptor[] descriptors = new DependencyDescriptor[argumentCount]; - Set autowiredBeans = new LinkedHashSet<>(argumentCount); + Set autowiredBeanNames = new LinkedHashSet<>(argumentCount * 2); Assert.state(beanFactory != null, "No BeanFactory available"); TypeConverter typeConverter = beanFactory.getTypeConverter(); for (int i = 0; i < arguments.length; i++) { @@ -763,7 +762,7 @@ private Object[] resolveMethodArguments(Method method, Object bean, @Nullable St currDesc.setContainingClass(bean.getClass()); descriptors[i] = currDesc; try { - Object arg = beanFactory.resolveDependency(currDesc, beanName, autowiredBeans, typeConverter); + Object arg = beanFactory.resolveDependency(currDesc, beanName, autowiredBeanNames, typeConverter); if (arg == null && !this.required) { arguments = null; break; @@ -778,16 +777,16 @@ private Object[] resolveMethodArguments(Method method, Object bean, @Nullable St if (!this.cached) { if (arguments != null) { DependencyDescriptor[] cachedMethodArguments = Arrays.copyOf(descriptors, argumentCount); - registerDependentBeans(beanName, autowiredBeans); - if (autowiredBeans.size() == argumentCount) { - Iterator it = autowiredBeans.iterator(); + registerDependentBeans(beanName, autowiredBeanNames); + if (autowiredBeanNames.size() == argumentCount) { + Iterator it = autowiredBeanNames.iterator(); Class[] paramTypes = method.getParameterTypes(); for (int i = 0; i < paramTypes.length; i++) { String autowiredBeanName = it.next(); if (arguments[i] != null && beanFactory.containsBean(autowiredBeanName) && beanFactory.isTypeMatch(autowiredBeanName, paramTypes[i])) { cachedMethodArguments[i] = new ShortcutDependencyDescriptor( - descriptors[i], autowiredBeanName, paramTypes[i]); + descriptors[i], autowiredBeanName); } } } @@ -813,17 +812,14 @@ private static class ShortcutDependencyDescriptor extends DependencyDescriptor { private final String shortcut; - private final Class requiredType; - - public ShortcutDependencyDescriptor(DependencyDescriptor original, String shortcut, Class requiredType) { + public ShortcutDependencyDescriptor(DependencyDescriptor original, String shortcut) { super(original); this.shortcut = shortcut; - this.requiredType = requiredType; } @Override public Object resolveShortcut(BeanFactory beanFactory) { - return beanFactory.getBean(this.shortcut, this.requiredType); + return beanFactory.getBean(this.shortcut, getDependencyType()); } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java index fcb2ef67e36a..fb0ba14b0be4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -1504,8 +1504,8 @@ protected void autowireByType( converter = bw; } - Set autowiredBeanNames = new LinkedHashSet<>(4); String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw); + Set autowiredBeanNames = new LinkedHashSet<>(propertyNames.length * 2); for (String propertyName : propertyNames) { try { PropertyDescriptor pd = bw.getPropertyDescriptor(propertyName); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java index be9667b19a38..a15ebb5a1ab4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -134,7 +134,7 @@ else if (value instanceof BeanDefinition) { return resolveInnerBean(argName, innerBeanName, bd); } else if (value instanceof DependencyDescriptor) { - Set autowiredBeanNames = new LinkedHashSet<>(4); + Set autowiredBeanNames = new LinkedHashSet<>(2); Object result = this.beanFactory.resolveDependency( (DependencyDescriptor) value, this.beanName, autowiredBeanNames, this.typeConverter); for (String autowiredBeanName : autowiredBeanNames) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java index dd268fc517fe..b1ce9e61faf1 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,6 +45,7 @@ import org.springframework.beans.TypeMismatchException; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.InjectionPoint; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; @@ -85,12 +86,6 @@ class ConstructorResolver { private static final Object[] EMPTY_ARGS = new Object[0]; - /** - * Marker for autowired arguments in a cached argument array, to be replaced - * by a {@linkplain #resolveAutowiredArgument resolved autowired argument}. - */ - private static final Object autowiredArgumentMarker = new Object(); - private static final NamedThreadLocal currentInjectionPoint = new NamedThreadLocal<>("Current injection point"); @@ -729,7 +724,7 @@ private ArgumentsHolder createArgumentArray( ArgumentsHolder args = new ArgumentsHolder(paramTypes.length); Set usedValueHolders = new HashSet<>(paramTypes.length); - Set autowiredBeanNames = new LinkedHashSet<>(4); + Set allAutowiredBeanNames = new LinkedHashSet<>(paramTypes.length * 2); for (int paramIndex = 0; paramIndex < paramTypes.length; paramIndex++) { Class paramType = paramTypes[paramIndex]; @@ -764,8 +759,8 @@ private ArgumentsHolder createArgumentArray( throw new UnsatisfiedDependencyException( mbd.getResourceDescription(), beanName, new InjectionPoint(methodParam), "Could not convert argument value of type [" + - ObjectUtils.nullSafeClassName(valueHolder.getValue()) + - "] to required type [" + paramType.getName() + "]: " + ex.getMessage()); + ObjectUtils.nullSafeClassName(valueHolder.getValue()) + + "] to required type [" + paramType.getName() + "]: " + ex.getMessage()); } Object sourceHolder = valueHolder.getSource(); if (sourceHolder instanceof ConstructorArgumentValues.ValueHolder) { @@ -788,11 +783,17 @@ private ArgumentsHolder createArgumentArray( "] - did you specify the correct bean references as arguments?"); } try { - Object autowiredArgument = resolveAutowiredArgument( - methodParam, beanName, autowiredBeanNames, converter, fallback); - args.rawArguments[paramIndex] = autowiredArgument; - args.arguments[paramIndex] = autowiredArgument; - args.preparedArguments[paramIndex] = autowiredArgumentMarker; + ConstructorDependencyDescriptor desc = new ConstructorDependencyDescriptor(methodParam, true); + Set autowiredBeanNames = new LinkedHashSet<>(2); + Object arg = resolveAutowiredArgument( + desc, paramType, beanName, autowiredBeanNames, converter, fallback); + if (arg != null) { + setShortcutIfPossible(desc, paramType, autowiredBeanNames); + } + allAutowiredBeanNames.addAll(autowiredBeanNames); + args.rawArguments[paramIndex] = arg; + args.arguments[paramIndex] = arg; + args.preparedArguments[paramIndex] = desc; args.resolveNecessary = true; } catch (BeansException ex) { @@ -802,14 +803,7 @@ private ArgumentsHolder createArgumentArray( } } - for (String autowiredBeanName : autowiredBeanNames) { - this.beanFactory.registerDependentBean(autowiredBeanName, beanName); - if (logger.isDebugEnabled()) { - logger.debug("Autowiring by type from bean name '" + beanName + - "' via " + (executable instanceof Constructor ? "constructor" : "factory method") + - " to bean named '" + autowiredBeanName + "'"); - } - } + registerDependentBeans(executable, beanName, allAutowiredBeanNames); return args; } @@ -829,31 +823,57 @@ private Object[] resolvePreparedArguments(String beanName, RootBeanDefinition mb Object[] resolvedArgs = new Object[argsToResolve.length]; for (int argIndex = 0; argIndex < argsToResolve.length; argIndex++) { Object argValue = argsToResolve[argIndex]; - MethodParameter methodParam = MethodParameter.forExecutable(executable, argIndex); - if (argValue == autowiredArgumentMarker) { - argValue = resolveAutowiredArgument(methodParam, beanName, null, converter, true); + Class paramType = paramTypes[argIndex]; + boolean convertNecessary = false; + if (argValue instanceof ConstructorDependencyDescriptor) { + ConstructorDependencyDescriptor descriptor = (ConstructorDependencyDescriptor) argValue; + try { + argValue = resolveAutowiredArgument(descriptor, paramType, beanName, + null, converter, true); + } + catch (BeansException ex) { + // Unexpected target bean mismatch for cached argument -> re-resolve + synchronized (descriptor) { + if (!descriptor.hasShortcut()) { + throw ex; + } + descriptor.setShortcut(null); + Set autowiredBeanNames = new LinkedHashSet<>(2); + argValue = resolveAutowiredArgument(descriptor, paramType, beanName, + autowiredBeanNames, converter, true); + if (argValue != null) { + setShortcutIfPossible(descriptor, paramType, autowiredBeanNames); + } + registerDependentBeans(executable, beanName, autowiredBeanNames); + } + } } else if (argValue instanceof BeanMetadataElement) { argValue = valueResolver.resolveValueIfNecessary("constructor argument", argValue); + convertNecessary = true; } else if (argValue instanceof String) { argValue = this.beanFactory.evaluateBeanDefinitionString((String) argValue, mbd); + convertNecessary = true; } - Class paramType = paramTypes[argIndex]; - try { - resolvedArgs[argIndex] = converter.convertIfNecessary(argValue, paramType, methodParam); - } - catch (TypeMismatchException ex) { - throw new UnsatisfiedDependencyException( - mbd.getResourceDescription(), beanName, new InjectionPoint(methodParam), - "Could not convert argument value of type [" + ObjectUtils.nullSafeClassName(argValue) + - "] to required type [" + paramType.getName() + "]: " + ex.getMessage()); + if (convertNecessary) { + MethodParameter methodParam = MethodParameter.forExecutable(executable, argIndex); + try { + argValue = converter.convertIfNecessary(argValue, paramType, methodParam); + } + catch (TypeMismatchException ex) { + throw new UnsatisfiedDependencyException( + mbd.getResourceDescription(), beanName, new InjectionPoint(methodParam), + "Could not convert argument value of type [" + ObjectUtils.nullSafeClassName(argValue) + + "] to required type [" + paramType.getName() + "]: " + ex.getMessage()); + } } + resolvedArgs[argIndex] = argValue; } return resolvedArgs; } - protected Constructor getUserDeclaredConstructor(Constructor constructor) { + private Constructor getUserDeclaredConstructor(Constructor constructor) { Class declaringClass = constructor.getDeclaringClass(); Class userClass = ClassUtils.getUserClass(declaringClass); if (userClass != declaringClass) { @@ -869,23 +889,22 @@ protected Constructor getUserDeclaredConstructor(Constructor constructor) } /** - * Template method for resolving the specified argument which is supposed to be autowired. + * Resolve the specified argument which is supposed to be autowired. */ @Nullable - protected Object resolveAutowiredArgument(MethodParameter param, String beanName, + Object resolveAutowiredArgument(DependencyDescriptor descriptor, Class paramType, String beanName, @Nullable Set autowiredBeanNames, TypeConverter typeConverter, boolean fallback) { - Class paramType = param.getParameterType(); if (InjectionPoint.class.isAssignableFrom(paramType)) { InjectionPoint injectionPoint = currentInjectionPoint.get(); if (injectionPoint == null) { - throw new IllegalStateException("No current InjectionPoint available for " + param); + throw new IllegalStateException("No current InjectionPoint available for " + descriptor); } return injectionPoint; } + try { - return this.beanFactory.resolveDependency( - new DependencyDescriptor(param, true), beanName, autowiredBeanNames, typeConverter); + return this.beanFactory.resolveDependency(descriptor, beanName, autowiredBeanNames, typeConverter); } catch (NoUniqueBeanDefinitionException ex) { throw ex; @@ -908,6 +927,31 @@ else if (CollectionFactory.isApproximableMapType(paramType)) { } } + private void setShortcutIfPossible( + ConstructorDependencyDescriptor descriptor, Class paramType, Set autowiredBeanNames) { + + if (autowiredBeanNames.size() == 1) { + String autowiredBeanName = autowiredBeanNames.iterator().next(); + if (this.beanFactory.containsBean(autowiredBeanName) && + this.beanFactory.isTypeMatch(autowiredBeanName, paramType)) { + descriptor.setShortcut(autowiredBeanName); + } + } + } + + private void registerDependentBeans( + Executable executable, String beanName, Set autowiredBeanNames) { + + for (String autowiredBeanName : autowiredBeanNames) { + this.beanFactory.registerDependentBean(autowiredBeanName, beanName); + if (logger.isDebugEnabled()) { + logger.debug("Autowiring by type from bean name '" + beanName + "' via " + + (executable instanceof Constructor ? "constructor" : "factory method") + + " to bean named '" + autowiredBeanName + "'"); + } + } + } + static InjectionPoint setCurrentInjectionPoint(@Nullable InjectionPoint injectionPoint) { InjectionPoint old = currentInjectionPoint.get(); if (injectionPoint != null) { @@ -1006,4 +1050,35 @@ public static String[] evaluate(Constructor candidate, int paramCount) { } } + + /** + * DependencyDescriptor marker for constructor arguments, + * for differentiating between a provided DependencyDescriptor instance + * and an internally built DependencyDescriptor for autowiring purposes. + */ + @SuppressWarnings("serial") + private static class ConstructorDependencyDescriptor extends DependencyDescriptor { + + @Nullable + private volatile String shortcut; + + public ConstructorDependencyDescriptor(MethodParameter methodParameter, boolean required) { + super(methodParameter, required); + } + + public void setShortcut(@Nullable String shortcut) { + this.shortcut = shortcut; + } + + public boolean hasShortcut() { + return (this.shortcut != null); + } + + @Override + public Object resolveShortcut(BeanFactory beanFactory) { + String shortcut = this.shortcut; + return (shortcut != null ? beanFactory.getBean(shortcut, getDependencyType()) : null); + } + } + } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java index f3ec59432ddd..9170a4abe6f4 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java @@ -72,6 +72,8 @@ import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.annotation.Order; import org.springframework.core.testfixture.io.SerializationTestUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -131,6 +133,8 @@ public void testResourceInjection() { bean = bf.getBean("annotatedBean", ResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); assertThat(bean.getTestBean2()).isSameAs(tb); + + assertThat(bf.getDependenciesForBean("annotatedBean")).isEqualTo(new String[] {"testBean"}); } @Test @@ -152,10 +156,12 @@ public void testResourceInjectionWithNullBean() { assertThat(bean.getTestBean()).isNull(); assertThat(bean.getTestBean2()).isNull(); assertThat(bean.getTestBean3()).isNull(); + + assertThat(bf.getDependenciesForBean("annotatedBean")).isEqualTo(new String[] {"testBean"}); } @Test - void resourceInjectionWithSometimesNullBean() { + void resourceInjectionWithSometimesNullBeanEarly() { RootBeanDefinition bd = new RootBeanDefinition(OptionalResourceInjectionBean.class); bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); bf.registerBeanDefinition("annotatedBean", bd); @@ -170,6 +176,18 @@ void resourceInjectionWithSometimesNullBean() { assertThat(bean.getTestBean2()).isNull(); assertThat(bean.getTestBean3()).isNull(); + SometimesNullFactoryMethods.active = false; + bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isNull(); + assertThat(bean.getTestBean2()).isNull(); + assertThat(bean.getTestBean3()).isNull(); + + SometimesNullFactoryMethods.active = true; + bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isNotNull(); + assertThat(bean.getTestBean2()).isNotNull(); + assertThat(bean.getTestBean3()).isNotNull(); + SometimesNullFactoryMethods.active = true; bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); assertThat(bean.getTestBean()).isNotNull(); @@ -188,12 +206,43 @@ void resourceInjectionWithSometimesNullBean() { assertThat(bean.getTestBean2()).isNull(); assertThat(bean.getTestBean3()).isNull(); + assertThat(bf.getDependenciesForBean("annotatedBean")).isEqualTo(new String[] {"testBean"}); + } + + @Test + void resourceInjectionWithSometimesNullBeanLate() { + RootBeanDefinition bd = new RootBeanDefinition(OptionalResourceInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + RootBeanDefinition tb = new RootBeanDefinition(SometimesNullFactoryMethods.class); + tb.setFactoryMethodName("createTestBean"); + tb.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("testBean", tb); + + SometimesNullFactoryMethods.active = true; + OptionalResourceInjectionBean bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isNotNull(); + assertThat(bean.getTestBean2()).isNotNull(); + assertThat(bean.getTestBean3()).isNotNull(); + SometimesNullFactoryMethods.active = true; bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); assertThat(bean.getTestBean()).isNotNull(); assertThat(bean.getTestBean2()).isNotNull(); assertThat(bean.getTestBean3()).isNotNull(); + SometimesNullFactoryMethods.active = false; + bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isNull(); + assertThat(bean.getTestBean2()).isNull(); + assertThat(bean.getTestBean3()).isNull(); + + SometimesNullFactoryMethods.active = false; + bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isNull(); + assertThat(bean.getTestBean2()).isNull(); + assertThat(bean.getTestBean3()).isNull(); + SometimesNullFactoryMethods.active = true; bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); assertThat(bean.getTestBean()).isNotNull(); @@ -205,6 +254,8 @@ void resourceInjectionWithSometimesNullBean() { assertThat(bean.getTestBean()).isNull(); assertThat(bean.getTestBean2()).isNull(); assertThat(bean.getTestBean3()).isNull(); + + assertThat(bf.getDependenciesForBean("annotatedBean")).isEqualTo(new String[] {"testBean"}); } @Test @@ -233,10 +284,7 @@ public void testExtendedResourceInjection() { assertThat(bean.getNestedTestBean()).isSameAs(ntb); assertThat(bean.getBeanFactory()).isSameAs(bf); - String[] depBeans = bf.getDependenciesForBean("annotatedBean"); - assertThat(depBeans.length).isEqualTo(2); - assertThat(depBeans[0]).isEqualTo("testBean"); - assertThat(depBeans[1]).isEqualTo("nestedTestBean"); + assertThat(bf.getDependenciesForBean("annotatedBean")).isEqualTo(new String[] {"testBean", "nestedTestBean"}); } @Test @@ -719,6 +767,9 @@ public void testConstructorResourceInjection() { assertThat(bean.getTestBean4()).isSameAs(tb); assertThat(bean.getNestedTestBean()).isSameAs(ntb); assertThat(bean.getBeanFactory()).isSameAs(bf); + + assertThat(bf.getDependenciesForBean("annotatedBean")).isEqualTo( + new String[] {"testBean", "nestedTestBean", ObjectUtils.identityToString(bf)}); } @Test @@ -881,6 +932,80 @@ public void testConstructorResourceInjectionWithNoCandidatesAndNoFallback() { .satisfies(methodParameterDeclaredOn(ConstructorWithoutFallbackBean.class)); } + @Test + void constructorResourceInjectionWithSometimesNullBeanEarly() { + RootBeanDefinition bd = new RootBeanDefinition(ConstructorWithNullableArgument.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + RootBeanDefinition tb = new RootBeanDefinition(SometimesNullFactoryMethods.class); + tb.setFactoryMethodName("createTestBean"); + tb.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("testBean", tb); + + SometimesNullFactoryMethods.active = false; + ConstructorWithNullableArgument bean = (ConstructorWithNullableArgument) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNull(); + + SometimesNullFactoryMethods.active = false; + bean = (ConstructorWithNullableArgument) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNull(); + + SometimesNullFactoryMethods.active = true; + bean = (ConstructorWithNullableArgument) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNotNull(); + + SometimesNullFactoryMethods.active = true; + bean = (ConstructorWithNullableArgument) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNotNull(); + + SometimesNullFactoryMethods.active = false; + bean = (ConstructorWithNullableArgument) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNull(); + + SometimesNullFactoryMethods.active = false; + bean = (ConstructorWithNullableArgument) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNull(); + + assertThat(bf.getDependenciesForBean("annotatedBean")).isEqualTo(new String[] {"testBean"}); + } + + @Test + void constructorResourceInjectionWithSometimesNullBeanLate() { + RootBeanDefinition bd = new RootBeanDefinition(ConstructorWithNullableArgument.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + RootBeanDefinition tb = new RootBeanDefinition(SometimesNullFactoryMethods.class); + tb.setFactoryMethodName("createTestBean"); + tb.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("testBean", tb); + + SometimesNullFactoryMethods.active = true; + ConstructorWithNullableArgument bean = (ConstructorWithNullableArgument) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNotNull(); + + SometimesNullFactoryMethods.active = true; + bean = (ConstructorWithNullableArgument) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNotNull(); + + SometimesNullFactoryMethods.active = false; + bean = (ConstructorWithNullableArgument) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNull(); + + SometimesNullFactoryMethods.active = false; + bean = (ConstructorWithNullableArgument) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNull(); + + SometimesNullFactoryMethods.active = true; + bean = (ConstructorWithNullableArgument) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNotNull(); + + SometimesNullFactoryMethods.active = false; + bean = (ConstructorWithNullableArgument) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNull(); + + assertThat(bf.getDependenciesForBean("annotatedBean")).isEqualTo(new String[] {"testBean"}); + } + @Test public void testConstructorResourceInjectionWithCollectionAndNullFromFactoryBean() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition( @@ -2804,6 +2929,21 @@ public ITestBean getTestBean3() { } + public static class ConstructorWithNullableArgument { + + protected ITestBean testBean3; + + @Autowired(required = false) + public ConstructorWithNullableArgument(@Nullable ITestBean testBean3) { + this.testBean3 = testBean3; + } + + public ITestBean getTestBean3() { + return this.testBean3; + } + } + + public static class ConstructorsCollectionResourceInjectionBean { protected ITestBean testBean3; diff --git a/spring-core/src/main/java/org/springframework/util/MethodInvoker.java b/spring-core/src/main/java/org/springframework/util/MethodInvoker.java index b3e0c5554806..0443c5699ca1 100644 --- a/spring-core/src/main/java/org/springframework/util/MethodInvoker.java +++ b/spring-core/src/main/java/org/springframework/util/MethodInvoker.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -123,8 +123,8 @@ public String getTargetMethod() { /** * Set a fully qualified static method name to invoke, - * e.g. "example.MyExampleClass.myExampleMethod". - * Convenient alternative to specifying targetClass and targetMethod. + * e.g. "example.MyExampleClass.myExampleMethod". This is a + * convenient alternative to specifying targetClass and targetMethod. * @see #setTargetClass * @see #setTargetMethod */ @@ -157,14 +157,16 @@ public Object[] getArguments() { public void prepare() throws ClassNotFoundException, NoSuchMethodException { if (this.staticMethod != null) { int lastDotIndex = this.staticMethod.lastIndexOf('.'); - if (lastDotIndex == -1 || lastDotIndex == this.staticMethod.length()) { + if (lastDotIndex == -1 || lastDotIndex == this.staticMethod.length() - 1) { throw new IllegalArgumentException( "staticMethod must be a fully qualified class plus method name: " + "e.g. 'example.MyExampleClass.myExampleMethod'"); } String className = this.staticMethod.substring(0, lastDotIndex); String methodName = this.staticMethod.substring(lastDotIndex + 1); - this.targetClass = resolveClassName(className); + if (this.targetClass == null || !this.targetClass.getName().equals(className)) { + this.targetClass = resolveClassName(className); + } this.targetMethod = methodName; } From ef65429823ba25ecd7f0f8759206c0dc54b2a871 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sat, 15 Jul 2023 14:20:00 +0200 Subject: [PATCH 05/47] Polishing (cherry picked from commit 3a278cc66d428dcc42faa86830cc9b7b8b08c8e3) --- .../support/ByteBufferConverterTests.java | 29 +++++++------- .../CollectionToCollectionConverterTests.java | 4 +- .../support/MapToMapConverterTests.java | 4 +- .../support/ObjectToObjectConverterTests.java | 38 ++++++++++--------- 4 files changed, 39 insertions(+), 36 deletions(-) diff --git a/spring-core/src/test/java/org/springframework/core/convert/support/ByteBufferConverterTests.java b/spring-core/src/test/java/org/springframework/core/convert/support/ByteBufferConverterTests.java index 9c0f46ecb438..c1bcf11551b9 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/support/ByteBufferConverterTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/support/ByteBufferConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,57 +33,56 @@ */ class ByteBufferConverterTests { - private GenericConversionService conversionService; + private final GenericConversionService conversionService = new DefaultConversionService(); @BeforeEach void setup() { - this.conversionService = new DefaultConversionService(); - this.conversionService.addConverter(new ByteArrayToOtherTypeConverter()); - this.conversionService.addConverter(new OtherTypeToByteArrayConverter()); + conversionService.addConverter(new ByteArrayToOtherTypeConverter()); + conversionService.addConverter(new OtherTypeToByteArrayConverter()); } @Test - void byteArrayToByteBuffer() throws Exception { + void byteArrayToByteBuffer() { byte[] bytes = new byte[] { 1, 2, 3 }; - ByteBuffer convert = this.conversionService.convert(bytes, ByteBuffer.class); + ByteBuffer convert = conversionService.convert(bytes, ByteBuffer.class); assertThat(convert.array()).isNotSameAs(bytes); assertThat(convert.array()).isEqualTo(bytes); } @Test - void byteBufferToByteArray() throws Exception { + void byteBufferToByteArray() { byte[] bytes = new byte[] { 1, 2, 3 }; ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); - byte[] convert = this.conversionService.convert(byteBuffer, byte[].class); + byte[] convert = conversionService.convert(byteBuffer, byte[].class); assertThat(convert).isNotSameAs(bytes); assertThat(convert).isEqualTo(bytes); } @Test - void byteBufferToOtherType() throws Exception { + void byteBufferToOtherType() { byte[] bytes = new byte[] { 1, 2, 3 }; ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); - OtherType convert = this.conversionService.convert(byteBuffer, OtherType.class); + OtherType convert = conversionService.convert(byteBuffer, OtherType.class); assertThat(convert.bytes).isNotSameAs(bytes); assertThat(convert.bytes).isEqualTo(bytes); } @Test - void otherTypeToByteBuffer() throws Exception { + void otherTypeToByteBuffer() { byte[] bytes = new byte[] { 1, 2, 3 }; OtherType otherType = new OtherType(bytes); - ByteBuffer convert = this.conversionService.convert(otherType, ByteBuffer.class); + ByteBuffer convert = conversionService.convert(otherType, ByteBuffer.class); assertThat(convert.array()).isNotSameAs(bytes); assertThat(convert.array()).isEqualTo(bytes); } @Test - void byteBufferToByteBuffer() throws Exception { + void byteBufferToByteBuffer() { byte[] bytes = new byte[] { 1, 2, 3 }; ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); - ByteBuffer convert = this.conversionService.convert(byteBuffer, ByteBuffer.class); + ByteBuffer convert = conversionService.convert(byteBuffer, ByteBuffer.class); assertThat(convert).isNotSameAs(byteBuffer.rewind()); assertThat(convert).isEqualTo(byteBuffer.rewind()); assertThat(convert).isEqualTo(ByteBuffer.wrap(bytes)); diff --git a/spring-core/src/test/java/org/springframework/core/convert/support/CollectionToCollectionConverterTests.java b/spring-core/src/test/java/org/springframework/core/convert/support/CollectionToCollectionConverterTests.java index b7eca2c60473..931a59374551 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/support/CollectionToCollectionConverterTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/support/CollectionToCollectionConverterTests.java @@ -49,11 +49,11 @@ */ class CollectionToCollectionConverterTests { - private GenericConversionService conversionService = new GenericConversionService(); + private final GenericConversionService conversionService = new GenericConversionService(); @BeforeEach - void setUp() { + void setup() { conversionService.addConverter(new CollectionToCollectionConverter(conversionService)); } diff --git a/spring-core/src/test/java/org/springframework/core/convert/support/MapToMapConverterTests.java b/spring-core/src/test/java/org/springframework/core/convert/support/MapToMapConverterTests.java index fd900a79e779..9a753dd7eaf0 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/support/MapToMapConverterTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/support/MapToMapConverterTests.java @@ -38,7 +38,7 @@ /** * @author Keith Donald - * @author Phil Webb + * @author Phillip Webb * @author Juergen Hoeller */ class MapToMapConverterTests { @@ -47,7 +47,7 @@ class MapToMapConverterTests { @BeforeEach - void setUp() { + void setup() { conversionService.addConverter(new MapToMapConverter(conversionService)); } diff --git a/spring-core/src/test/java/org/springframework/core/convert/support/ObjectToObjectConverterTests.java b/spring-core/src/test/java/org/springframework/core/convert/support/ObjectToObjectConverterTests.java index 7565d74992aa..4af44b77f4fa 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/support/ObjectToObjectConverterTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/support/ObjectToObjectConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.core.convert.ConverterNotFoundException; @@ -29,15 +30,19 @@ * Unit tests for {@link ObjectToObjectConverter}. * * @author Sam Brannen - * @author Phil Webb + * @author Phillip Webb * @since 5.3.21 * @see org.springframework.core.convert.converter.DefaultConversionServiceTests#convertObjectToObjectUsingValueOfMethod() */ class ObjectToObjectConverterTests { - private final GenericConversionService conversionService = new GenericConversionService() {{ - addConverter(new ObjectToObjectConverter()); - }}; + private final GenericConversionService conversionService = new GenericConversionService(); + + + @BeforeEach + void setup() { + conversionService.addConverter(new ObjectToObjectConverter()); + } /** @@ -47,7 +52,7 @@ class ObjectToObjectConverterTests { @Test void nonStaticToTargetTypeSimpleNameMethodWithMatchingReturnType() { assertThat(conversionService.canConvert(Source.class, Data.class)) - .as("can convert Source to Data").isTrue(); + .as("can convert Source to Data").isTrue(); Data data = conversionService.convert(new Source("test"), Data.class); assertThat(data).asString().isEqualTo("test"); } @@ -55,21 +60,21 @@ void nonStaticToTargetTypeSimpleNameMethodWithMatchingReturnType() { @Test void nonStaticToTargetTypeSimpleNameMethodWithDifferentReturnType() { assertThat(conversionService.canConvert(Text.class, Data.class)) - .as("can convert Text to Data").isFalse(); + .as("can convert Text to Data").isFalse(); assertThat(conversionService.canConvert(Text.class, Optional.class)) - .as("can convert Text to Optional").isFalse(); + .as("can convert Text to Optional").isFalse(); assertThatExceptionOfType(ConverterNotFoundException.class) - .as("convert Text to Data") - .isThrownBy(() -> conversionService.convert(new Text("test"), Data.class)); + .as("convert Text to Data") + .isThrownBy(() -> conversionService.convert(new Text("test"), Data.class)); } @Test void staticValueOfFactoryMethodWithDifferentReturnType() { assertThat(conversionService.canConvert(String.class, Data.class)) - .as("can convert String to Data").isFalse(); + .as("can convert String to Data").isFalse(); assertThatExceptionOfType(ConverterNotFoundException.class) - .as("convert String to Data") - .isThrownBy(() -> conversionService.convert("test", Data.class)); + .as("convert String to Data") + .isThrownBy(() -> conversionService.convert("test", Data.class)); } @@ -84,9 +89,9 @@ private Source(String value) { public Data toData() { return new Data(this.value); } - } + static class Text { private final String value; @@ -98,9 +103,9 @@ private Text(String value) { public Optional toData() { return Optional.of(new Data(this.value)); } - } + static class Data { private final String value; @@ -115,9 +120,8 @@ public String toString() { } public static Optional valueOf(String string) { - return (string != null) ? Optional.of(new Data(string)) : Optional.empty(); + return (string != null ? Optional.of(new Data(string)) : Optional.empty()); } - } } From 0f33f79c05315150e31e8c14a9cce50af05c0cb7 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sun, 16 Jul 2023 16:22:17 +0200 Subject: [PATCH 06/47] Avoid synchronization for shortcut re-resolution See gh-30883 (cherry picked from commit 161a71763969a86dbfd344cb3391bfd3d2231bfb) --- .../factory/support/ConstructorResolver.java | 18 ++++--- ...wiredAnnotationBeanPostProcessorTests.java | 54 +++++++++++++++++++ 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java index b1ce9e61faf1..a30075f27318 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java @@ -833,14 +833,18 @@ private Object[] resolvePreparedArguments(String beanName, RootBeanDefinition mb } catch (BeansException ex) { // Unexpected target bean mismatch for cached argument -> re-resolve - synchronized (descriptor) { - if (!descriptor.hasShortcut()) { - throw ex; - } + Set autowiredBeanNames = null; + if (descriptor.hasShortcut()) { + // Reset shortcut and try to re-resolve it in this thread... descriptor.setShortcut(null); - Set autowiredBeanNames = new LinkedHashSet<>(2); - argValue = resolveAutowiredArgument(descriptor, paramType, beanName, - autowiredBeanNames, converter, true); + autowiredBeanNames = new LinkedHashSet<>(2); + } + logger.debug("Failed to resolve cached argument", ex); + argValue = resolveAutowiredArgument(descriptor, paramType, beanName, + autowiredBeanNames, converter, true); + if (autowiredBeanNames != null && !descriptor.hasShortcut()) { + // We encountered as stale shortcut before, and the shortcut has + // not been re-resolved by another thread in the meantime... if (argValue != null) { setShortcutIfPossible(descriptor, paramType, autowiredBeanNames); } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java index 9170a4abe6f4..b7d89f13de04 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java @@ -446,6 +446,22 @@ public void testOptionalResourceInjectionWithSingletonRemoval() { assertThat(bean.nestedTestBeansField[1]).isSameAs(ntb2); bf.destroySingleton("testBean"); + bf.registerSingleton("testBeanX", tb); + + bean = bf.getBean("annotatedBean", OptionalResourceInjectionBean.class); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getIndexedTestBean()).isSameAs(itb); + assertThat(bean.getNestedTestBeans()).hasSize(2); + assertThat(bean.getNestedTestBeans()[0]).isSameAs(ntb1); + assertThat(bean.getNestedTestBeans()[1]).isSameAs(ntb2); + assertThat(bean.nestedTestBeansField).hasSize(2); + assertThat(bean.nestedTestBeansField[0]).isSameAs(ntb1); + assertThat(bean.nestedTestBeansField[1]).isSameAs(ntb2); + + bf.destroySingleton("testBeanX"); bean = bf.getBean("annotatedBean", OptionalResourceInjectionBean.class); assertThat(bean.getTestBean()).isNull(); @@ -503,6 +519,22 @@ public void testOptionalResourceInjectionWithBeanDefinitionRemoval() { assertThat(bean.nestedTestBeansField[1]).isSameAs(ntb2); bf.removeBeanDefinition("testBean"); + bf.registerBeanDefinition("testBeanX", new RootBeanDefinition(TestBean.class)); + + bean = bf.getBean("annotatedBean", OptionalResourceInjectionBean.class); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBeanX")); + assertThat(bean.getTestBean2()).isSameAs(bf.getBean("testBeanX")); + assertThat(bean.getTestBean3()).isSameAs(bf.getBean("testBeanX")); + assertThat(bean.getTestBean4()).isSameAs(bf.getBean("testBeanX")); + assertThat(bean.getIndexedTestBean()).isSameAs(itb); + assertThat(bean.getNestedTestBeans()).hasSize(2); + assertThat(bean.getNestedTestBeans()[0]).isSameAs(ntb1); + assertThat(bean.getNestedTestBeans()[1]).isSameAs(ntb2); + assertThat(bean.nestedTestBeansField).hasSize(2); + assertThat(bean.nestedTestBeansField[0]).isSameAs(ntb1); + assertThat(bean.nestedTestBeansField[1]).isSameAs(ntb2); + + bf.removeBeanDefinition("testBeanX"); bean = bf.getBean("annotatedBean", OptionalResourceInjectionBean.class); assertThat(bean.getTestBean()).isNull(); @@ -791,6 +823,17 @@ public void testConstructorResourceInjectionWithSingletonRemoval() { assertThat(bean.getBeanFactory()).isSameAs(bf); bf.destroySingleton("nestedTestBean"); + bf.registerSingleton("nestedTestBeanX", ntb); + + bean = bf.getBean("annotatedBean", ConstructorResourceInjectionBean.class); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBean()).isSameAs(ntb); + assertThat(bean.getBeanFactory()).isSameAs(bf); + + bf.destroySingleton("nestedTestBeanX"); bean = bf.getBean("annotatedBean", ConstructorResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); @@ -829,6 +872,17 @@ public void testConstructorResourceInjectionWithBeanDefinitionRemoval() { assertThat(bean.getBeanFactory()).isSameAs(bf); bf.removeBeanDefinition("nestedTestBean"); + bf.registerBeanDefinition("nestedTestBeanX", new RootBeanDefinition(NestedTestBean.class)); + + bean = bf.getBean("annotatedBean", ConstructorResourceInjectionBean.class); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBean()).isSameAs(bf.getBean("nestedTestBeanX")); + assertThat(bean.getBeanFactory()).isSameAs(bf); + + bf.removeBeanDefinition("nestedTestBeanX"); bean = bf.getBean("annotatedBean", ConstructorResourceInjectionBean.class); assertThat(bean.getTestBean()).isSameAs(tb); From b387d9bf106d8b53e8a5232bf09421063c9892e3 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 18 Jul 2023 13:27:15 +0200 Subject: [PATCH 07/47] MethodIntrospector handles overriding bridge method correctly Closes gh-30906 (cherry picked from commit 616f728afa272270d1a606e61b515b1bb9339064) --- .../AnnotationDrivenEventListenerTests.java | 33 +++++++++++++------ .../core/MethodIntrospector.java | 5 +-- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java b/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java index c493f075b1ae..4374b4210d59 100644 --- a/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -164,12 +164,12 @@ void contextEventsAreReceived() { ContextEventListener listener = this.context.getBean(ContextEventListener.class); List events = this.eventCollector.getEvents(listener); - assertThat(events.size()).as("Wrong number of initial context events").isEqualTo(1); + assertThat(events).as("Wrong number of initial context events").hasSize(1); assertThat(events.get(0).getClass()).isEqualTo(ContextRefreshedEvent.class); this.context.stop(); List eventsAfterStop = this.eventCollector.getEvents(listener); - assertThat(eventsAfterStop.size()).as("Wrong number of context events on shutdown").isEqualTo(2); + assertThat(eventsAfterStop).as("Wrong number of context events on shutdown").hasSize(2); assertThat(eventsAfterStop.get(1).getClass()).isEqualTo(ContextStoppedEvent.class); this.eventCollector.assertTotalEventsCount(2); } @@ -334,7 +334,7 @@ void eventListenerWorksWithSimpleInterfaceProxy() { load(ScopedProxyTestBean.class); SimpleService proxy = this.context.getBean(SimpleService.class); - assertThat(proxy instanceof Advised).as("bean should be a proxy").isTrue(); + assertThat(proxy).as("bean should be a proxy").isInstanceOf(Advised.class); this.eventCollector.assertNoEventReceived(proxy.getId()); this.context.publishEvent(new ContextRefreshedEvent(this.context)); @@ -351,7 +351,7 @@ void eventListenerWorksWithAnnotatedInterfaceProxy() { load(AnnotatedProxyTestBean.class); AnnotatedSimpleService proxy = this.context.getBean(AnnotatedSimpleService.class); - assertThat(proxy instanceof Advised).as("bean should be a proxy").isTrue(); + assertThat(proxy).as("bean should be a proxy").isInstanceOf(Advised.class); this.eventCollector.assertNoEventReceived(proxy.getId()); this.context.publishEvent(new ContextRefreshedEvent(this.context)); @@ -517,7 +517,6 @@ void replyWithPayload() { ReplyEventListener replyEventListener = this.context.getBean(ReplyEventListener.class); TestEventListener listener = this.context.getBean(TestEventListener.class); - this.eventCollector.assertNoEventReceived(listener); this.eventCollector.assertNoEventReceived(replyEventListener); this.context.publishEvent(event); @@ -634,6 +633,17 @@ void orderedListeners() { assertThat(listener.order).contains("first", "second", "third"); } + @Test + void publicSubclassWithInheritedEventListener() { + load(PublicSubclassWithInheritedEventListener.class); + TestEventListener listener = this.context.getBean(PublicSubclassWithInheritedEventListener.class); + + this.eventCollector.assertNoEventReceived(listener); + this.context.publishEvent("test"); + this.eventCollector.assertEvent(listener, "test"); + this.eventCollector.assertTotalEventsCount(1); + } + @Test @Disabled // SPR-15122 void listenersReceiveEarlyEvents() { load(EventOnPostConstruct.class, OrderedTestListener.class); @@ -646,7 +656,7 @@ void listenersReceiveEarlyEvents() { void missingListenerBeanIgnored() { load(MissingEventListener.class); context.getBean(UseMissingEventListener.class); - context.getBean(ApplicationEventMulticaster.class).multicastEvent(new TestEvent(this)); + context.publishEvent(new TestEvent(this)); } @@ -753,7 +763,6 @@ static class ContextEventListener extends AbstractTestEventListener { public void handleContextEvent(ApplicationContextEvent event) { collectEvent(event); } - } @@ -980,7 +989,6 @@ public void handleString(GenericEventPojo value) { } - @EventListener @Retention(RetentionPolicy.RUNTIME) public @interface ConditionalEvent { @@ -1032,7 +1040,7 @@ public void handleRatio(Double ratio) { } - @Configuration + @Component static class OrderedTestListener extends TestEventListener { public final List order = new ArrayList<>(); @@ -1056,6 +1064,11 @@ public void handleSecond(String payload) { } + @Component + public static class PublicSubclassWithInheritedEventListener extends TestEventListener { + } + + static class EventOnPostConstruct { @Autowired diff --git a/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java b/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java index 2947945cc488..9f905cbda473 100644 --- a/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java +++ b/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -74,7 +74,8 @@ public static Map selectMethods(Class targetType, final Metada T result = metadataLookup.inspect(specificMethod); if (result != null) { Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); - if (bridgedMethod == specificMethod || metadataLookup.inspect(bridgedMethod) == null) { + if (bridgedMethod == specificMethod || bridgedMethod == method || + metadataLookup.inspect(bridgedMethod) == null) { methodMap.put(specificMethod, result); } } From 3a8c0dbd8aef97f124ea0f9c4b8aff4c476970f8 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 18 Jul 2023 22:12:58 +0200 Subject: [PATCH 08/47] Decouple exception messages for sync=true from @Cacheable (cherry picked from commit bbcc788f609098f3dd22750718b446b122188ae1) --- .../cache/annotation/Cacheable.java | 8 ++-- .../cache/interceptor/CacheAspectSupport.java | 38 +++++++++---------- .../interceptor/CacheSyncFailureTests.java | 37 +++++++++--------- 3 files changed, 43 insertions(+), 40 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java b/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java index 6d626e783230..13e097ca2a04 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -175,9 +175,9 @@ *
  • Only one cache may be specified
  • *
  • No other cache-related operation can be combined
  • * - * This is effectively a hint and the actual cache provider that you are - * using may not support it in a synchronized fashion. Check your provider - * documentation for more details on the actual semantics. + * This is effectively a hint and the chosen cache provider might not actually + * support it in a synchronized fashion. Check your provider documentation for + * more details on the actual semantics. * @since 4.3 * @see org.springframework.cache.Cache#get(Object, Callable) */ diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java index c528a83206bd..260d48299e18 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -214,7 +214,7 @@ public void afterPropertiesSet() { @Override public void afterSingletonsInstantiated() { if (getCacheResolver() == null) { - // Lazily initialize cache resolver via default cache manager... + // Lazily initialize cache resolver via default cache manager Assert.state(this.beanFactory != null, "CacheResolver or BeanFactory must be set on cache aspect"); try { setCacheManager(this.beanFactory.getBean(CacheManager.class)); @@ -307,22 +307,22 @@ else if (StringUtils.hasText(operation.getCacheManager())) { } /** - * Return a bean with the specified name and type. Used to resolve services that - * are referenced by name in a {@link CacheOperation}. - * @param beanName the name of the bean, as defined by the operation - * @param expectedType type for the bean - * @return the bean matching that name + * Retrieve a bean with the specified name and type. + * Used to resolve services that are referenced by name in a {@link CacheOperation}. + * @param name the name of the bean, as defined by the cache operation + * @param serviceType the type expected by the operation's service reference + * @return the bean matching the expected type, qualified by the given name * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException if such bean does not exist * @see CacheOperation#getKeyGenerator() * @see CacheOperation#getCacheManager() * @see CacheOperation#getCacheResolver() */ - protected T getBean(String beanName, Class expectedType) { + protected T getBean(String name, Class serviceType) { if (this.beanFactory == null) { throw new IllegalStateException( - "BeanFactory must be set on cache aspect for " + expectedType.getSimpleName() + " retrieval"); + "BeanFactory must be set on cache aspect for " + serviceType.getSimpleName() + " retrieval"); } - return BeanFactoryAnnotationUtils.qualifiedBeanOfType(this.beanFactory, expectedType, beanName); + return BeanFactoryAnnotationUtils.qualifiedBeanOfType(this.beanFactory, serviceType, name); } /** @@ -388,12 +388,11 @@ private Object execute(final CacheOperationInvoker invoker, Method method, Cache } } else { - // No caching required, only call the underlying method + // No caching required, just call the underlying method return invokeOperation(invoker); } } - // Process any early evictions processCacheEvicts(contexts.get(CacheEvictOperation.class), true, CacheOperationExpressionEvaluator.NO_RESULT); @@ -641,21 +640,22 @@ private boolean determineSyncFlag(Method method) { if (syncEnabled) { if (this.contexts.size() > 1) { throw new IllegalStateException( - "@Cacheable(sync=true) cannot be combined with other cache operations on '" + method + "'"); + "A sync=true operation cannot be combined with other cache operations on '" + method + "'"); } if (cacheOperationContexts.size() > 1) { throw new IllegalStateException( - "Only one @Cacheable(sync=true) entry is allowed on '" + method + "'"); + "Only one sync=true operation is allowed on '" + method + "'"); } CacheOperationContext cacheOperationContext = cacheOperationContexts.iterator().next(); - CacheableOperation operation = (CacheableOperation) cacheOperationContext.getOperation(); + CacheOperation operation = cacheOperationContext.getOperation(); if (cacheOperationContext.getCaches().size() > 1) { throw new IllegalStateException( - "@Cacheable(sync=true) only allows a single cache on '" + operation + "'"); + "A sync=true operation is restricted to a single cache on '" + operation + "'"); } - if (StringUtils.hasText(operation.getUnless())) { + if (operation instanceof CacheableOperation && + StringUtils.hasText(((CacheableOperation) operation).getUnless())) { throw new IllegalStateException( - "@Cacheable(sync=true) does not support unless attribute on '" + operation + "'"); + "A sync=true operation does not support the unless attribute on '" + operation + "'"); } return true; } @@ -885,13 +885,13 @@ public int compareTo(CacheOperationCacheKey other) { } } + /** * Internal holder class for recording that a cache method was invoked. */ private static class InvocationAwareResult { boolean invoked; - } } diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheSyncFailureTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheSyncFailureTests.java index de4776adae2e..56b22970d1ac 100644 --- a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheSyncFailureTests.java +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheSyncFailureTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,8 +48,9 @@ public class CacheSyncFailureTests { private SimpleService simpleService; + @BeforeEach - public void setUp() { + public void setup() { this.context = new AnnotationConfigApplicationContext(Config.class); this.simpleService = this.context.getBean(SimpleService.class); } @@ -61,39 +62,40 @@ public void closeContext() { } } + @Test public void unlessSync() { - assertThatIllegalStateException().isThrownBy(() -> - this.simpleService.unlessSync("key")) - .withMessageContaining("@Cacheable(sync=true) does not support unless attribute"); + assertThatIllegalStateException() + .isThrownBy(() -> this.simpleService.unlessSync("key")) + .withMessageContaining("A sync=true operation does not support the unless attribute"); } @Test public void severalCachesSync() { - assertThatIllegalStateException().isThrownBy(() -> - this.simpleService.severalCachesSync("key")) - .withMessageContaining("@Cacheable(sync=true) only allows a single cache"); + assertThatIllegalStateException() + .isThrownBy(() -> this.simpleService.severalCachesSync("key")) + .withMessageContaining("A sync=true operation is restricted to a single cache"); } @Test public void severalCachesWithResolvedSync() { - assertThatIllegalStateException().isThrownBy(() -> - this.simpleService.severalCachesWithResolvedSync("key")) - .withMessageContaining("@Cacheable(sync=true) only allows a single cache"); + assertThatIllegalStateException() + .isThrownBy(() -> this.simpleService.severalCachesWithResolvedSync("key")) + .withMessageContaining("A sync=true operation is restricted to a single cache"); } @Test public void syncWithAnotherOperation() { - assertThatIllegalStateException().isThrownBy(() -> - this.simpleService.syncWithAnotherOperation("key")) - .withMessageContaining("@Cacheable(sync=true) cannot be combined with other cache operations"); + assertThatIllegalStateException() + .isThrownBy(() -> this.simpleService.syncWithAnotherOperation("key")) + .withMessageContaining("A sync=true operation cannot be combined with other cache operations"); } @Test public void syncWithTwoGetOperations() { - assertThatIllegalStateException().isThrownBy(() -> - this.simpleService.syncWithTwoGetOperations("key")) - .withMessageContaining("Only one @Cacheable(sync=true) entry is allowed"); + assertThatIllegalStateException() + .isThrownBy(() -> this.simpleService.syncWithTwoGetOperations("key")) + .withMessageContaining("Only one sync=true operation is allowed"); } @@ -131,6 +133,7 @@ public Object syncWithTwoGetOperations(Object arg1) { } } + @Configuration @EnableCaching static class Config implements CachingConfigurer { From a7b7466274bed2476394cf07d1ecead6a64acde6 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 19 Jul 2023 01:17:25 +0200 Subject: [PATCH 09/47] Polishing --- .../aspectj/AspectJExpressionPointcut.java | 10 ++++----- .../aop/framework/CglibAopProxy.java | 7 +++---- .../beans/factory/support/LookupOverride.java | 4 ++-- .../beans/testfixture/beans/TestBean.java | 4 ++-- .../AnnotationConfigApplicationContext.java | 8 +++---- .../AbstractXmlApplicationContext.java | 14 +++++++------ .../mock/env/MockEnvironment.java | 2 +- .../core/env/CommandLinePropertySource.java | 8 +++---- .../util/ConcurrentReferenceHashMap.java | 21 +++++++++---------- .../util/comparator/NullSafeComparator.java | 12 +++++------ .../testfixture/env/MockPropertySource.java | 8 +++---- .../connection/CachingConnectionFactory.java | 4 ++-- .../mock/env/MockEnvironment.java | 11 +++++----- .../mock/env/MockPropertySource.java | 6 ++++-- .../interceptor/RollbackRuleAttribute.java | 4 ++-- .../org/springframework/http/HttpCookie.java | 12 +++++------ .../support/ServletContextResource.java | 2 +- .../web/filter/DelegatingFilterProxy.java | 16 +++++++------- .../web/util/pattern/PathPattern.java | 12 ++++++----- .../client/ExchangeFilterFunctions.java | 4 ++-- .../web/servlet/tags/form/ItemPet.java | 6 ++++-- src/docs/asciidoc/core/core-beans.adoc | 4 ++-- 22 files changed, 93 insertions(+), 86 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java index b2efee6caa2c..5c79bb5aa703 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -248,8 +248,8 @@ private PointcutParser initializePointcutParser(@Nullable ClassLoader classLoade /** * If a pointcut expression has been specified in XML, the user cannot - * write {@code and} as "&&" (though && will work). - * We also allow {@code and} between two pointcut sub-expressions. + * write "and" as "&&" (though {@code &&} will work). + *

    We also allow "and" between two pointcut sub-expressions. *

    This method converts back to {@code &&} for the AspectJ pointcut parser. */ private String replaceBooleanOperators(String pcExpr) { @@ -527,7 +527,7 @@ public boolean equals(@Nullable Object other) { return false; } AspectJExpressionPointcut otherPc = (AspectJExpressionPointcut) other; - return ObjectUtils.nullSafeEquals(this.getExpression(), otherPc.getExpression()) && + return ObjectUtils.nullSafeEquals(getExpression(), otherPc.getExpression()) && ObjectUtils.nullSafeEquals(this.pointcutDeclarationScope, otherPc.pointcutDeclarationScope) && ObjectUtils.nullSafeEquals(this.pointcutParameterNames, otherPc.pointcutParameterNames) && ObjectUtils.nullSafeEquals(this.pointcutParameterTypes, otherPc.pointcutParameterTypes); @@ -535,7 +535,7 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - int hashCode = ObjectUtils.nullSafeHashCode(this.getExpression()); + int hashCode = ObjectUtils.nullSafeHashCode(getExpression()); hashCode = 31 * hashCode + ObjectUtils.nullSafeHashCode(this.pointcutDeclarationScope); hashCode = 31 * hashCode + ObjectUtils.nullSafeHashCode(this.pointcutParameterNames); hashCode = 31 * hashCode + ObjectUtils.nullSafeHashCode(this.pointcutParameterTypes); diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java b/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java index 1af6ae029aaf..a172c86c2472 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java @@ -424,10 +424,9 @@ public static class SerializableNoOp implements NoOp, Serializable { /** - * Method interceptor used for static targets with no advice chain. The call - * is passed directly back to the target. Used when the proxy needs to be - * exposed and it can't be determined that the method won't return - * {@code this}. + * Method interceptor used for static targets with no advice chain. The call is + * passed directly back to the target. Used when the proxy needs to be exposed + * and it can't be determined that the method won't return {@code this}. */ private static class StaticUnadvisedInterceptor implements MethodInterceptor, Serializable { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/LookupOverride.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/LookupOverride.java index c441bf7e9cc3..f24001330206 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/LookupOverride.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/LookupOverride.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -112,7 +112,7 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - return (29 * super.hashCode() + ObjectUtils.nullSafeHashCode(this.beanName)); + return super.hashCode() * 29 + ObjectUtils.nullSafeHashCode(this.beanName); } @Override diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java index ed54d0d05f4b..ce870f57846e 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -477,7 +477,7 @@ public boolean equals(Object other) { @Override public int hashCode() { - return this.age; + return TestBean.class.hashCode(); } @Override diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigApplicationContext.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigApplicationContext.java index 86ea5feb7335..6ca78078cb18 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigApplicationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -65,7 +65,7 @@ public class AnnotationConfigApplicationContext extends GenericApplicationContex * through {@link #register} calls and then manually {@linkplain #refresh refreshed}. */ public AnnotationConfigApplicationContext() { - StartupStep createAnnotatedBeanDefReader = this.getApplicationStartup().start("spring.context.annotated-bean-reader.create"); + StartupStep createAnnotatedBeanDefReader = getApplicationStartup().start("spring.context.annotated-bean-reader.create"); this.reader = new AnnotatedBeanDefinitionReader(this); createAnnotatedBeanDefReader.end(); this.scanner = new ClassPathBeanDefinitionScanner(this); @@ -163,7 +163,7 @@ public void setScopeMetadataResolver(ScopeMetadataResolver scopeMetadataResolver @Override public void register(Class... componentClasses) { Assert.notEmpty(componentClasses, "At least one component class must be specified"); - StartupStep registerComponentClass = this.getApplicationStartup().start("spring.context.component-classes.register") + StartupStep registerComponentClass = getApplicationStartup().start("spring.context.component-classes.register") .tag("classes", () -> Arrays.toString(componentClasses)); this.reader.register(componentClasses); registerComponentClass.end(); @@ -180,7 +180,7 @@ public void register(Class... componentClasses) { @Override public void scan(String... basePackages) { Assert.notEmpty(basePackages, "At least one base package must be specified"); - StartupStep scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan") + StartupStep scanPackages = getApplicationStartup().start("spring.context.base-packages.scan") .tag("packages", () -> Arrays.toString(basePackages)); this.scanner.scan(basePackages); scanPackages.end(); diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractXmlApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractXmlApplicationContext.java index 85cb2250b59e..c21002abd2cc 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractXmlApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractXmlApplicationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.BeanDefinitionDocumentReader; import org.springframework.beans.factory.xml.ResourceEntityResolver; import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.context.ApplicationContext; @@ -84,7 +85,7 @@ protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throw // Configure the bean definition reader with this context's // resource loading environment. - beanDefinitionReader.setEnvironment(this.getEnvironment()); + beanDefinitionReader.setEnvironment(getEnvironment()); beanDefinitionReader.setResourceLoader(this); beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this)); @@ -95,12 +96,13 @@ protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throw } /** - * Initialize the bean definition reader used for loading the bean - * definitions of this context. Default implementation is empty. + * Initialize the bean definition reader used for loading the bean definitions + * of this context. The default implementation sets the validating flag. *

    Can be overridden in subclasses, e.g. for turning off XML validation - * or using a different XmlBeanDefinitionParser implementation. + * or using a different {@link BeanDefinitionDocumentReader} implementation. * @param reader the bean definition reader used by this context - * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader#setDocumentReaderClass + * @see XmlBeanDefinitionReader#setValidating + * @see XmlBeanDefinitionReader#setDocumentReaderClass */ protected void initBeanDefinitionReader(XmlBeanDefinitionReader reader) { reader.setValidating(this.validating); diff --git a/spring-context/src/test/java/org/springframework/mock/env/MockEnvironment.java b/spring-context/src/test/java/org/springframework/mock/env/MockEnvironment.java index 6397865a1f88..9a533f563570 100644 --- a/spring-context/src/test/java/org/springframework/mock/env/MockEnvironment.java +++ b/spring-context/src/test/java/org/springframework/mock/env/MockEnvironment.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-core/src/main/java/org/springframework/core/env/CommandLinePropertySource.java b/spring-core/src/main/java/org/springframework/core/env/CommandLinePropertySource.java index 0f80080d1d6b..c317f5e2dc72 100644 --- a/spring-core/src/main/java/org/springframework/core/env/CommandLinePropertySource.java +++ b/spring-core/src/main/java/org/springframework/core/env/CommandLinePropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -252,7 +252,7 @@ public void setNonOptionArgsPropertyName(String nonOptionArgsPropertyName) { @Override public final boolean containsProperty(String name) { if (this.nonOptionArgsPropertyName.equals(name)) { - return !this.getNonOptionArgs().isEmpty(); + return !getNonOptionArgs().isEmpty(); } return this.containsOption(name); } @@ -270,7 +270,7 @@ public final boolean containsProperty(String name) { @Nullable public final String getProperty(String name) { if (this.nonOptionArgsPropertyName.equals(name)) { - Collection nonOptionArguments = this.getNonOptionArgs(); + Collection nonOptionArguments = getNonOptionArgs(); if (nonOptionArguments.isEmpty()) { return null; } @@ -278,7 +278,7 @@ public final String getProperty(String name) { return StringUtils.collectionToCommaDelimitedString(nonOptionArguments); } } - Collection optionValues = this.getOptionValues(name); + Collection optionValues = getOptionValues(name); if (optionValues == null) { return null; } diff --git a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java index 3291ca6e079a..75507d411d49 100644 --- a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java +++ b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -752,28 +752,27 @@ public V setValue(@Nullable V value) { } @Override - public String toString() { - return (this.key + "=" + this.value); - } - - @Override - @SuppressWarnings("rawtypes") - public final boolean equals(@Nullable Object other) { + public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof Map.Entry)) { + if (!(other instanceof Map.Entry)) { return false; } - Map.Entry otherEntry = (Map.Entry) other; + Map.Entry otherEntry = (Map.Entry) other; return (ObjectUtils.nullSafeEquals(getKey(), otherEntry.getKey()) && ObjectUtils.nullSafeEquals(getValue(), otherEntry.getValue())); } @Override - public final int hashCode() { + public int hashCode() { return (ObjectUtils.nullSafeHashCode(this.key) ^ ObjectUtils.nullSafeHashCode(this.value)); } + + @Override + public String toString() { + return (this.key + "=" + this.value); + } } diff --git a/spring-core/src/main/java/org/springframework/util/comparator/NullSafeComparator.java b/spring-core/src/main/java/org/springframework/util/comparator/NullSafeComparator.java index 37933af5397d..6eae4de69b55 100644 --- a/spring-core/src/main/java/org/springframework/util/comparator/NullSafeComparator.java +++ b/spring-core/src/main/java/org/springframework/util/comparator/NullSafeComparator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -85,7 +85,7 @@ private NullSafeComparator(boolean nullsLow) { * @param nullsLow whether to treat nulls lower or higher than non-null objects */ public NullSafeComparator(Comparator comparator, boolean nullsLow) { - Assert.notNull(comparator, "Non-null Comparator is required"); + Assert.notNull(comparator, "Comparator must not be null"); this.nonNullComparator = comparator; this.nullsLow = nullsLow; } @@ -107,16 +107,16 @@ public int compare(@Nullable T o1, @Nullable T o2) { @Override - @SuppressWarnings("unchecked") public boolean equals(@Nullable Object other) { if (this == other) { return true; } - if (!(other instanceof NullSafeComparator)) { + if (!(other instanceof NullSafeComparator)) { return false; } - NullSafeComparator otherComp = (NullSafeComparator) other; - return (this.nonNullComparator.equals(otherComp.nonNullComparator) && this.nullsLow == otherComp.nullsLow); + NullSafeComparator otherComp = (NullSafeComparator) other; + return (this.nonNullComparator.equals(otherComp.nonNullComparator) && + this.nullsLow == otherComp.nullsLow); } @Override diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/env/MockPropertySource.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/env/MockPropertySource.java index ae4944ddccb7..a4cbd1f22ed1 100644 --- a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/env/MockPropertySource.java +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/env/MockPropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ * * The {@link #setProperty} and {@link #withProperty} methods are exposed for * convenience, for example: - *

    + * 
      * {@code
      *   PropertySource source = new MockPropertySource().withProperty("foo", "bar");
      * }
    @@ -77,7 +77,7 @@ public MockPropertySource(Properties properties) {
     
     	/**
     	 * Create a new {@code MockPropertySource} with the given name and backed by the given
    -	 * {@link Properties} object
    +	 * {@link Properties} object.
     	 * @param name the {@linkplain #getName() name} of the property source
     	 * @param properties the properties to use
     	 */
    @@ -99,7 +99,7 @@ public void setProperty(String name, Object value) {
     	 * @return this {@link MockPropertySource} instance
     	 */
     	public MockPropertySource withProperty(String name, Object value) {
    -		this.setProperty(name, value);
    +		setProperty(name, value);
     		return this;
     	}
     
    diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java b/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java
    index 3bab8917c78f..627ccfa2e33f 100644
    --- a/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java
    +++ b/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2022 the original author or authors.
    + * Copyright 2002-2023 the original author or authors.
      *
      * Licensed under the Apache License, Version 2.0 (the "License");
      * you may not use this file except in compliance with the License.
    @@ -622,7 +622,7 @@ public boolean equals(@Nullable Object other) {
     
     		@Override
     		public int hashCode() {
    -			return (31 * super.hashCode() + ObjectUtils.nullSafeHashCode(this.selector));
    +			return super.hashCode() * 31 + ObjectUtils.nullSafeHashCode(this.selector);
     		}
     
     		@Override
    diff --git a/spring-test/src/main/java/org/springframework/mock/env/MockEnvironment.java b/spring-test/src/main/java/org/springframework/mock/env/MockEnvironment.java
    index 8495e7c37b06..88072db943d3 100644
    --- a/spring-test/src/main/java/org/springframework/mock/env/MockEnvironment.java
    +++ b/spring-test/src/main/java/org/springframework/mock/env/MockEnvironment.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2018 the original author or authors.
    + * Copyright 2002-2023 the original author or authors.
      *
      * Licensed under the Apache License, Version 2.0 (the "License");
      * you may not use this file except in compliance with the License.
    @@ -21,8 +21,7 @@
     
     /**
      * Simple {@link ConfigurableEnvironment} implementation exposing
    - * {@link #setProperty(String, String)} and {@link #withProperty(String, String)}
    - * methods for testing purposes.
    + * {@link #setProperty} and {@link #withProperty} methods for testing purposes.
      *
      * @author Chris Beams
      * @author Sam Brannen
    @@ -31,7 +30,8 @@
      */
     public class MockEnvironment extends AbstractEnvironment {
     
    -	private MockPropertySource propertySource = new MockPropertySource();
    +	private final MockPropertySource propertySource = new MockPropertySource();
    +
     
     	/**
     	 * Create a new {@code MockEnvironment} with a single {@link MockPropertySource}.
    @@ -40,6 +40,7 @@ public MockEnvironment() {
     		getPropertySources().addLast(this.propertySource);
     	}
     
    +
     	/**
     	 * Set a property on the underlying {@link MockPropertySource} for this environment.
     	 */
    @@ -54,7 +55,7 @@ public void setProperty(String key, String value) {
     	 * @see MockPropertySource#withProperty
     	 */
     	public MockEnvironment withProperty(String key, String value) {
    -		this.setProperty(key, value);
    +		setProperty(key, value);
     		return this;
     	}
     
    diff --git a/spring-test/src/main/java/org/springframework/mock/env/MockPropertySource.java b/spring-test/src/main/java/org/springframework/mock/env/MockPropertySource.java
    index 2f3eb44151f8..3ef180fcf22b 100644
    --- a/spring-test/src/main/java/org/springframework/mock/env/MockPropertySource.java
    +++ b/spring-test/src/main/java/org/springframework/mock/env/MockPropertySource.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2012 the original author or authors.
    + * Copyright 2002-2023 the original author or authors.
      *
      * Licensed under the Apache License, Version 2.0 (the "License");
      * you may not use this file except in compliance with the License.
    @@ -48,6 +48,7 @@ public class MockPropertySource extends PropertiesPropertySource {
     	 */
     	public static final String MOCK_PROPERTIES_PROPERTY_SOURCE_NAME = "mockProperties";
     
    +
     	/**
     	 * Create a new {@code MockPropertySource} named {@value #MOCK_PROPERTIES_PROPERTY_SOURCE_NAME}
     	 * that will maintain its own internal {@link Properties} instance.
    @@ -84,6 +85,7 @@ public MockPropertySource(String name, Properties properties) {
     		super(name, properties);
     	}
     
    +
     	/**
     	 * Set the given property on the underlying {@link Properties} object.
     	 */
    @@ -97,7 +99,7 @@ public void setProperty(String name, Object value) {
     	 * @return this {@link MockPropertySource} instance
     	 */
     	public MockPropertySource withProperty(String name, Object value) {
    -		this.setProperty(name, value);
    +		setProperty(name, value);
     		return this;
     	}
     
    diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java
    index 49327d934521..76754ce77a87 100644
    --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java
    +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java
    @@ -159,8 +159,8 @@ public boolean equals(@Nullable Object other) {
     		if (!(other instanceof RollbackRuleAttribute)) {
     			return false;
     		}
    -		RollbackRuleAttribute rhs = (RollbackRuleAttribute) other;
    -		return this.exceptionPattern.equals(rhs.exceptionPattern);
    +		RollbackRuleAttribute otherAttr = (RollbackRuleAttribute) other;
    +		return this.exceptionPattern.equals(otherAttr.exceptionPattern);
     	}
     
     	@Override
    diff --git a/spring-web/src/main/java/org/springframework/http/HttpCookie.java b/spring-web/src/main/java/org/springframework/http/HttpCookie.java
    index 9ce7fde3a26e..279015a91a69 100644
    --- a/spring-web/src/main/java/org/springframework/http/HttpCookie.java
    +++ b/spring-web/src/main/java/org/springframework/http/HttpCookie.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2017 the original author or authors.
    + * Copyright 2002-2023 the original author or authors.
      *
      * Licensed under the Apache License, Version 2.0 (the "License");
      * you may not use this file except in compliance with the License.
    @@ -56,11 +56,6 @@ public String getValue() {
     	}
     
     
    -	@Override
    -	public int hashCode() {
    -		return this.name.hashCode();
    -	}
    -
     	@Override
     	public boolean equals(@Nullable Object other) {
     		if (this == other) {
    @@ -73,6 +68,11 @@ public boolean equals(@Nullable Object other) {
     		return (this.name.equalsIgnoreCase(otherCookie.getName()));
     	}
     
    +	@Override
    +	public int hashCode() {
    +		return this.name.hashCode();
    +	}
    +
     	@Override
     	public String toString() {
     		return this.name + '=' + this.value;
    diff --git a/spring-web/src/main/java/org/springframework/web/context/support/ServletContextResource.java b/spring-web/src/main/java/org/springframework/web/context/support/ServletContextResource.java
    index c8efe40c6003..41ba664f100f 100644
    --- a/spring-web/src/main/java/org/springframework/web/context/support/ServletContextResource.java
    +++ b/spring-web/src/main/java/org/springframework/web/context/support/ServletContextResource.java
    @@ -251,7 +251,7 @@ public boolean equals(@Nullable Object other) {
     			return false;
     		}
     		ServletContextResource otherRes = (ServletContextResource) other;
    -		return (this.servletContext.equals(otherRes.servletContext) && this.path.equals(otherRes.path));
    +		return (this.path.equals(otherRes.path) && this.servletContext.equals(otherRes.servletContext));
     	}
     
     	/**
    diff --git a/spring-web/src/main/java/org/springframework/web/filter/DelegatingFilterProxy.java b/spring-web/src/main/java/org/springframework/web/filter/DelegatingFilterProxy.java
    index 0dbc22408240..dccd60228e9c 100644
    --- a/spring-web/src/main/java/org/springframework/web/filter/DelegatingFilterProxy.java
    +++ b/spring-web/src/main/java/org/springframework/web/filter/DelegatingFilterProxy.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2021 the original author or authors.
    + * Copyright 2002-2023 the original author or authors.
      *
      * Licensed under the Apache License, Version 2.0 (the "License");
      * you may not use this file except in compliance with the License.
    @@ -42,7 +42,7 @@
      * be delegated to that bean in the Spring context, which is required to implement
      * the standard Servlet Filter interface.
      *
    - * 

    This approach is particularly useful for Filter implementation with complex + *

    This approach is particularly useful for Filter implementations with complex * setup needs, allowing to apply the full Spring bean definition machinery to * Filter instances. Alternatively, consider standard Filter setup in combination * with looking up service beans from the Spring root application context. @@ -52,11 +52,11 @@ * Spring application context to manage the lifecycle of that bean. Specifying * the "targetFilterLifecycle" filter init-param as "true" will enforce invocation * of the {@code Filter.init} and {@code Filter.destroy} lifecycle methods - * on the target bean, letting the servlet container manage the filter lifecycle. + * on the target bean, letting the Servlet container manage the filter lifecycle. * - *

    As of Spring 3.1, {@code DelegatingFilterProxy} has been updated to optionally - * accept constructor parameters when using a Servlet container's instance-based filter - * registration methods, usually in conjunction with Spring's + *

    {@code DelegatingFilterProxy} can optionally accept constructor parameters + * when using a Servlet container's instance-based filter registration methods, + * usually in conjunction with Spring's * {@link org.springframework.web.WebApplicationInitializer} SPI. These constructors allow * for providing the delegate Filter bean directly, or providing the application context * and bean name to fetch, avoiding the need to look up the application context from the @@ -160,10 +160,10 @@ public DelegatingFilterProxy(String targetBeanName) { */ public DelegatingFilterProxy(String targetBeanName, @Nullable WebApplicationContext wac) { Assert.hasText(targetBeanName, "Target Filter bean name must not be null or empty"); - this.setTargetBeanName(targetBeanName); + setTargetBeanName(targetBeanName); this.webApplicationContext = wac; if (wac != null) { - this.setEnvironment(wac.getEnvironment()); + setEnvironment(wac.getEnvironment()); } } diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/PathPattern.java b/spring-web/src/main/java/org/springframework/web/util/pattern/PathPattern.java index f4569d4f2666..b3d02b6c26b7 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/PathPattern.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/PathPattern.java @@ -25,6 +25,7 @@ import org.springframework.http.server.PathContainer; import org.springframework.http.server.PathContainer.Element; +import org.springframework.http.server.PathContainer.PathSegment; import org.springframework.http.server.PathContainer.Separator; import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; @@ -430,6 +431,9 @@ else if (!StringUtils.hasLength(pattern2string.patternString)) { @Override public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } if (!(other instanceof PathPattern)) { return false; } @@ -600,13 +604,11 @@ public static class PathRemainingMatchInfo { private final PathMatchInfo pathMatchInfo; - PathRemainingMatchInfo(PathContainer pathMatched, PathContainer pathRemaining) { this(pathMatched, pathRemaining, PathMatchInfo.EMPTY); } - PathRemainingMatchInfo(PathContainer pathMatched, PathContainer pathRemaining, - PathMatchInfo pathMatchInfo) { + PathRemainingMatchInfo(PathContainer pathMatched, PathContainer pathRemaining, PathMatchInfo pathMatchInfo) { this.pathRemaining = pathRemaining; this.pathMatched = pathMatched; this.pathMatchInfo = pathMatchInfo; @@ -726,8 +728,8 @@ boolean isSeparator(int pathIndex) { */ String pathElementValue(int pathIndex) { Element element = (pathIndex < this.pathLength) ? this.pathElements.get(pathIndex) : null; - if (element instanceof PathContainer.PathSegment) { - return ((PathContainer.PathSegment)element).valueToMatch(); + if (element instanceof PathSegment) { + return ((PathSegment) element).valueToMatch(); } return ""; } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctions.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctions.java index 485d5d9e8da3..c8be890e40c9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctions.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctions.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -183,7 +183,7 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - return 31 * this.username.hashCode() + this.password.hashCode(); + return this.username.hashCode() * 31 + this.password.hashCode(); } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/form/ItemPet.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/form/ItemPet.java index 18d833cbb421..7d03434599f7 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/form/ItemPet.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/form/ItemPet.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,8 @@ import java.beans.PropertyEditorSupport; +import org.springframework.util.ObjectUtils; + /** * @author Juergen Hoeller */ @@ -50,7 +52,7 @@ public boolean equals(Object other) { return false; } ItemPet otherPet = (ItemPet) other; - return (this.name != null && this.name.equals(otherPet.getName())); + return ObjectUtils.nullSafeEquals(this.name, otherPet.getName()); } @Override diff --git a/src/docs/asciidoc/core/core-beans.adoc b/src/docs/asciidoc/core/core-beans.adoc index 5ba8c60b9451..e020f6290b10 100644 --- a/src/docs/asciidoc/core/core-beans.adoc +++ b/src/docs/asciidoc/core/core-beans.adoc @@ -11061,7 +11061,7 @@ Here is an example of instrumentation in the `AnnotationConfigApplicationContext .Java ---- // create a startup step and start recording - StartupStep scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan"); + StartupStep scanPackages = getApplicationStartup().start("spring.context.base-packages.scan"); // add tagging information to the current step scanPackages.tag("packages", () -> Arrays.toString(basePackages)); // perform the actual phase we're instrumenting @@ -11073,7 +11073,7 @@ Here is an example of instrumentation in the `AnnotationConfigApplicationContext .Kotlin ---- // create a startup step and start recording - val scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan") + val scanPackages = getApplicationStartup().start("spring.context.base-packages.scan") // add tagging information to the current step scanPackages.tag("packages", () -> Arrays.toString(basePackages)) // perform the actual phase we're instrumenting From 340b32a3cb3c59a7981b7f8ec0dc1e271c3a18cd Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 19 Jul 2023 23:31:38 +0200 Subject: [PATCH 10/47] Polishing --- .../validation/AbstractBindingResult.java | 6 +- .../validation/AbstractErrors.java | 19 +- .../validation/BeanPropertyBindingResult.java | 6 +- .../validation/BindException.java | 6 +- .../validation/DirectFieldBindingResult.java | 6 +- .../springframework/validation/Errors.java | 86 +++--- .../springframework/validation/Validator.java | 19 +- .../validation/DataBinderTests.java | 271 +++++++++--------- .../validation/ValidationUtilsTests.java | 16 +- .../web/bind/EscapedErrors.java | 6 +- .../support/WebExchangeBindException.java | 16 +- 11 files changed, 243 insertions(+), 214 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/validation/AbstractBindingResult.java b/spring-context/src/main/java/org/springframework/validation/AbstractBindingResult.java index b8857b15b50b..18278e009d90 100644 --- a/spring-context/src/main/java/org/springframework/validation/AbstractBindingResult.java +++ b/spring-context/src/main/java/org/springframework/validation/AbstractBindingResult.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -102,8 +102,8 @@ public void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable Str } @Override - public void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, - @Nullable String defaultMessage) { + public void rejectValue(@Nullable String field, String errorCode, + @Nullable Object[] errorArgs, @Nullable String defaultMessage) { if (!StringUtils.hasLength(getNestedPath()) && !StringUtils.hasLength(field)) { // We're at the top of the nested object hierarchy, diff --git a/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java b/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java index 9556dc3a286e..b9e5617a53c6 100644 --- a/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java +++ b/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,13 +28,14 @@ import org.springframework.util.StringUtils; /** - * Abstract implementation of the {@link Errors} interface. Provides common - * access to evaluated errors; however, does not define concrete management + * Abstract implementation of the {@link Errors} interface. + * Provides nested path handling but does not define concrete management * of {@link ObjectError ObjectErrors} and {@link FieldError FieldErrors}. * * @author Juergen Hoeller * @author Rossen Stoyanchev * @since 2.5.3 + * @see AbstractBindingResult */ @SuppressWarnings("serial") public abstract class AbstractErrors implements Errors, Serializable { @@ -81,8 +82,8 @@ protected void doSetNestedPath(@Nullable String nestedPath) { nestedPath = ""; } nestedPath = canonicalFieldName(nestedPath); - if (nestedPath.length() > 0 && !nestedPath.endsWith(Errors.NESTED_PATH_SEPARATOR)) { - nestedPath += Errors.NESTED_PATH_SEPARATOR; + if (nestedPath.length() > 0 && !nestedPath.endsWith(NESTED_PATH_SEPARATOR)) { + nestedPath += NESTED_PATH_SEPARATOR; } this.nestedPath = nestedPath; } @@ -97,7 +98,7 @@ protected String fixedField(@Nullable String field) { } else { String path = getNestedPath(); - return (path.endsWith(Errors.NESTED_PATH_SEPARATOR) ? + return (path.endsWith(NESTED_PATH_SEPARATOR) ? path.substring(0, path.length() - NESTED_PATH_SEPARATOR.length()) : path); } } @@ -201,9 +202,9 @@ public List getFieldErrors(String field) { List fieldErrors = getFieldErrors(); List result = new ArrayList<>(); String fixedField = fixedField(field); - for (FieldError error : fieldErrors) { - if (isMatchingFieldError(fixedField, error)) { - result.add(error); + for (FieldError fieldError : fieldErrors) { + if (isMatchingFieldError(fixedField, fieldError)) { + result.add(fieldError); } } return Collections.unmodifiableList(result); diff --git a/spring-context/src/main/java/org/springframework/validation/BeanPropertyBindingResult.java b/spring-context/src/main/java/org/springframework/validation/BeanPropertyBindingResult.java index 8558a2619c97..1cc6bb8e3654 100644 --- a/spring-context/src/main/java/org/springframework/validation/BeanPropertyBindingResult.java +++ b/spring-context/src/main/java/org/springframework/validation/BeanPropertyBindingResult.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,7 +55,7 @@ public class BeanPropertyBindingResult extends AbstractPropertyBindingResult imp /** - * Creates a new instance of the {@link BeanPropertyBindingResult} class. + * Create a new {@code BeanPropertyBindingResult} for the given target. * @param target the target bean to bind onto * @param objectName the name of the target object */ @@ -64,7 +64,7 @@ public BeanPropertyBindingResult(@Nullable Object target, String objectName) { } /** - * Creates a new instance of the {@link BeanPropertyBindingResult} class. + * Create a new {@code BeanPropertyBindingResult} for the given target. * @param target the target bean to bind onto * @param objectName the name of the target object * @param autoGrowNestedPaths whether to "auto-grow" a nested path that contains a null value diff --git a/spring-context/src/main/java/org/springframework/validation/BindException.java b/spring-context/src/main/java/org/springframework/validation/BindException.java index afc0c5c2ae71..b84c81081a87 100644 --- a/spring-context/src/main/java/org/springframework/validation/BindException.java +++ b/spring-context/src/main/java/org/springframework/validation/BindException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -128,7 +128,9 @@ public void rejectValue(@Nullable String field, String errorCode, String default } @Override - public void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + public void rejectValue(@Nullable String field, String errorCode, + @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + this.bindingResult.rejectValue(field, errorCode, errorArgs, defaultMessage); } diff --git a/spring-context/src/main/java/org/springframework/validation/DirectFieldBindingResult.java b/spring-context/src/main/java/org/springframework/validation/DirectFieldBindingResult.java index 5ad401de5bec..f01232ca1f6f 100644 --- a/spring-context/src/main/java/org/springframework/validation/DirectFieldBindingResult.java +++ b/spring-context/src/main/java/org/springframework/validation/DirectFieldBindingResult.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,7 +46,7 @@ public class DirectFieldBindingResult extends AbstractPropertyBindingResult { /** - * Create a new DirectFieldBindingResult instance. + * Create a new {@code DirectFieldBindingResult} for the given target. * @param target the target object to bind onto * @param objectName the name of the target object */ @@ -55,7 +55,7 @@ public DirectFieldBindingResult(@Nullable Object target, String objectName) { } /** - * Create a new DirectFieldBindingResult instance. + * Create a new {@code DirectFieldBindingResult} for the given target. * @param target the target object to bind onto * @param objectName the name of the target object * @param autoGrowNestedPaths whether to "auto-grow" a nested path that contains a null value diff --git a/spring-context/src/main/java/org/springframework/validation/Errors.java b/spring-context/src/main/java/org/springframework/validation/Errors.java index 45ae5d7b57af..18a7bc1910af 100644 --- a/spring-context/src/main/java/org/springframework/validation/Errors.java +++ b/spring-context/src/main/java/org/springframework/validation/Errors.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,24 +22,24 @@ import org.springframework.lang.Nullable; /** - * Stores and exposes information about data-binding and validation - * errors for a specific object. + * Stores and exposes information about data-binding and validation errors + * for a specific object. * - *

    Field names can be properties of the target object (e.g. "name" - * when binding to a customer object), or nested fields in case of - * subobjects (e.g. "address.street"). Supports subtree navigation - * via {@link #setNestedPath(String)}: for example, an - * {@code AddressValidator} validates "address", not being aware - * that this is a subobject of customer. + *

    Field names are typically properties of the target object (e.g. "name" + * when binding to a customer object). Implementations may also support nested + * fields in case of nested objects (e.g. "address.street"), in conjunction + * with subtree navigation via {@link #setNestedPath}: for example, an + * {@code AddressValidator} may validate "address", not being aware that this + * is a nested object of a top-level customer object. * *

    Note: {@code Errors} objects are single-threaded. * * @author Rod Johnson * @author Juergen Hoeller - * @see #setNestedPath - * @see BindException - * @see DataBinder + * @see Validator * @see ValidationUtils + * @see BindException + * @see BindingResult */ public interface Errors { @@ -66,6 +66,7 @@ public interface Errors { * @param nestedPath nested path within this object, * e.g. "address" (defaults to "", {@code null} is also acceptable). * Can end with a dot: both "address" and "address." are valid. + * @see #getNestedPath() */ void setNestedPath(String nestedPath); @@ -73,6 +74,7 @@ public interface Errors { * Return the current nested path of this {@link Errors} object. *

    Returns a nested path with a dot, i.e. "address.", for easy * building of concatenated paths. Default is an empty String. + * @see #setNestedPath(String) */ String getNestedPath(); @@ -86,14 +88,14 @@ public interface Errors { *

    For example: current path "spouse.", pushNestedPath("child") → * result path "spouse.child."; popNestedPath() → "spouse." again. * @param subPath the sub path to push onto the nested path stack - * @see #popNestedPath + * @see #popNestedPath() */ void pushNestedPath(String subPath); /** * Pop the former nested path from the nested path stack. * @throws IllegalStateException if there is no former nested path on the stack - * @see #pushNestedPath + * @see #pushNestedPath(String) */ void popNestedPath() throws IllegalStateException; @@ -101,6 +103,7 @@ public interface Errors { * Register a global error for the entire target object, * using the given error description. * @param errorCode error code, interpretable as a message key + * @see #reject(String, Object[], String) */ void reject(String errorCode); @@ -109,6 +112,7 @@ public interface Errors { * using the given error description. * @param errorCode error code, interpretable as a message key * @param defaultMessage fallback default message + * @see #reject(String, Object[], String) */ void reject(String errorCode, String defaultMessage); @@ -119,6 +123,7 @@ public interface Errors { * @param errorArgs error arguments, for argument binding via MessageFormat * (can be {@code null}) * @param defaultMessage fallback default message + * @see #rejectValue(String, String, Object[], String) */ void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage); @@ -132,7 +137,7 @@ public interface Errors { * global error if the current object is the top object. * @param field the field name (may be {@code null} or empty String) * @param errorCode error code, interpretable as a message key - * @see #getNestedPath() + * @see #rejectValue(String, String, Object[], String) */ void rejectValue(@Nullable String field, String errorCode); @@ -147,7 +152,7 @@ public interface Errors { * @param field the field name (may be {@code null} or empty String) * @param errorCode error code, interpretable as a message key * @param defaultMessage fallback default message - * @see #getNestedPath() + * @see #rejectValue(String, String, Object[], String) */ void rejectValue(@Nullable String field, String errorCode, String defaultMessage); @@ -164,7 +169,7 @@ public interface Errors { * @param errorArgs error arguments, for argument binding via MessageFormat * (can be {@code null}) * @param defaultMessage fallback default message - * @see #getNestedPath() + * @see #reject(String, Object[], String) */ void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage); @@ -179,35 +184,40 @@ void rejectValue(@Nullable String field, String errorCode, * to refer to the same target object, or at least contain compatible errors * that apply to the target object of this {@code Errors} instance. * @param errors the {@code Errors} instance to merge in + * @see #getAllErrors() */ void addAllErrors(Errors errors); /** - * Return if there were any errors. + * Determine if there were any errors. + * @see #hasGlobalErrors() + * @see #hasFieldErrors() */ boolean hasErrors(); /** - * Return the total number of errors. + * Determine the total number of errors. + * @see #getGlobalErrorCount() + * @see #getFieldErrorCount() */ int getErrorCount(); /** * Get all errors, both global and field ones. - * @return a list of {@link ObjectError} instances + * @return a list of {@link ObjectError}/{@link FieldError} instances + * @see #getGlobalErrors() + * @see #getFieldErrors() */ List getAllErrors(); /** - * Are there any global errors? - * @return {@code true} if there are any global errors + * Determine if there were any global errors. * @see #hasFieldErrors() */ boolean hasGlobalErrors(); /** - * Return the number of global errors. - * @return the number of global errors + * Determine the number of global errors. * @see #getFieldErrorCount() */ int getGlobalErrorCount(); @@ -215,26 +225,26 @@ void rejectValue(@Nullable String field, String errorCode, /** * Get all global errors. * @return a list of {@link ObjectError} instances + * @see #getFieldErrors() */ List getGlobalErrors(); /** * Get the first global error, if any. * @return the global error, or {@code null} + * @see #getFieldError() */ @Nullable ObjectError getGlobalError(); /** - * Are there any field errors? - * @return {@code true} if there are any errors associated with a field + * Determine if there were any errors associated with a field. * @see #hasGlobalErrors() */ boolean hasFieldErrors(); /** - * Return the number of errors associated with a field. - * @return the number of errors associated with a field + * Determine the number of errors associated with a field. * @see #getGlobalErrorCount() */ int getFieldErrorCount(); @@ -242,36 +252,39 @@ void rejectValue(@Nullable String field, String errorCode, /** * Get all errors associated with a field. * @return a List of {@link FieldError} instances + * @see #getGlobalErrors() */ List getFieldErrors(); /** * Get the first error associated with a field, if any. * @return the field-specific error, or {@code null} + * @see #getGlobalError() */ @Nullable FieldError getFieldError(); /** - * Are there any errors associated with the given field? + * Determine if there were any errors associated with the given field. * @param field the field name - * @return {@code true} if there were any errors associated with the given field + * @see #hasFieldErrors() */ boolean hasFieldErrors(String field); /** - * Return the number of errors associated with the given field. + * Determine the number of errors associated with the given field. * @param field the field name - * @return the number of errors associated with the given field + * @see #getFieldErrorCount() */ int getFieldErrorCount(String field); /** * Get all errors associated with the given field. - *

    Implementations should support not only full field names like - * "name" but also pattern matches like "na*" or "address.*". + *

    Implementations may support not only full field names like + * "address.street" but also pattern matches like "address.*". * @param field the field name * @return a List of {@link FieldError} instances + * @see #getFieldErrors() */ List getFieldErrors(String field); @@ -279,6 +292,7 @@ void rejectValue(@Nullable String field, String errorCode, * Get the first error associated with the given field, if any. * @param field the field name * @return the field-specific error, or {@code null} + * @see #getFieldError() */ @Nullable FieldError getFieldError(String field); @@ -290,17 +304,19 @@ void rejectValue(@Nullable String field, String errorCode, * even if there were type mismatches. * @param field the field name * @return the current value of the given field + * @see #getFieldType(String) */ @Nullable Object getFieldValue(String field); /** - * Return the type of a given field. + * Determine the type of the given field, as far as possible. *

    Implementations should be able to determine the type even * when the field value is {@code null}, for example from some * associated descriptor. * @param field the field name * @return the type of the field, or {@code null} if not determinable + * @see #getFieldValue(String) */ @Nullable Class getFieldType(String field); diff --git a/spring-context/src/main/java/org/springframework/validation/Validator.java b/spring-context/src/main/java/org/springframework/validation/Validator.java index b67b6d5d8b77..2aa282396358 100644 --- a/spring-context/src/main/java/org/springframework/validation/Validator.java +++ b/spring-context/src/main/java/org/springframework/validation/Validator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,14 +54,14 @@ * } * }

    * - *

    See also the Spring reference manual for a fuller discussion of - * the {@code Validator} interface and its role in an enterprise - * application. + *

    See also the Spring reference manual for a fuller discussion of the + * {@code Validator} interface and its role in an enterprise application. * * @author Rod Johnson * @see SmartValidator * @see Errors * @see ValidationUtils + * @see DataBinder#setValidator */ public interface Validator { @@ -81,11 +81,14 @@ public interface Validator { boolean supports(Class clazz); /** - * Validate the supplied {@code target} object, which must be - * of a {@link Class} for which the {@link #supports(Class)} method - * typically has (or would) return {@code true}. + * Validate the given {@code target} object which must be of a + * {@link Class} for which the {@link #supports(Class)} method + * typically has returned (or would return) {@code true}. *

    The supplied {@link Errors errors} instance can be used to report - * any resulting validation errors. + * any resulting validation errors, typically as part of a larger + * binding process which this validator is meant to participate in. + * Binding errors have typically been pre-registered with the + * {@link Errors errors} instance before this invocation already. * @param target the object that is to be validated * @param errors contextual state about the validation process * @see ValidationUtils diff --git a/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java b/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java index 546c599c01f7..037dc8d214a3 100644 --- a/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java +++ b/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -95,11 +95,11 @@ void bindingNoErrors() throws BindException { binder.bind(pvs); binder.close(); - assertThat(rod.getName().equals("Rod")).as("changed name correctly").isTrue(); - assertThat(rod.getAge() == 32).as("changed age correctly").isTrue(); + assertThat(rod.getName()).as("changed name correctly").isEqualTo("Rod"); + assertThat(rod.getAge()).as("changed age correctly").isEqualTo(32); Map map = binder.getBindingResult().getModel(); - assertThat(map.size() == 2).as("There is one element in map").isTrue(); + assertThat(map).as("There is one element in map").hasSize(2); TestBean tb = (TestBean) map.get("person"); assertThat(tb.equals(rod)).as("Same object").isTrue(); @@ -157,8 +157,9 @@ void bindingNoErrorsNotIgnoreUnknown() { pvs.add("name", "Rod"); pvs.add("age", 32); pvs.add("nonExisting", "someValue"); - assertThatExceptionOfType(NotWritablePropertyException.class).isThrownBy(() -> - binder.bind(pvs)); + + assertThatExceptionOfType(NotWritablePropertyException.class) + .isThrownBy(() -> binder.bind(pvs)); } @Test @@ -168,8 +169,9 @@ void bindingNoErrorsWithInvalidField() { MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("name", "Rod"); pvs.add("spouse.age", 32); - assertThatExceptionOfType(NullValueInNestedPathException.class).isThrownBy(() -> - binder.bind(pvs)); + + assertThatExceptionOfType(NullValueInNestedPathException.class) + .isThrownBy(() -> binder.bind(pvs)); } @Test @@ -197,57 +199,56 @@ void bindingWithErrors() { pvs.add("age", "32x"); pvs.add("touchy", "m.y"); binder.bind(pvs); - assertThatExceptionOfType(BindException.class).isThrownBy( - binder::close) - .satisfies(ex -> { - assertThat(rod.getName()).isEqualTo("Rod"); - Map map = binder.getBindingResult().getModel(); - TestBean tb = (TestBean) map.get("person"); - assertThat(tb).isSameAs(rod); - - BindingResult br = (BindingResult) map.get(BindingResult.MODEL_KEY_PREFIX + "person"); - assertThat(BindingResultUtils.getBindingResult(map, "person")).isEqualTo(br); - assertThat(BindingResultUtils.getRequiredBindingResult(map, "person")).isEqualTo(br); - - assertThat(BindingResultUtils.getBindingResult(map, "someOtherName")).isNull(); - assertThatIllegalStateException().isThrownBy(() -> - BindingResultUtils.getRequiredBindingResult(map, "someOtherName")); - - assertThat(binder.getBindingResult()).as("Added itself to map").isSameAs(br); - assertThat(br.hasErrors()).isTrue(); - assertThat(br.getErrorCount()).isEqualTo(2); - - assertThat(br.hasFieldErrors("age")).isTrue(); - assertThat(br.getFieldErrorCount("age")).isEqualTo(1); - assertThat(binder.getBindingResult().getFieldValue("age")).isEqualTo("32x"); - FieldError ageError = binder.getBindingResult().getFieldError("age"); - assertThat(ageError).isNotNull(); - assertThat(ageError.getCode()).isEqualTo("typeMismatch"); - assertThat(ageError.getRejectedValue()).isEqualTo("32x"); - assertThat(ageError.contains(TypeMismatchException.class)).isTrue(); - assertThat(ageError.contains(NumberFormatException.class)).isTrue(); - assertThat(ageError.unwrap(NumberFormatException.class).getMessage()).contains("32x"); - assertThat(tb.getAge()).isEqualTo(0); - - assertThat(br.hasFieldErrors("touchy")).isTrue(); - assertThat(br.getFieldErrorCount("touchy")).isEqualTo(1); - assertThat(binder.getBindingResult().getFieldValue("touchy")).isEqualTo("m.y"); - FieldError touchyError = binder.getBindingResult().getFieldError("touchy"); - assertThat(touchyError).isNotNull(); - assertThat(touchyError.getCode()).isEqualTo("methodInvocation"); - assertThat(touchyError.getRejectedValue()).isEqualTo("m.y"); - assertThat(touchyError.contains(MethodInvocationException.class)).isTrue(); - assertThat(touchyError.unwrap(MethodInvocationException.class).getCause().getMessage()).contains("a ."); - assertThat(tb.getTouchy()).isNull(); - - DataBinder binder2 = new DataBinder(new TestBean(), "person"); - MutablePropertyValues pvs2 = new MutablePropertyValues(); - pvs2.add("name", "Rod"); - pvs2.add("age", "32x"); - pvs2.add("touchy", "m.y"); - binder2.bind(pvs2); - assertThat(ex.getBindingResult()).isEqualTo(binder2.getBindingResult()); - }); + + assertThatExceptionOfType(BindException.class).isThrownBy(binder::close).satisfies(ex -> { + assertThat(rod.getName()).isEqualTo("Rod"); + Map map = binder.getBindingResult().getModel(); + TestBean tb = (TestBean) map.get("person"); + assertThat(tb).isSameAs(rod); + + BindingResult br = (BindingResult) map.get(BindingResult.MODEL_KEY_PREFIX + "person"); + assertThat(BindingResultUtils.getBindingResult(map, "person")).isEqualTo(br); + assertThat(BindingResultUtils.getRequiredBindingResult(map, "person")).isEqualTo(br); + + assertThat(BindingResultUtils.getBindingResult(map, "someOtherName")).isNull(); + assertThatIllegalStateException().isThrownBy(() -> + BindingResultUtils.getRequiredBindingResult(map, "someOtherName")); + + assertThat(binder.getBindingResult()).as("Added itself to map").isSameAs(br); + assertThat(br.hasErrors()).isTrue(); + assertThat(br.getErrorCount()).isEqualTo(2); + + assertThat(br.hasFieldErrors("age")).isTrue(); + assertThat(br.getFieldErrorCount("age")).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldValue("age")).isEqualTo("32x"); + FieldError ageError = binder.getBindingResult().getFieldError("age"); + assertThat(ageError).isNotNull(); + assertThat(ageError.getCode()).isEqualTo("typeMismatch"); + assertThat(ageError.getRejectedValue()).isEqualTo("32x"); + assertThat(ageError.contains(TypeMismatchException.class)).isTrue(); + assertThat(ageError.contains(NumberFormatException.class)).isTrue(); + assertThat(ageError.unwrap(NumberFormatException.class).getMessage()).contains("32x"); + assertThat(tb.getAge()).isEqualTo(0); + + assertThat(br.hasFieldErrors("touchy")).isTrue(); + assertThat(br.getFieldErrorCount("touchy")).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldValue("touchy")).isEqualTo("m.y"); + FieldError touchyError = binder.getBindingResult().getFieldError("touchy"); + assertThat(touchyError).isNotNull(); + assertThat(touchyError.getCode()).isEqualTo("methodInvocation"); + assertThat(touchyError.getRejectedValue()).isEqualTo("m.y"); + assertThat(touchyError.contains(MethodInvocationException.class)).isTrue(); + assertThat(touchyError.unwrap(MethodInvocationException.class).getCause().getMessage()).contains("a ."); + assertThat(tb.getTouchy()).isNull(); + + DataBinder binder2 = new DataBinder(new TestBean(), "person"); + MutablePropertyValues pvs2 = new MutablePropertyValues(); + pvs2.add("name", "Rod"); + pvs2.add("age", "32x"); + pvs2.add("touchy", "m.y"); + binder2.bind(pvs2); + assertThat(ex.getBindingResult()).isEqualTo(binder2.getBindingResult()); + }); } @Test @@ -257,15 +258,17 @@ void bindingWithSystemFieldError() { MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("class.classLoader.URLs[0]", "https://myserver"); binder.setIgnoreUnknownFields(false); - assertThatExceptionOfType(NotWritablePropertyException.class).isThrownBy(() -> - binder.bind(pvs)) - .withMessageContaining("classLoader"); + + assertThatExceptionOfType(NotWritablePropertyException.class) + .isThrownBy(() -> binder.bind(pvs)) + .withMessageContaining("classLoader"); } @Test void bindingWithErrorsAndCustomEditors() { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); + binder.registerCustomEditor(String.class, "touchy", new PropertyEditorSupport() { @Override public void setAsText(String text) throws IllegalArgumentException { @@ -286,6 +289,7 @@ public String getAsText() { return ((TestBean) getValue()).getName(); } }); + MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("name", "Rod"); pvs.add("age", "32x"); @@ -293,41 +297,39 @@ public String getAsText() { pvs.add("spouse", "Kerry"); binder.bind(pvs); - assertThatExceptionOfType(BindException.class).isThrownBy( - binder::close) - .satisfies(ex -> { - assertThat(rod.getName()).isEqualTo("Rod"); - Map model = binder.getBindingResult().getModel(); - TestBean tb = (TestBean) model.get("person"); - assertThat(tb).isEqualTo(rod); - - BindingResult br = (BindingResult) model.get(BindingResult.MODEL_KEY_PREFIX + "person"); - assertThat(binder.getBindingResult()).isSameAs(br); - assertThat(br.hasErrors()).isTrue(); - assertThat(br.getErrorCount()).isEqualTo(2); - - assertThat(br.hasFieldErrors("age")).isTrue(); - assertThat(br.getFieldErrorCount("age")).isEqualTo(1); - assertThat(binder.getBindingResult().getFieldValue("age")).isEqualTo("32x"); - FieldError ageError = binder.getBindingResult().getFieldError("age"); - assertThat(ageError).isNotNull(); - assertThat(ageError.getCode()).isEqualTo("typeMismatch"); - assertThat(ageError.getRejectedValue()).isEqualTo("32x"); - assertThat(tb.getAge()).isEqualTo(0); - - assertThat(br.hasFieldErrors("touchy")).isTrue(); - assertThat(br.getFieldErrorCount("touchy")).isEqualTo(1); - assertThat(binder.getBindingResult().getFieldValue("touchy")).isEqualTo("m.y"); - FieldError touchyError = binder.getBindingResult().getFieldError("touchy"); - assertThat(touchyError).isNotNull(); - assertThat(touchyError.getCode()).isEqualTo("methodInvocation"); - assertThat(touchyError.getRejectedValue()).isEqualTo("m.y"); - assertThat(tb.getTouchy()).isNull(); - - assertThat(br.hasFieldErrors("spouse")).isFalse(); - assertThat(binder.getBindingResult().getFieldValue("spouse")).isEqualTo("Kerry"); - assertThat(tb.getSpouse()).isNotNull(); - }); + assertThatExceptionOfType(BindException.class).isThrownBy(binder::close).satisfies(ex -> { + assertThat(rod.getName()).isEqualTo("Rod"); + Map model = binder.getBindingResult().getModel(); + TestBean tb = (TestBean) model.get("person"); + assertThat(tb).isEqualTo(rod); + + BindingResult br = (BindingResult) model.get(BindingResult.MODEL_KEY_PREFIX + "person"); + assertThat(binder.getBindingResult()).isSameAs(br); + assertThat(br.hasErrors()).isTrue(); + assertThat(br.getErrorCount()).isEqualTo(2); + + assertThat(br.hasFieldErrors("age")).isTrue(); + assertThat(br.getFieldErrorCount("age")).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldValue("age")).isEqualTo("32x"); + FieldError ageError = binder.getBindingResult().getFieldError("age"); + assertThat(ageError).isNotNull(); + assertThat(ageError.getCode()).isEqualTo("typeMismatch"); + assertThat(ageError.getRejectedValue()).isEqualTo("32x"); + assertThat(tb.getAge()).isEqualTo(0); + + assertThat(br.hasFieldErrors("touchy")).isTrue(); + assertThat(br.getFieldErrorCount("touchy")).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldValue("touchy")).isEqualTo("m.y"); + FieldError touchyError = binder.getBindingResult().getFieldError("touchy"); + assertThat(touchyError).isNotNull(); + assertThat(touchyError.getCode()).isEqualTo("methodInvocation"); + assertThat(touchyError.getRejectedValue()).isEqualTo("m.y"); + assertThat(tb.getTouchy()).isNull(); + + assertThat(br.hasFieldErrors("spouse")).isFalse(); + assertThat(binder.getBindingResult().getFieldValue("spouse")).isEqualTo("Kerry"); + assertThat(tb.getSpouse()).isNotNull(); + }); } @Test @@ -576,7 +578,7 @@ void bindingWithCustomFormatter() { editor = binder.getBindingResult().findEditor("myFloat", null); assertThat(editor).isNotNull(); editor.setAsText("1,6"); - assertThat(((Number) editor.getValue()).floatValue() == 1.6f).isTrue(); + assertThat(((Number) editor.getValue()).floatValue()).isEqualTo(1.6f); } finally { LocaleContextHolder.resetLocaleContext(); @@ -752,15 +754,15 @@ void bindingWithAllowedFieldsUsingAsterisks() throws BindException { binder.bind(pvs); binder.close(); - assertThat("Rod".equals(rod.getName())).as("changed name correctly").isTrue(); - assertThat("Rod".equals(rod.getTouchy())).as("changed touchy correctly").isTrue(); - assertThat(rod.getAge() == 0).as("did not change age").isTrue(); + assertThat(rod.getName()).as("changed name correctly").isEqualTo("Rod"); + assertThat(rod.getTouchy()).as("changed touchy correctly").isEqualTo("Rod"); + assertThat(rod.getAge()).as("did not change age").isEqualTo(0); String[] disallowedFields = binder.getBindingResult().getSuppressedFields(); assertThat(disallowedFields).hasSize(1); assertThat(disallowedFields[0]).isEqualTo("age"); Map m = binder.getBindingResult().getModel(); - assertThat(m.size() == 2).as("There is one element in map").isTrue(); + assertThat(m).as("There is one element in map").hasSize(2); TestBean tb = (TestBean) m.get("person"); assertThat(tb.equals(rod)).as("Same object").isTrue(); } @@ -914,7 +916,7 @@ public String getAsText() { binder.getBindingResult().rejectValue("touchy", "someCode", "someMessage"); binder.getBindingResult().rejectValue("spouse.name", "someCode", "someMessage"); - assertThat(binder.getBindingResult().getNestedPath()).isEqualTo(""); + assertThat(binder.getBindingResult().getNestedPath()).isEmpty(); assertThat(binder.getBindingResult().getFieldValue("name")).isEqualTo("value"); assertThat(binder.getBindingResult().getFieldError("name").getRejectedValue()).isEqualTo("prefixvalue"); assertThat(tb.getName()).isEqualTo("prefixvalue"); @@ -1010,7 +1012,7 @@ public String print(String object, Locale locale) { binder.getBindingResult().rejectValue("touchy", "someCode", "someMessage"); binder.getBindingResult().rejectValue("spouse.name", "someCode", "someMessage"); - assertThat(binder.getBindingResult().getNestedPath()).isEqualTo(""); + assertThat(binder.getBindingResult().getNestedPath()).isEmpty(); assertThat(binder.getBindingResult().getFieldValue("name")).isEqualTo("value"); assertThat(binder.getBindingResult().getFieldError("name").getRejectedValue()).isEqualTo("prefixvalue"); assertThat(tb.getName()).isEqualTo("prefixvalue"); @@ -1134,12 +1136,11 @@ void validatorNoErrors() throws Exception { tb2.setAge(34); tb.setSpouse(tb2); DataBinder db = new DataBinder(tb, "tb"); + db.setValidator(new TestBeanValidator()); MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("spouse.age", "argh"); db.bind(pvs); Errors errors = db.getBindingResult(); - Validator testValidator = new TestBeanValidator(); - testValidator.validate(tb, errors); errors.setNestedPath("spouse"); assertThat(errors.getNestedPath()).isEqualTo("spouse."); @@ -1148,7 +1149,7 @@ void validatorNoErrors() throws Exception { spouseValidator.validate(tb.getSpouse(), errors); errors.setNestedPath(""); - assertThat(errors.getNestedPath()).isEqualTo(""); + assertThat(errors.getNestedPath()).isEmpty(); errors.pushNestedPath("spouse"); assertThat(errors.getNestedPath()).isEqualTo("spouse."); errors.pushNestedPath("spouse"); @@ -1156,7 +1157,7 @@ void validatorNoErrors() throws Exception { errors.popNestedPath(); assertThat(errors.getNestedPath()).isEqualTo("spouse."); errors.popNestedPath(); - assertThat(errors.getNestedPath()).isEqualTo(""); + assertThat(errors.getNestedPath()).isEmpty(); try { errors.popNestedPath(); } @@ -1166,7 +1167,7 @@ void validatorNoErrors() throws Exception { errors.pushNestedPath("spouse"); assertThat(errors.getNestedPath()).isEqualTo("spouse."); errors.setNestedPath(""); - assertThat(errors.getNestedPath()).isEqualTo(""); + assertThat(errors.getNestedPath()).isEmpty(); try { errors.popNestedPath(); } @@ -1187,8 +1188,7 @@ void validatorNoErrors() throws Exception { void validatorWithErrors() { TestBean tb = new TestBean(); tb.setSpouse(new TestBean()); - - Errors errors = new BeanPropertyBindingResult(tb, "tb"); + Errors errors = new DataBinder(tb, "tb").getBindingResult(); Validator testValidator = new TestBeanValidator(); testValidator.validate(tb, errors); @@ -1201,7 +1201,11 @@ void validatorWithErrors() { errors.setNestedPath(""); assertThat(errors.hasErrors()).isTrue(); assertThat(errors.getErrorCount()).isEqualTo(6); + assertThat(errors.getAllErrors()) + .containsAll(errors.getGlobalErrors()) + .containsAll(errors.getFieldErrors()); + assertThat(errors.hasGlobalErrors()).isTrue(); assertThat(errors.getGlobalErrorCount()).isEqualTo(2); assertThat(errors.getGlobalError().getCode()).isEqualTo("NAME_TOUCHY_MISMATCH"); assertThat((errors.getGlobalErrors().get(0)).getCode()).isEqualTo("NAME_TOUCHY_MISMATCH"); @@ -1257,10 +1261,11 @@ void validatorWithErrorsAndCodesPrefix() { TestBean tb = new TestBean(); tb.setSpouse(new TestBean()); - BeanPropertyBindingResult errors = new BeanPropertyBindingResult(tb, "tb"); + DataBinder dataBinder = new DataBinder(tb, "tb"); DefaultMessageCodesResolver codesResolver = new DefaultMessageCodesResolver(); codesResolver.setPrefix("validation."); - errors.setMessageCodesResolver(codesResolver); + dataBinder.setMessageCodesResolver(codesResolver); + Errors errors = dataBinder.getBindingResult(); Validator testValidator = new TestBeanValidator(); testValidator.validate(tb, errors); @@ -1273,7 +1278,11 @@ void validatorWithErrorsAndCodesPrefix() { errors.setNestedPath(""); assertThat(errors.hasErrors()).isTrue(); assertThat(errors.getErrorCount()).isEqualTo(6); + assertThat(errors.getAllErrors()) + .containsAll(errors.getGlobalErrors()) + .containsAll(errors.getFieldErrors()); + assertThat(errors.hasGlobalErrors()).isTrue(); assertThat(errors.getGlobalErrorCount()).isEqualTo(2); assertThat(errors.getGlobalError().getCode()).isEqualTo("validation.NAME_TOUCHY_MISMATCH"); assertThat((errors.getGlobalErrors().get(0)).getCode()).isEqualTo("validation.NAME_TOUCHY_MISMATCH"); @@ -1327,9 +1336,11 @@ void validatorWithErrorsAndCodesPrefix() { @Test void validatorWithNestedObjectNull() { TestBean tb = new TestBean(); - Errors errors = new BeanPropertyBindingResult(tb, "tb"); + Errors errors = new DataBinder(tb, "tb").getBindingResult(); + Validator testValidator = new TestBeanValidator(); testValidator.validate(tb, errors); + errors.setNestedPath("spouse."); assertThat(errors.getNestedPath()).isEqualTo("spouse."); Validator spouseValidator = new SpouseValidator(); @@ -1724,7 +1735,7 @@ public void setAsText(String text) throws IllegalArgumentException { pvs.add("stringArray", new String[] {"a1", "b2"}); binder.bind(pvs); assertThat(binder.getBindingResult().hasErrors()).isFalse(); - assertThat(tb.getStringArray().length).isEqualTo(2); + assertThat(tb.getStringArray()).hasSize(2); assertThat(tb.getStringArray()[0]).isEqualTo("Xa1"); assertThat(tb.getStringArray()[1]).isEqualTo("Xb2"); } @@ -1800,16 +1811,16 @@ void rejectWithoutDefaultMessage() { tb.setName("myName"); tb.setAge(99); - BeanPropertyBindingResult ex = new BeanPropertyBindingResult(tb, "tb"); - ex.reject("invalid"); - ex.rejectValue("age", "invalidField"); + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(tb, "tb"); + errors.reject("invalid"); + errors.rejectValue("age", "invalidField"); StaticMessageSource ms = new StaticMessageSource(); ms.addMessage("invalid", Locale.US, "general error"); ms.addMessage("invalidField", Locale.US, "invalid field"); - assertThat(ms.getMessage(ex.getGlobalError(), Locale.US)).isEqualTo("general error"); - assertThat(ms.getMessage(ex.getFieldError("age"), Locale.US)).isEqualTo("invalid field"); + assertThat(ms.getMessage(errors.getGlobalError(), Locale.US)).isEqualTo("general error"); + assertThat(ms.getMessage(errors.getFieldError("age"), Locale.US)).isEqualTo("invalid field"); } @Test @@ -1877,13 +1888,13 @@ void autoGrowWithinDefaultLimit() { void autoGrowBeyondDefaultLimit() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); - MutablePropertyValues mpvs = new MutablePropertyValues(); mpvs.add("friends[256]", ""); + assertThatExceptionOfType(InvalidPropertyException.class) - .isThrownBy(() -> binder.bind(mpvs)) - .havingRootCause() - .isInstanceOf(IndexOutOfBoundsException.class); + .isThrownBy(() -> binder.bind(mpvs)) + .havingRootCause() + .isInstanceOf(IndexOutOfBoundsException.class); } @Test @@ -1904,13 +1915,13 @@ void autoGrowBeyondCustomLimit() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); binder.setAutoGrowCollectionLimit(10); - MutablePropertyValues mpvs = new MutablePropertyValues(); mpvs.add("friends[16]", ""); + assertThatExceptionOfType(InvalidPropertyException.class) - .isThrownBy(() -> binder.bind(mpvs)) - .havingRootCause() - .isInstanceOf(IndexOutOfBoundsException.class); + .isThrownBy(() -> binder.bind(mpvs)) + .havingRootCause() + .isInstanceOf(IndexOutOfBoundsException.class); } @Test @@ -1926,7 +1937,7 @@ void nestedGrowingList() { List list = (List) form.getF().get("list"); assertThat(list.get(0)).isEqualTo("firstValue"); assertThat(list.get(1)).isEqualTo("secondValue"); - assertThat(list.size()).isEqualTo(2); + assertThat(list).hasSize(2); } @Test @@ -1959,7 +1970,7 @@ void setAutoGrowCollectionLimit() { pvs.add("integerList[256]", "1"); binder.bind(pvs); - assertThat(tb.getIntegerList().size()).isEqualTo(257); + assertThat(tb.getIntegerList()).hasSize(257); assertThat(tb.getIntegerList().get(256)).isEqualTo(Integer.valueOf(1)); assertThat(binder.getBindingResult().getFieldValue("integerList[256]")).isEqualTo(1); } diff --git a/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java b/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java index 0a027b95df43..1131a2d645c6 100644 --- a/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java +++ b/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ public class ValidationUtilsTests { @Test - public void testInvokeValidatorWithNullValidator() throws Exception { + public void testInvokeValidatorWithNullValidator() { TestBean tb = new TestBean(); Errors errors = new BeanPropertyBindingResult(tb, "tb"); assertThatIllegalArgumentException().isThrownBy(() -> @@ -43,14 +43,14 @@ public void testInvokeValidatorWithNullValidator() throws Exception { } @Test - public void testInvokeValidatorWithNullErrors() throws Exception { + public void testInvokeValidatorWithNullErrors() { TestBean tb = new TestBean(); assertThatIllegalArgumentException().isThrownBy(() -> ValidationUtils.invokeValidator(new EmptyValidator(), tb, null)); } @Test - public void testInvokeValidatorSunnyDay() throws Exception { + public void testInvokeValidatorSunnyDay() { TestBean tb = new TestBean(); Errors errors = new BeanPropertyBindingResult(tb, "tb"); ValidationUtils.invokeValidator(new EmptyValidator(), tb, errors); @@ -59,7 +59,7 @@ public void testInvokeValidatorSunnyDay() throws Exception { } @Test - public void testValidationUtilsSunnyDay() throws Exception { + public void testValidationUtilsSunnyDay() { TestBean tb = new TestBean(""); Validator testValidator = new EmptyValidator(); @@ -75,7 +75,7 @@ public void testValidationUtilsSunnyDay() throws Exception { } @Test - public void testValidationUtilsNull() throws Exception { + public void testValidationUtilsNull() { TestBean tb = new TestBean(); Errors errors = new BeanPropertyBindingResult(tb, "tb"); Validator testValidator = new EmptyValidator(); @@ -85,7 +85,7 @@ public void testValidationUtilsNull() throws Exception { } @Test - public void testValidationUtilsEmpty() throws Exception { + public void testValidationUtilsEmpty() { TestBean tb = new TestBean(""); Errors errors = new BeanPropertyBindingResult(tb, "tb"); Validator testValidator = new EmptyValidator(); @@ -113,7 +113,7 @@ public void testValidationUtilsEmptyVariants() { } @Test - public void testValidationUtilsEmptyOrWhitespace() throws Exception { + public void testValidationUtilsEmptyOrWhitespace() { TestBean tb = new TestBean(); Validator testValidator = new EmptyOrWhitespaceValidator(); diff --git a/spring-web/src/main/java/org/springframework/web/bind/EscapedErrors.java b/spring-web/src/main/java/org/springframework/web/bind/EscapedErrors.java index a2cb47e4787a..b9a75b541a44 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/EscapedErrors.java +++ b/spring-web/src/main/java/org/springframework/web/bind/EscapedErrors.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -110,8 +110,8 @@ public void rejectValue(@Nullable String field, String errorCode, String default } @Override - public void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, - @Nullable String defaultMessage) { + public void rejectValue(@Nullable String field, String errorCode, + @Nullable Object[] errorArgs, @Nullable String defaultMessage) { this.source.rejectValue(field, errorCode, errorArgs, defaultMessage); } diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java index 8c605fc66254..d89aac94d1d1 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,14 +54,13 @@ public WebExchangeBindException(MethodParameter parameter, BindingResult binding /** * Return the BindingResult that this BindException wraps. - * Will typically be a BeanPropertyBindingResult. + *

    Will typically be a BeanPropertyBindingResult. * @see BeanPropertyBindingResult */ public final BindingResult getBindingResult() { return this.bindingResult; } - @Override public String getObjectName() { return this.bindingResult.getObjectName(); @@ -87,7 +86,6 @@ public void popNestedPath() throws IllegalStateException { this.bindingResult.popNestedPath(); } - @Override public void reject(String errorCode) { this.bindingResult.reject(errorCode); @@ -114,8 +112,8 @@ public void rejectValue(@Nullable String field, String errorCode, String default } @Override - public void rejectValue( - @Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + public void rejectValue(@Nullable String field, String errorCode, + @Nullable Object[] errorArgs, @Nullable String defaultMessage) { this.bindingResult.rejectValue(field, errorCode, errorArgs, defaultMessage); } @@ -125,7 +123,6 @@ public void addAllErrors(Errors errors) { this.bindingResult.addAllErrors(errors); } - @Override public boolean hasErrors() { return this.bindingResult.hasErrors(); @@ -276,7 +273,6 @@ public String[] getSuppressedFields() { return this.bindingResult.getSuppressedFields(); } - /** * Returns diagnostic information about the errors held in this object. */ @@ -287,8 +283,8 @@ public String getMessage() { StringBuilder sb = new StringBuilder("Validation failed for argument at index ") .append(parameter.getParameterIndex()).append(" in method: ") .append(parameter.getExecutable().toGenericString()) - .append(", with ").append(this.bindingResult.getErrorCount()).append(" error(s): "); - for (ObjectError error : this.bindingResult.getAllErrors()) { + .append(", with ").append(getErrorCount()).append(" error(s): "); + for (ObjectError error : getAllErrors()) { sb.append('[').append(error).append("] "); } return sb.toString(); From 790abeda1c9f6bb64f15bb3a20f677f4ed4905be Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 21 Jul 2023 20:36:43 +0200 Subject: [PATCH 11/47] Polishing --- .../cache/caffeine/CaffeineCacheManager.java | 8 ++--- .../caffeine/CaffeineCacheManagerTests.java | 34 +++++++++---------- .../java/org/springframework/cache/Cache.java | 14 +++++--- .../interceptor/AbstractCacheInvoker.java | 8 ++--- .../cache/interceptor/CacheAspectSupport.java | 22 ++++++------ .../cache/support/SimpleCacheManager.java | 8 +++-- .../cache/CacheReproTests.java | 6 +++- 7 files changed, 56 insertions(+), 44 deletions(-) diff --git a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java index 239a7350cdc5..e80cb747f396 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java +++ b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java @@ -110,7 +110,7 @@ public void setCacheNames(@Nullable Collection cacheNames) { * Set the Caffeine to use for building each individual * {@link CaffeineCache} instance. * @see #createNativeCaffeineCache - * @see com.github.benmanes.caffeine.cache.Caffeine#build() + * @see Caffeine#build() */ public void setCaffeine(Caffeine caffeine) { Assert.notNull(caffeine, "Caffeine must not be null"); @@ -121,7 +121,7 @@ public void setCaffeine(Caffeine caffeine) { * Set the {@link CaffeineSpec} to use for building each individual * {@link CaffeineCache} instance. * @see #createNativeCaffeineCache - * @see com.github.benmanes.caffeine.cache.Caffeine#from(CaffeineSpec) + * @see Caffeine#from(CaffeineSpec) */ public void setCaffeineSpec(CaffeineSpec caffeineSpec) { doSetCaffeine(Caffeine.from(caffeineSpec)); @@ -132,7 +132,7 @@ public void setCaffeineSpec(CaffeineSpec caffeineSpec) { * individual {@link CaffeineCache} instance. The given value needs to * comply with Caffeine's {@link CaffeineSpec} (see its javadoc). * @see #createNativeCaffeineCache - * @see com.github.benmanes.caffeine.cache.Caffeine#from(String) + * @see Caffeine#from(String) */ public void setCacheSpecification(String cacheSpecification) { doSetCaffeine(Caffeine.from(cacheSpecification)); @@ -149,7 +149,7 @@ private void doSetCaffeine(Caffeine cacheBuilder) { * Set the Caffeine CacheLoader to use for building each individual * {@link CaffeineCache} instance, turning it into a LoadingCache. * @see #createNativeCaffeineCache - * @see com.github.benmanes.caffeine.cache.Caffeine#build(CacheLoader) + * @see Caffeine#build(CacheLoader) * @see com.github.benmanes.caffeine.cache.LoadingCache */ public void setCacheLoader(CacheLoader cacheLoader) { diff --git a/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java index f8c0de21f2d7..94ffbd245c2e 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java @@ -38,19 +38,17 @@ public class CaffeineCacheManagerTests { @Test public void testDynamicMode() { CacheManager cm = new CaffeineCacheManager(); + Cache cache1 = cm.getCache("c1"); - boolean condition2 = cache1 instanceof CaffeineCache; - assertThat(condition2).isTrue(); + assertThat(cache1).isInstanceOf(CaffeineCache.class); Cache cache1again = cm.getCache("c1"); assertThat(cache1).isSameAs(cache1again); Cache cache2 = cm.getCache("c2"); - boolean condition1 = cache2 instanceof CaffeineCache; - assertThat(condition1).isTrue(); + assertThat(cache2).isInstanceOf(CaffeineCache.class); Cache cache2again = cm.getCache("c2"); assertThat(cache2).isSameAs(cache2again); Cache cache3 = cm.getCache("c3"); - boolean condition = cache3 instanceof CaffeineCache; - assertThat(condition).isTrue(); + assertThat(cache3).isInstanceOf(CaffeineCache.class); Cache cache3again = cm.getCache("c3"); assertThat(cache3).isSameAs(cache3again); @@ -62,19 +60,23 @@ public void testDynamicMode() { assertThat(cache1.get("key3").get()).isNull(); cache1.evict("key3"); assertThat(cache1.get("key3")).isNull(); + assertThat(cache1.get("key3", () -> "value3")).isEqualTo("value3"); + assertThat(cache1.get("key3", () -> "value3")).isEqualTo("value3"); + cache1.evict("key3"); + assertThat(cache1.get("key3", () -> (String) null)).isNull(); + assertThat(cache1.get("key3", () -> (String) null)).isNull(); } @Test public void testStaticMode() { CaffeineCacheManager cm = new CaffeineCacheManager("c1", "c2"); + Cache cache1 = cm.getCache("c1"); - boolean condition3 = cache1 instanceof CaffeineCache; - assertThat(condition3).isTrue(); + assertThat(cache1).isInstanceOf(CaffeineCache.class); Cache cache1again = cm.getCache("c1"); assertThat(cache1).isSameAs(cache1again); Cache cache2 = cm.getCache("c2"); - boolean condition2 = cache2 instanceof CaffeineCache; - assertThat(condition2).isTrue(); + assertThat(cache2).isInstanceOf(CaffeineCache.class); Cache cache2again = cm.getCache("c2"); assertThat(cache2).isSameAs(cache2again); Cache cache3 = cm.getCache("c3"); @@ -91,13 +93,11 @@ public void testStaticMode() { cm.setAllowNullValues(false); Cache cache1x = cm.getCache("c1"); - boolean condition1 = cache1x instanceof CaffeineCache; - assertThat(condition1).isTrue(); - assertThat(cache1x != cache1).isTrue(); + assertThat(cache1x).isInstanceOf(CaffeineCache.class); + assertThat(cache1x).isNotSameAs(cache1); Cache cache2x = cm.getCache("c2"); - boolean condition = cache2x instanceof CaffeineCache; - assertThat(condition).isTrue(); - assertThat(cache2x != cache2).isTrue(); + assertThat(cache2x).isInstanceOf(CaffeineCache.class); + assertThat(cache2x).isNotSameAs(cache2); Cache cache3x = cm.getCache("c3"); assertThat(cache3x).isNull(); @@ -190,7 +190,7 @@ public void cacheLoaderUseLoadingCache() { assertThat(value.get()).isEqualTo("pong"); assertThatIllegalArgumentException().isThrownBy(() -> assertThat(cache1.get("foo")).isNull()) - .withMessageContaining("I only know ping"); + .withMessageContaining("I only know ping"); } @Test diff --git a/spring-context/src/main/java/org/springframework/cache/Cache.java b/spring-context/src/main/java/org/springframework/cache/Cache.java index 8a3b904f4904..648ff88e3957 100644 --- a/spring-context/src/main/java/org/springframework/cache/Cache.java +++ b/spring-context/src/main/java/org/springframework/cache/Cache.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,14 +23,20 @@ /** * Interface that defines common cache operations. * - * Note: Due to the generic use of caching, it is recommended that - * implementations allow storage of {@code null} values (for example to - * cache methods that return {@code null}). + *

    Serves as an SPI for Spring's annotation-based caching model + * ({@link org.springframework.cache.annotation.Cacheable} and co) + * as well as an API for direct usage in applications. + * + *

    Note: Due to the generic use of caching, it is recommended + * that implementations allow storage of {@code null} values + * (for example to cache methods that return {@code null}). * * @author Costin Leau * @author Juergen Hoeller * @author Stephane Nicoll * @since 3.1 + * @see CacheManager + * @see org.springframework.cache.annotation.Cacheable */ public interface Cache { diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheInvoker.java b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheInvoker.java index 4710f8e1c30d..d5c71acd8a06 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheInvoker.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheInvoker.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -82,12 +82,12 @@ protected Cache.ValueWrapper doGet(Cache cache, Object key) { * Execute {@link Cache#put(Object, Object)} on the specified {@link Cache} * and invoke the error handler if an exception occurs. */ - protected void doPut(Cache cache, Object key, @Nullable Object result) { + protected void doPut(Cache cache, Object key, @Nullable Object value) { try { - cache.put(key, result); + cache.put(key, value); } catch (RuntimeException ex) { - getErrorHandler().handleCachePutError(ex, cache, key, result); + getErrorHandler().handleCachePutError(ex, cache, key, value); } } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java index 260d48299e18..077850a2add9 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java @@ -397,11 +397,11 @@ private Object execute(final CacheOperationInvoker invoker, Method method, Cache processCacheEvicts(contexts.get(CacheEvictOperation.class), true, CacheOperationExpressionEvaluator.NO_RESULT); - // Check if we have a cached item matching the conditions + // Check if we have a cached value matching the conditions Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class)); - // Collect puts from any @Cacheable miss, if no cached item is found - List cachePutRequests = new ArrayList<>(); + // Collect puts from any @Cacheable miss, if no cached value is found + List cachePutRequests = new ArrayList<>(1); if (cacheHit == null) { collectPutRequests(contexts.get(CacheableOperation.class), CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests); @@ -468,7 +468,7 @@ private Object unwrapReturnValue(@Nullable Object returnValue) { private boolean hasCachePut(CacheOperationContexts contexts) { // Evaluate the conditions *without* the result object because we don't have it yet... Collection cachePutContexts = contexts.get(CachePutOperation.class); - Collection excluded = new ArrayList<>(); + Collection excluded = new ArrayList<>(1); for (CacheOperationContext context : cachePutContexts) { try { if (!context.isConditionPassing(CacheOperationExpressionEvaluator.RESULT_UNAVAILABLE)) { @@ -521,9 +521,9 @@ private void logInvalidating(CacheOperationContext context, CacheEvictOperation } /** - * Find a cached item only for {@link CacheableOperation} that passes the condition. + * Find a cached value only for {@link CacheableOperation} that passes the condition. * @param contexts the cacheable operations - * @return a {@link Cache.ValueWrapper} holding the cached item, + * @return a {@link Cache.ValueWrapper} holding the cached value, * or {@code null} if none is found */ @Nullable @@ -548,9 +548,9 @@ private Cache.ValueWrapper findCachedItem(Collection cont /** * Collect the {@link CachePutRequest} for all {@link CacheOperation} using - * the specified result item. + * the specified result value. * @param contexts the contexts to handle - * @param result the result item (never {@code null}) + * @param result the result value (never {@code null}) * @param putRequests the collection to update */ private void collectPutRequests(Collection contexts, @@ -722,7 +722,7 @@ public CacheOperationContext(CacheOperationMetadata metadata, Object[] args, Obj this.args = extractArgs(metadata.method, args); this.target = target; this.caches = CacheAspectSupport.this.getCaches(this, metadata.cacheResolver); - this.cacheNames = createCacheNames(this.caches); + this.cacheNames = prepareCacheNames(this.caches); } @Override @@ -810,8 +810,8 @@ protected Collection getCacheNames() { return this.cacheNames; } - private Collection createCacheNames(Collection caches) { - Collection names = new ArrayList<>(); + private Collection prepareCacheNames(Collection caches) { + Collection names = new ArrayList<>(caches.size()); for (Cache cache : caches) { names.add(cache.getName()); } diff --git a/spring-context/src/main/java/org/springframework/cache/support/SimpleCacheManager.java b/spring-context/src/main/java/org/springframework/cache/support/SimpleCacheManager.java index c130f8f2698c..08500e04606a 100644 --- a/spring-context/src/main/java/org/springframework/cache/support/SimpleCacheManager.java +++ b/spring-context/src/main/java/org/springframework/cache/support/SimpleCacheManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,14 +24,16 @@ /** * Simple cache manager working against a given collection of caches. * Useful for testing or simple caching declarations. - *

    - * When using this implementation directly, i.e. not via a regular + * + *

    When using this implementation directly, i.e. not via a regular * bean registration, {@link #initializeCaches()} should be invoked * to initialize its internal state once the * {@linkplain #setCaches(Collection) caches have been provided}. * * @author Costin Leau * @since 3.1 + * @see NoOpCache + * @see org.springframework.cache.concurrent.ConcurrentMapCache */ public class SimpleCacheManager extends AbstractCacheManager { diff --git a/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java b/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java index 7b4911a8b49a..87862aa84401 100644 --- a/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java +++ b/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java @@ -118,6 +118,7 @@ void spr13081ConfigNoCacheNameIsRequired() { assertThat(cacheResolver.getCache("foo").get("foo")).isNull(); Object result = bean.getSimple("foo"); // cache name = id assertThat(cacheResolver.getCache("foo").get("foo").get()).isEqualTo(result); + context.close(); } @@ -127,7 +128,7 @@ void spr13081ConfigFailIfCacheResolverReturnsNullCacheName() { Spr13081Service bean = context.getBean(Spr13081Service.class); assertThatIllegalStateException().isThrownBy(() -> bean.getSimple(null)) - .withMessageContaining(MyCacheResolver.class.getName()); + .withMessageContaining(MyCacheResolver.class.getName()); context.close(); } @@ -146,6 +147,7 @@ void spr14230AdaptsToOptional() { TestBean tb2 = bean.findById("tb1").get(); assertThat(tb2).isNotSameAs(tb); assertThat(cache.get("tb1").get()).isSameAs(tb2); + context.close(); } @@ -177,6 +179,7 @@ void spr15271FindsOnInterfaceWithInterfaceProxy() { bean.insertItem(tb); assertThat(bean.findById("tb1").get()).isSameAs(tb); assertThat(cache.get("tb1").get()).isSameAs(tb); + context.close(); } @@ -190,6 +193,7 @@ void spr15271FindsOnInterfaceWithCglibProxy() { bean.insertItem(tb); assertThat(bean.findById("tb1").get()).isSameAs(tb); assertThat(cache.get("tb1").get()).isSameAs(tb); + context.close(); } From c9849d69721e2fcfe80372e700d18e97d1a0ffe5 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sat, 22 Jul 2023 00:43:09 +0200 Subject: [PATCH 12/47] Polishing (cherry picked from commit 3a9e0ea8a7c9334b397b0b79f126c8fe07ff6ab8) --- .../cache/caffeine/CaffeineCache.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java index ef8c3b03e5bd..5192ba1677a5 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java +++ b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -59,8 +59,8 @@ public CaffeineCache(String name, com.github.benmanes.caffeine.cache.Cache cache, boolean allowNullValues) { @@ -86,7 +86,7 @@ public final com.github.benmanes.caffeine.cache.Cache getNativeC @SuppressWarnings("unchecked") @Override @Nullable - public T get(Object key, final Callable valueLoader) { + public T get(Object key, Callable valueLoader) { return (T) fromStoreValue(this.cache.get(key, new LoadFunction(valueLoader))); } @@ -106,7 +106,7 @@ public void put(Object key, @Nullable Object value) { @Override @Nullable - public ValueWrapper putIfAbsent(Object key, @Nullable final Object value) { + public ValueWrapper putIfAbsent(Object key, @Nullable Object value) { PutIfAbsentFunction callable = new PutIfAbsentFunction(value); Object result = this.cache.get(key, callable); return (callable.called ? null : toValueWrapper(result)); @@ -140,7 +140,7 @@ private class PutIfAbsentFunction implements Function { @Nullable private final Object value; - private boolean called; + boolean called; public PutIfAbsentFunction(@Nullable Object value) { this.value = value; @@ -159,16 +159,17 @@ private class LoadFunction implements Function { private final Callable valueLoader; public LoadFunction(Callable valueLoader) { + Assert.notNull(valueLoader, "Callable must not be null"); this.valueLoader = valueLoader; } @Override - public Object apply(Object o) { + public Object apply(Object key) { try { return toStoreValue(this.valueLoader.call()); } catch (Exception ex) { - throw new ValueRetrievalException(o, this.valueLoader, ex); + throw new ValueRetrievalException(key, this.valueLoader, ex); } } } From d1efc891dbff36e0db8a4b3c9dd1c90dfa011bce Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sat, 22 Jul 2023 00:43:26 +0200 Subject: [PATCH 13/47] Upgrade to Netty 4.1.95 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 99f8ffb540b2..06af26a95ad4 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,7 @@ configure(allprojects) { project -> dependencyManagement { imports { mavenBom "com.fasterxml.jackson:jackson-bom:2.12.7" - mavenBom "io.netty:netty-bom:4.1.94.Final" + mavenBom "io.netty:netty-bom:4.1.95.Final" mavenBom "io.projectreactor:reactor-bom:2020.0.34" mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR13" mavenBom "io.rsocket:rsocket-bom:1.1.3" From 6dea580145ac7b9cec04e3b7761ccf0ef6296a48 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 24 Jul 2023 11:21:07 +0200 Subject: [PATCH 14/47] Clarify DataAccessException/ScriptException declarations for R2DBC Closes gh-30932 (cherry picked from commit 5bcf5c6f7cac0ff5f1a7ba78ad9d7b7d2df7fb52) --- .../r2dbc/connection/init/DatabasePopulator.java | 10 +++++----- .../init/ResourceDatabasePopulator.java | 4 ++-- .../r2dbc/connection/init/ScriptUtils.java | 14 +++++++------- .../r2dbc/core/ConnectionAccessor.java | 15 +++++++++------ .../r2dbc/core/DatabaseClient.java | 6 +++--- .../r2dbc/core/DefaultDatabaseClient.java | 5 ++--- 6 files changed, 28 insertions(+), 26 deletions(-) diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/DatabasePopulator.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/DatabasePopulator.java index 467a25549cfd..8adf9831b30c 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/DatabasePopulator.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/DatabasePopulator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import io.r2dbc.spi.ConnectionFactory; import reactor.core.publisher.Mono; -import org.springframework.dao.DataAccessException; import org.springframework.r2dbc.connection.ConnectionFactoryUtils; import org.springframework.util.Assert; @@ -44,17 +43,18 @@ public interface DatabasePopulator { * already configured and ready to use, must not be {@code null} * @return {@link Mono} that initiates script execution and is * notified upon completion - * @throws ScriptException in all other error cases + * @throws ScriptException in case of any errors */ - Mono populate(Connection connection) throws ScriptException; + Mono populate(Connection connection); /** * Execute the given {@link DatabasePopulator} against the given {@link ConnectionFactory}. * @param connectionFactory the {@link ConnectionFactory} to execute against * @return {@link Mono} that initiates {@link DatabasePopulator#populate(Connection)} * and is notified upon completion + * @throws ScriptException in case of any errors */ - default Mono populate(ConnectionFactory connectionFactory) throws DataAccessException { + default Mono populate(ConnectionFactory connectionFactory) { Assert.notNull(connectionFactory, "ConnectionFactory must not be null"); return Mono.usingWhen(ConnectionFactoryUtils.getConnection(connectionFactory), // this::populate, // diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ResourceDatabasePopulator.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ResourceDatabasePopulator.java index a6aeac31364c..86dc2f4fcf23 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ResourceDatabasePopulator.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ResourceDatabasePopulator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -260,7 +260,7 @@ public void setDataBufferFactory(DataBufferFactory dataBufferFactory) { @Override - public Mono populate(Connection connection) throws ScriptException { + public Mono populate(Connection connection) { Assert.notNull(connection, "Connection must not be null"); return Flux.fromIterable(this.scripts).concatMap(resource -> { EncodedResource encodedScript = new EncodedResource(resource, this.sqlScriptEncoding); diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptUtils.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptUtils.java index c374d68c05a4..18437ddca93a 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptUtils.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -125,7 +125,7 @@ public abstract class ScriptUtils { * @see org.springframework.r2dbc.connection.ConnectionFactoryUtils#getConnection * @see org.springframework.r2dbc.connection.ConnectionFactoryUtils#releaseConnection */ - public static Mono executeSqlScript(Connection connection, Resource resource) throws ScriptException { + public static Mono executeSqlScript(Connection connection, Resource resource) { return executeSqlScript(connection, new EncodedResource(resource)); } @@ -149,7 +149,7 @@ public static Mono executeSqlScript(Connection connection, Resource resour * @see org.springframework.r2dbc.connection.ConnectionFactoryUtils#getConnection * @see org.springframework.r2dbc.connection.ConnectionFactoryUtils#releaseConnection */ - public static Mono executeSqlScript(Connection connection, EncodedResource resource) throws ScriptException { + public static Mono executeSqlScript(Connection connection, EncodedResource resource) { return executeSqlScript(connection, resource, DefaultDataBufferFactory.sharedInstance, false, false, DEFAULT_COMMENT_PREFIXES, DEFAULT_STATEMENT_SEPARATOR, DEFAULT_BLOCK_COMMENT_START_DELIMITER, DEFAULT_BLOCK_COMMENT_END_DELIMITER); @@ -189,7 +189,7 @@ public static Mono executeSqlScript(Connection connection, EncodedResource public static Mono executeSqlScript(Connection connection, EncodedResource resource, DataBufferFactory dataBufferFactory, boolean continueOnError, boolean ignoreFailedDrops, String commentPrefix, @Nullable String separator, String blockCommentStartDelimiter, - String blockCommentEndDelimiter) throws ScriptException { + String blockCommentEndDelimiter) { return executeSqlScript(connection, resource, dataBufferFactory, continueOnError, ignoreFailedDrops, new String[] { commentPrefix }, separator, @@ -230,7 +230,7 @@ public static Mono executeSqlScript(Connection connection, EncodedResource public static Mono executeSqlScript(Connection connection, EncodedResource resource, DataBufferFactory dataBufferFactory, boolean continueOnError, boolean ignoreFailedDrops, String[] commentPrefixes, @Nullable String separator, String blockCommentStartDelimiter, - String blockCommentEndDelimiter) throws ScriptException { + String blockCommentEndDelimiter) { if (logger.isDebugEnabled()) { logger.debug("Executing SQL script from " + resource); @@ -365,7 +365,7 @@ private static void appendSeparatorToScriptIfNecessary(StringBuilder scriptBuild */ static boolean containsStatementSeparator(EncodedResource resource, String script, String separator, String[] commentPrefixes, String blockCommentStartDelimiter, - String blockCommentEndDelimiter) throws ScriptException { + String blockCommentEndDelimiter) { boolean inSingleQuote = false; boolean inDoubleQuote = false; @@ -448,7 +448,7 @@ else if (script.startsWith(blockCommentStartDelimiter, i)) { */ static List splitSqlScript(EncodedResource resource, String script, String separator, String[] commentPrefixes, String blockCommentStartDelimiter, - String blockCommentEndDelimiter) throws ScriptException { + String blockCommentEndDelimiter) { Assert.hasText(script, "'script' must not be null or empty"); Assert.notNull(separator, "'separator' must not be null"); diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/ConnectionAccessor.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/ConnectionAccessor.java index 9f794ce67101..d3424708960d 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/ConnectionAccessor.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/ConnectionAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +22,6 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import org.springframework.dao.DataAccessException; - /** * Interface declaring methods that accept callback {@link Function} * to operate within the scope of a {@link Connection}. @@ -31,13 +29,16 @@ * close the connection as the connections may be pooled or be * subject to other kinds of resource management. * - *

    Callback functions are responsible for creating a + *

    Callback functions are responsible for creating a * {@link org.reactivestreams.Publisher} that defines the scope of how * long the allocated {@link Connection} is valid. Connections are * released after the publisher terminates. * + *

    This serves as a base interface for {@link DatabaseClient}. + * * @author Mark Paluch * @since 5.3 + * @see DatabaseClient */ public interface ConnectionAccessor { @@ -49,8 +50,9 @@ public interface ConnectionAccessor { * {@link Function} closure, otherwise resources may get defunct. * @param action the callback object that specifies the connection action * @return the resulting {@link Mono} + * @throws org.springframework.dao.DataAccessException in case of any errors */ - Mono inConnection(Function> action) throws DataAccessException; + Mono inConnection(Function> action); /** * Execute a callback {@link Function} within a {@link Connection} scope. @@ -60,7 +62,8 @@ public interface ConnectionAccessor { * {@link Function} closure, otherwise resources may get defunct. * @param action the callback object that specifies the connection action * @return the resulting {@link Flux} + * @throws org.springframework.dao.DataAccessException in case of any errors */ - Flux inConnectionMany(Function> action) throws DataAccessException; + Flux inConnectionMany(Function> action); } diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DatabaseClient.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DatabaseClient.java index 71569e190180..f9a6daef4d18 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DatabaseClient.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DatabaseClient.java @@ -32,9 +32,9 @@ import org.springframework.util.Assert; /** - * A non-blocking, reactive client for performing database calls requests with - * Reactive Streams back pressure. Provides a higher level, common API over - * R2DBC client libraries. + * A non-blocking, reactive client for performing database calls with Reactive Streams + * back pressure. Provides a higher level, common API over R2DBC client libraries. + * Propagates {@link org.springframework.dao.DataAccessException} variants for errors. * *

    Use one of the static factory methods {@link #create(ConnectionFactory)} * or obtain a {@link DatabaseClient#builder()} to create an instance. diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClient.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClient.java index 5a552ea7fa66..6d3c33673093 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClient.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClient.java @@ -44,7 +44,6 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import org.springframework.dao.DataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.lang.Nullable; import org.springframework.r2dbc.connection.ConnectionFactoryUtils; @@ -105,7 +104,7 @@ public GenericExecuteSpec sql(Supplier sqlSupplier) { } @Override - public Mono inConnection(Function> action) throws DataAccessException { + public Mono inConnection(Function> action) { Assert.notNull(action, "Callback object must not be null"); Mono connectionMono = getConnection().map( connection -> new ConnectionCloseHolder(connection, this::closeConnection)); @@ -127,7 +126,7 @@ public Mono inConnection(Function> action) throws Dat } @Override - public Flux inConnectionMany(Function> action) throws DataAccessException { + public Flux inConnectionMany(Function> action) { Assert.notNull(action, "Callback object must not be null"); Mono connectionMono = getConnection().map( connection -> new ConnectionCloseHolder(connection, this::closeConnection)); From 840bd574db9031d0f7583d6c8edbb3a5bcc3d997 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 24 Jul 2023 11:21:13 +0200 Subject: [PATCH 15/47] Polishing (cherry picked from commit fdf1418dfbbdd7c06945d6223594c509bd194294) --- .../DefaultListableBeanFactoryTests.java | 82 ++++++++++--------- .../dao/support/DataAccessUtils.java | 5 +- 2 files changed, 48 insertions(+), 39 deletions(-) diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java index 45a4bb48c7bf..0c227674fd75 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java @@ -1271,10 +1271,11 @@ void autowireWithTwoMatchesForConstructorDependency() { lbf.registerBeanDefinition("rod", bd); RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); lbf.registerBeanDefinition("rod2", bd2); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> - lbf.autowire(ConstructorDependency.class, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false)) - .withMessageContaining("rod") - .withMessageContaining("rod2"); + + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> lbf.autowire(ConstructorDependency.class, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false)) + .withMessageContaining("rod") + .withMessageContaining("rod2"); } @Test @@ -1336,11 +1337,12 @@ void dependsOnCycle() { RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); bd2.setDependsOn("tb1"); lbf.registerBeanDefinition("tb2", bd2); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> - lbf.preInstantiateSingletons()) - .withMessageContaining("Circular") - .withMessageContaining("'tb2'") - .withMessageContaining("'tb1'"); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> lbf.preInstantiateSingletons()) + .withMessageContaining("Circular") + .withMessageContaining("'tb2'") + .withMessageContaining("'tb1'"); } @Test @@ -1354,11 +1356,12 @@ void implicitDependsOnCycle() { RootBeanDefinition bd3 = new RootBeanDefinition(TestBean.class); bd3.setDependsOn("tb1"); lbf.registerBeanDefinition("tb3", bd3); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - lbf::preInstantiateSingletons) - .withMessageContaining("Circular") - .withMessageContaining("'tb3'") - .withMessageContaining("'tb1'"); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(lbf::preInstantiateSingletons) + .withMessageContaining("Circular") + .withMessageContaining("'tb3'") + .withMessageContaining("'tb1'"); } @Test @@ -1493,10 +1496,11 @@ void getBeanByTypeWithMultiplePriority() { RootBeanDefinition bd2 = new RootBeanDefinition(HighPriorityTestBean.class); lbf.registerBeanDefinition("bd1", bd1); lbf.registerBeanDefinition("bd2", bd2); - assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(() -> - lbf.getBean(TestBean.class)) - .withMessageContaining("Multiple beans found with the same priority") - .withMessageContaining("5"); // conflicting priority + + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class) + .isThrownBy(() -> lbf.getBean(TestBean.class)) + .withMessageContaining("Multiple beans found with the same priority") + .withMessageContaining("5"); // conflicting priority } @Test @@ -1698,9 +1702,9 @@ void getBeanByTypeInstanceWithMultiplePrimary() { lbf.registerBeanDefinition("bd1", bd1); lbf.registerBeanDefinition("bd2", bd2); - assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(() -> - lbf.getBean(ConstructorDependency.class, 42)) - .withMessageContaining("more than one 'primary'"); + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class) + .isThrownBy(() -> lbf.getBean(ConstructorDependency.class, 42)) + .withMessageContaining("more than one 'primary'"); } @Test @@ -1881,10 +1885,11 @@ void autowireBeanByTypeWithTwoMatches() { RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); lbf.registerBeanDefinition("test", bd); lbf.registerBeanDefinition("spouse", bd2); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> - lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true)) - .withMessageContaining("test") - .withMessageContaining("spouse"); + + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true)) + .withMessageContaining("test") + .withMessageContaining("spouse"); } @Test @@ -1946,10 +1951,11 @@ void autowireBeanByTypeWithIdenticalPriorityCandidates() { RootBeanDefinition bd2 = new RootBeanDefinition(HighPriorityTestBean.class); lbf.registerBeanDefinition("test", bd); lbf.registerBeanDefinition("spouse", bd2); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> - lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true)) - .withCauseExactlyInstanceOf(NoUniqueBeanDefinitionException.class) - .withMessageContaining("5"); + + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true)) + .withCauseExactlyInstanceOf(NoUniqueBeanDefinitionException.class) + .withMessageContaining("5"); } @Test @@ -2185,19 +2191,21 @@ void constructorDependencyWithUnresolvableClass() { @Test void beanDefinitionWithInterface() { lbf.registerBeanDefinition("test", new RootBeanDefinition(ITestBean.class)); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> - lbf.getBean("test")) - .withMessageContaining("interface") - .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("test")); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> lbf.getBean("test")) + .withMessageContaining("interface") + .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("test")); } @Test void beanDefinitionWithAbstractClass() { lbf.registerBeanDefinition("test", new RootBeanDefinition(AbstractBeanFactory.class)); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> - lbf.getBean("test")) - .withMessageContaining("abstract") - .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("test")); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> lbf.getBean("test")) + .withMessageContaining("abstract") + .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("test")); } @Test diff --git a/spring-tx/src/main/java/org/springframework/dao/support/DataAccessUtils.java b/spring-tx/src/main/java/org/springframework/dao/support/DataAccessUtils.java index adb80a3afb7a..a72df8db9dc1 100644 --- a/spring-tx/src/main/java/org/springframework/dao/support/DataAccessUtils.java +++ b/spring-tx/src/main/java/org/springframework/dao/support/DataAccessUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,8 @@ /** * Miscellaneous utility methods for DAO implementations. - * Useful with any data access technology. + * + *

    Useful with any data access technology. * * @author Juergen Hoeller * @since 1.0.2 From 23eb0e31289f47f8f369a7c6028fa0021989e692 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 25 Jul 2023 19:12:07 +0200 Subject: [PATCH 16/47] Polishing (cherry picked from commit bbde68c49e66c3c531920cb80a55742262507be7) --- .../jdbc/core/JdbcOperations.java | 7 ++-- .../jdbc/core/JdbcTemplate.java | 10 ++--- .../BeanPropertySqlParameterSource.java | 7 ++-- .../NamedParameterJdbcTemplate.java | 11 +++-- .../namedparam/NamedParameterQueryTests.java | 42 +++++++------------ 5 files changed, 35 insertions(+), 42 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java index 75937f348bbf..7af681f52b4d 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,12 +35,13 @@ *

    Alternatively, the standard JDBC infrastructure can be mocked. * However, mocking this interface constitutes significantly less work. * As an alternative to a mock objects approach to testing data access code, - * consider the powerful integration testing support provided via the Spring - * TestContext Framework, in the {@code spring-test} artifact. + * consider the powerful integration testing support provided via the + * Spring TestContext Framework, in the {@code spring-test} artifact. * * @author Rod Johnson * @author Juergen Hoeller * @see JdbcTemplate + * @see org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations */ public interface JdbcOperations { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index eb0fb2475bca..335a96022a6a 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -65,8 +65,7 @@ * It executes core JDBC workflow, leaving application code to provide SQL * and extract results. This class executes SQL queries or updates, initiating * iteration over ResultSets and catching JDBC exceptions and translating - * them to the generic, more informative exception hierarchy defined in the - * {@code org.springframework.dao} package. + * them to the common {@code org.springframework.dao} exception hierarchy. * *

    Code using this class need only implement callback interfaces, giving * them a clearly defined contract. The {@link PreparedStatementCreator} callback @@ -75,7 +74,8 @@ * values from a ResultSet. See also {@link PreparedStatementSetter} and * {@link RowMapper} for two popular alternative callback interfaces. * - *

    Can be used within a service implementation via direct instantiation + *

    An instance of this template class is thread-safe once configured. + * Can be used within a service implementation via direct instantiation * with a DataSource reference, or get prepared in an application context * and given to services as bean reference. Note: The DataSource should * always be configured as a bean in the application context, in the first case @@ -88,12 +88,11 @@ *

    All SQL operations performed by this class are logged at debug level, * using "org.springframework.jdbc.core.JdbcTemplate" as log category. * - *

    NOTE: An instance of this class is thread-safe once configured. - * * @author Rod Johnson * @author Juergen Hoeller * @author Thomas Risberg * @since May 3, 2001 + * @see JdbcOperations * @see PreparedStatementCreator * @see PreparedStatementSetter * @see CallableStatementCreator @@ -103,6 +102,7 @@ * @see RowCallbackHandler * @see RowMapper * @see org.springframework.jdbc.support.SQLExceptionTranslator + * @see org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate */ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/BeanPropertySqlParameterSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/BeanPropertySqlParameterSource.java index 83b8edb24b90..be0233d47ce8 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/BeanPropertySqlParameterSource.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/BeanPropertySqlParameterSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,9 +32,10 @@ /** * {@link SqlParameterSource} implementation that obtains parameter values * from bean properties of a given JavaBean object. The names of the bean - * properties have to match the parameter names. + * properties have to match the parameter names. Supports components of + * record classes as well, with accessor methods matching parameter names. * - *

    Uses a Spring BeanWrapper for bean property access underneath. + *

    Uses a Spring {@link BeanWrapper} for bean property access underneath. * * @author Thomas Risberg * @author Juergen Hoeller diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java index 3d3752b5bf59..3c4043544711 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,16 +55,19 @@ * done at execution time. It also allows for expanding a {@link java.util.List} * of values to the appropriate number of placeholders. * - *

    The underlying {@link org.springframework.jdbc.core.JdbcTemplate} is + *

    An instance of this template class is thread-safe once configured. + * The underlying {@link org.springframework.jdbc.core.JdbcTemplate} is * exposed to allow for convenient access to the traditional * {@link org.springframework.jdbc.core.JdbcTemplate} methods. * - *

    NOTE: An instance of this class is thread-safe once configured. - * * @author Thomas Risberg * @author Juergen Hoeller * @since 2.0 * @see NamedParameterJdbcOperations + * @see SqlParameterSource + * @see ResultSetExtractor + * @see RowCallbackHandler + * @see RowMapper * @see org.springframework.jdbc.core.JdbcTemplate */ public class NamedParameterJdbcTemplate implements NamedParameterJdbcOperations { diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterQueryTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterQueryTests.java index 1a2c61469908..8c47dcda143e 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterQueryTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterQueryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,8 +34,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.jdbc.core.RowMapper; - import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; @@ -135,8 +133,7 @@ public void testQueryForListWithParamMapAndSingleRowAndColumn() throws Exception } @Test - public void testQueryForListWithParamMapAndIntegerElementAndSingleRowAndColumn() - throws Exception { + public void testQueryForListWithParamMapAndIntegerElementAndSingleRowAndColumn() throws Exception { given(resultSet.getMetaData()).willReturn(resultSetMetaData); given(resultSet.next()).willReturn(true, false); given(resultSet.getInt(1)).willReturn(11); @@ -174,11 +171,10 @@ public void testQueryForObjectWithParamMapAndRowMapper() throws Exception { MapSqlParameterSource params = new MapSqlParameterSource(); params.addValue("id", 3); - Object o = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID = :id", - params, (RowMapper) (rs, rowNum) -> rs.getInt(1)); + Integer value = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID = :id", + params, (rs, rowNum) -> rs.getInt(1)); - boolean condition = o instanceof Integer; - assertThat(condition).as("Correct result type").isTrue(); + assertThat(value).isEqualTo(22); verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?"); verify(preparedStatement).setObject(1, 3); } @@ -191,11 +187,10 @@ public void testQueryForObjectWithMapAndInteger() throws Exception { Map params = new HashMap<>(); params.put("id", 3); - Object o = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID = :id", + Integer value = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID = :id", params, Integer.class); - boolean condition = o instanceof Integer; - assertThat(condition).as("Correct result type").isTrue(); + assertThat(value).isEqualTo(22); verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?"); verify(preparedStatement).setObject(1, 3); } @@ -208,30 +203,26 @@ public void testQueryForObjectWithParamMapAndInteger() throws Exception { MapSqlParameterSource params = new MapSqlParameterSource(); params.addValue("id", 3); - Object o = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID = :id", + Integer value = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID = :id", params, Integer.class); - boolean condition = o instanceof Integer; - assertThat(condition).as("Correct result type").isTrue(); + assertThat(value).isEqualTo(22); verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?"); verify(preparedStatement).setObject(1, 3); } @Test public void testQueryForObjectWithParamMapAndList() throws Exception { - String sql = "SELECT AGE FROM CUSTMR WHERE ID IN (:ids)"; - String sqlToUse = "SELECT AGE FROM CUSTMR WHERE ID IN (?, ?)"; given(resultSet.getMetaData()).willReturn(resultSetMetaData); given(resultSet.next()).willReturn(true, false); given(resultSet.getInt(1)).willReturn(22); MapSqlParameterSource params = new MapSqlParameterSource(); params.addValue("ids", Arrays.asList(3, 4)); - Object o = template.queryForObject(sql, params, Integer.class); + Integer value = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID IN (:ids)", params, Integer.class); - boolean condition = o instanceof Integer; - assertThat(condition).as("Correct result type").isTrue(); - verify(connection).prepareStatement(sqlToUse); + assertThat(value).isEqualTo(22); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID IN (?, ?)"); verify(preparedStatement).setObject(1, 3); } @@ -246,14 +237,11 @@ public void testQueryForObjectWithParamMapAndListOfExpressionLists() throws Exce l1.add(new Object[] {3, "Rod"}); l1.add(new Object[] {4, "Juergen"}); params.addValue("multiExpressionList", l1); - Object o = template.queryForObject( - "SELECT AGE FROM CUSTMR WHERE (ID, NAME) IN (:multiExpressionList)", + Integer value = template.queryForObject("SELECT AGE FROM CUSTMR WHERE (ID, NAME) IN (:multiExpressionList)", params, Integer.class); - boolean condition = o instanceof Integer; - assertThat(condition).as("Correct result type").isTrue(); - verify(connection).prepareStatement( - "SELECT AGE FROM CUSTMR WHERE (ID, NAME) IN ((?, ?), (?, ?))"); + assertThat(value).isEqualTo(22); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE (ID, NAME) IN ((?, ?), (?, ?))"); verify(preparedStatement).setObject(1, 3); } From 14c8c9168cdb0a64af0b59471f1d8bb0684c42bf Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 26 Jul 2023 14:02:05 +0200 Subject: [PATCH 17/47] Polishing --- .../jdbc/core/simple/AbstractJdbcCall.java | 6 ++-- .../jdbc/core/simple/AbstractJdbcInsert.java | 31 ++++++++++--------- .../jdbc/core/simple/SimpleJdbcCall.java | 4 +-- .../jdbc/core/simple/SimpleJdbcInsert.java | 12 +++---- .../r2dbc/core/NamedParameterUtils.java | 18 +++++------ .../springframework/r2dbc/core/Parameter.java | 18 +++++------ src/docs/asciidoc/data-access.adoc | 25 +++++---------- src/docs/asciidoc/integration.adoc | 4 +-- 8 files changed, 54 insertions(+), 64 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcCall.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcCall.java index 82a2ad19c13d..0121db51c0b2 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcCall.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcCall.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ * Abstract class to provide base functionality for easy stored procedure calls * based on configuration options and database meta-data. * - *

    This class provides the base SPI for {@link SimpleJdbcCall}. + *

    This class provides the processing arrangement for {@link SimpleJdbcCall}. * * @author Thomas Risberg * @author Juergen Hoeller @@ -453,7 +453,7 @@ protected Map matchInParameterValuesWithCallParameters(SqlParame /** * Match the provided in parameter values with registered parameters and * parameters defined via meta-data processing. - * @param args the parameter values provided in a Map + * @param args the parameter values provided as a Map * @return a Map with parameter names and values */ protected Map matchInParameterValuesWithCallParameters(Map args) { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcInsert.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcInsert.java index 5d6d83c142d4..504ee7013b7e 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcInsert.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcInsert.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,10 +50,10 @@ import org.springframework.util.Assert; /** - * Abstract class to provide base functionality for easy inserts + * Abstract class to provide base functionality for easy (batch) inserts * based on configuration options and database meta-data. * - *

    This class provides the base SPI for {@link SimpleJdbcInsert}. + *

    This class provides the processing arrangement for {@link SimpleJdbcInsert}. * * @author Thomas Risberg * @author Juergen Hoeller @@ -409,7 +409,7 @@ protected KeyHolder doExecuteAndReturnKeyHolder(SqlParameterSource parameterSour /** * Delegate method to execute the insert, generating a single key. */ - private Number executeInsertAndReturnKeyInternal(final List values) { + private Number executeInsertAndReturnKeyInternal(List values) { KeyHolder kh = executeInsertAndReturnKeyHolderInternal(values); if (kh.getKey() != null) { return kh.getKey(); @@ -423,11 +423,11 @@ private Number executeInsertAndReturnKeyInternal(final List values) { /** * Delegate method to execute the insert, generating any number of keys. */ - private KeyHolder executeInsertAndReturnKeyHolderInternal(final List values) { + private KeyHolder executeInsertAndReturnKeyHolderInternal(List values) { if (logger.isDebugEnabled()) { logger.debug("The following parameters are used for call " + getInsertString() + " with: " + values); } - final KeyHolder keyHolder = new GeneratedKeyHolder(); + KeyHolder keyHolder = new GeneratedKeyHolder(); if (this.tableMetaDataContext.isGetGeneratedKeysSupported()) { getJdbcTemplate().update( @@ -455,7 +455,7 @@ private KeyHolder executeInsertAndReturnKeyHolderInternal(final List values) } Assert.state(getTableName() != null, "No table name set"); - final String keyQuery = this.tableMetaDataContext.getSimpleQueryForGetGeneratedKey( + String keyQuery = this.tableMetaDataContext.getSimpleQueryForGetGeneratedKey( getTableName(), getGeneratedKeyNames()[0]); Assert.state(keyQuery != null, "Query for simulating get generated keys must not be null"); @@ -535,8 +535,8 @@ private PreparedStatement prepareStatementForGeneratedKeys(Connection con) throw /** * Delegate method that executes a batch insert using the passed-in Maps of parameters. - * @param batch array of Maps with parameter names and values to be used in batch insert - * @return array of number of rows affected + * @param batch maps with parameter names and values to be used in the batch insert + * @return an array of number of rows affected */ @SuppressWarnings("unchecked") protected int[] doExecuteBatch(Map... batch) { @@ -549,9 +549,10 @@ protected int[] doExecuteBatch(Map... batch) { } /** - * Delegate method that executes a batch insert using the passed-in {@link SqlParameterSource SqlParameterSources}. - * @param batch array of SqlParameterSource with parameter names and values to be used in insert - * @return array of number of rows affected + * Delegate method that executes a batch insert using the passed-in + * {@link SqlParameterSource SqlParameterSources}. + * @param batch parameter sources with names and values to be used in the batch insert + * @return an array of number of rows affected */ protected int[] doExecuteBatch(SqlParameterSource... batch) { checkCompiled(); @@ -606,7 +607,7 @@ private void setParameterValues(PreparedStatement preparedStatement, List val * Match the provided in parameter values with registered parameters and parameters * defined via meta-data processing. * @param parameterSource the parameter values provided as a {@link SqlParameterSource} - * @return a Map with parameter names and values + * @return a List of values */ protected List matchInParameterValuesWithInsertColumns(SqlParameterSource parameterSource) { return this.tableMetaDataContext.matchInParameterValuesWithInsertColumns(parameterSource); @@ -615,8 +616,8 @@ protected List matchInParameterValuesWithInsertColumns(SqlParameterSourc /** * Match the provided in parameter values with registered parameters and parameters * defined via meta-data processing. - * @param args the parameter values provided in a Map - * @return a Map with parameter names and values + * @param args the parameter values provided as a Map + * @return a List of values */ protected List matchInParameterValuesWithInsertColumns(Map args) { return this.tableMetaDataContext.matchInParameterValuesWithInsertColumns(args); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCall.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCall.java index f26ea8aee3df..c5d946fad431 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCall.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCall.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,7 +47,7 @@ * any meta-data processing if you want to use parameter names that do not * match what is declared during the stored procedure compilation. * - *

    The actual insert is being handled using Spring's {@link JdbcTemplate}. + *

    The actual call is being handled using Spring's {@link JdbcTemplate}. * *

    Many of the configuration methods return the current instance of the * SimpleJdbcCall in order to provide the ability to chain multiple ones diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsert.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsert.java index ce210b39b43b..f48fb62d14d7 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsert.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsert.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,17 +26,17 @@ import org.springframework.jdbc.support.KeyHolder; /** - * A SimpleJdbcInsert is a multithreaded, reusable object providing easy insert + * A SimpleJdbcInsert is a multithreaded, reusable object providing easy (batch) insert * capabilities for a table. It provides meta-data processing to simplify the code - * needed to construct a basic insert statement. All you need to provide is the - * name of the table and a Map containing the column names and the column values. + * needed to construct a basic insert statement. All you need to provide is the name + * of the table and a Map containing the column names and the column values. * *

    The meta-data processing is based on the DatabaseMetaData provided by the * JDBC driver. As long as the JDBC driver can provide the names of the columns * for a specified table then we can rely on this auto-detection feature. If that * is not the case, then the column names must be specified explicitly. * - *

    The actual insert is handled using Spring's {@link JdbcTemplate}. + *

    The actual (batch) insert is handled using Spring's {@link JdbcTemplate}. * *

    Many of the configuration methods return the current instance of the * SimpleJdbcInsert to provide the ability to chain multiple ones together @@ -142,8 +142,8 @@ public KeyHolder executeAndReturnKeyHolder(SqlParameterSource parameterSource) { return doExecuteAndReturnKeyHolder(parameterSource); } - @Override @SuppressWarnings("unchecked") + @Override public int[] executeBatch(Map... batch) { return doExecuteBatch(batch); } diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java index a9c3231eaf18..ce9d1bc5a195 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.TreeMap; @@ -373,6 +372,7 @@ private static class ParameterHolder { private final int endIndex; ParameterHolder(String parameterName, int startIndex, int endIndex) { + Assert.notNull(parameterName, "Parameter name must not be null"); this.parameterName = parameterName; this.startIndex = startIndex; this.endIndex = endIndex; @@ -391,21 +391,21 @@ int getEndIndex() { } @Override - public boolean equals(Object o) { - if (this == o) { + public boolean equals(Object other) { + if (this == other) { return true; } - if (!(o instanceof ParameterHolder)) { + if (!(other instanceof ParameterHolder)) { return false; } - ParameterHolder that = (ParameterHolder) o; - return this.startIndex == that.startIndex && this.endIndex == that.endIndex - && Objects.equals(this.parameterName, that.parameterName); + ParameterHolder that = (ParameterHolder) other; + return (this.startIndex == that.startIndex && this.endIndex == that.endIndex && + this.parameterName.equals(that.parameterName)); } @Override public int hashCode() { - return Objects.hash(this.parameterName, this.startIndex, this.endIndex); + return this.parameterName.hashCode(); } } diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/Parameter.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/Parameter.java index 7615477fcedf..bea746611a3b 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/Parameter.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/Parameter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package org.springframework.r2dbc.core; -import java.util.Objects; - import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -109,21 +107,21 @@ public boolean isEmpty() { @Override - public boolean equals(Object obj) { - if (this == obj) { + public boolean equals(Object other) { + if (this == other) { return true; } - if (!(obj instanceof Parameter)) { + if (!(other instanceof Parameter)) { return false; } - Parameter other = (Parameter) obj; - return (ObjectUtils.nullSafeEquals(this.value, other.value) && - ObjectUtils.nullSafeEquals(this.type, other.type)); + Parameter that = (Parameter) other; + return (ObjectUtils.nullSafeEquals(this.value, that.value) && + ObjectUtils.nullSafeEquals(this.type, that.type)); } @Override public int hashCode() { - return Objects.hash(this.value, this.type); + return ObjectUtils.nullSafeHashCode(this.value) + ObjectUtils.nullSafeHashCode(this.type); } @Override diff --git a/src/docs/asciidoc/data-access.adoc b/src/docs/asciidoc/data-access.adoc index 88ddf5c2fe2f..46d97b22e909 100644 --- a/src/docs/asciidoc/data-access.adoc +++ b/src/docs/asciidoc/data-access.adoc @@ -3636,11 +3636,8 @@ parameters. The following example shows how to use `NamedParameterJdbcTemplate`: } public int countOfActorsByFirstName(String firstName) { - - String sql = "select count(*) from T_ACTOR where first_name = :first_name"; - + String sql = "select count(*) from t_actor where first_name = :first_name"; SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName); - return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class); } ---- @@ -3651,7 +3648,7 @@ parameters. The following example shows how to use `NamedParameterJdbcTemplate`: private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource) fun countOfActorsByFirstName(firstName: String): Int { - val sql = "select count(*) from T_ACTOR where first_name = :first_name" + val sql = "select count(*) from t_actor where first_name = :first_name" val namedParameters = MapSqlParameterSource("first_name", firstName) return namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Int::class.java)!! } @@ -3679,11 +3676,8 @@ The following example shows the use of the `Map`-based style: } public int countOfActorsByFirstName(String firstName) { - - String sql = "select count(*) from T_ACTOR where first_name = :first_name"; - + String sql = "select count(*) from t_actor where first_name = :first_name"; Map namedParameters = Collections.singletonMap("first_name", firstName); - return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class); } ---- @@ -3694,7 +3688,7 @@ The following example shows the use of the `Map`-based style: private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource) fun countOfActorsByFirstName(firstName: String): Int { - val sql = "select count(*) from T_ACTOR where first_name = :first_name" + val sql = "select count(*) from t_actor where first_name = :first_name" val namedParameters = mapOf("first_name" to firstName) return namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Int::class.java)!! } @@ -3761,12 +3755,9 @@ members of the class shown in the preceding example: } public int countOfActors(Actor exampleActor) { - // notice how the named parameters match the properties of the above 'Actor' class - String sql = "select count(*) from T_ACTOR where first_name = :firstName and last_name = :lastName"; - + String sql = "select count(*) from t_actor where first_name = :firstName and last_name = :lastName"; SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(exampleActor); - return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class); } ---- @@ -3780,7 +3771,7 @@ members of the class shown in the preceding example: fun countOfActors(exampleActor: Actor): Int { // notice how the named parameters match the properties of the above 'Actor' class - val sql = "select count(*) from T_ACTOR where first_name = :firstName and last_name = :lastName" + val sql = "select count(*) from t_actor where first_name = :firstName and last_name = :lastName" val namedParameters = BeanPropertySqlParameterSource(exampleActor) return namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Int::class.java)!! } @@ -6089,7 +6080,7 @@ The following example shows how to do so: ==== Passing in Lists of Values for IN Clause The SQL standard allows for selecting rows based on an expression that includes a -variable list of values. A typical example would be `select * from T_ACTOR where id in +variable list of values. A typical example would be `select * from t_actor where id in (1, 2, 3)`. This variable list is not directly supported for prepared statements by the JDBC standard. You cannot declare a variable number of placeholders. You need a number of variations with the desired number of placeholders prepared, or you need to generate @@ -6106,7 +6097,7 @@ limit is 1000. In addition to the primitive values in the value list, you can create a `java.util.List` of object arrays. This list can support multiple expressions being defined for the `in` -clause, such as `+++select * from T_ACTOR where (id, last_name) in ((1, 'Johnson'), (2, +clause, such as `+++select * from t_actor where (id, last_name) in ((1, 'Johnson'), (2, 'Harrop'))+++`. This, of course, requires that your database supports this syntax. diff --git a/src/docs/asciidoc/integration.adoc b/src/docs/asciidoc/integration.adoc index a05ea6e5599e..ba1fc2e14d99 100644 --- a/src/docs/asciidoc/integration.adoc +++ b/src/docs/asciidoc/integration.adoc @@ -6033,7 +6033,7 @@ confirm the exclusion. [[cache-annotations-evict]] -==== The `@CacheEvict` annotation +==== The `@CacheEvict` Annotation The cache abstraction allows not just population of a cache store but also eviction. This process is useful for removing stale or unused data from the cache. As opposed to @@ -6091,7 +6091,7 @@ The following example uses two `@CacheEvict` annotations: [[cache-annotations-config]] -==== The `@CacheConfig` annotation +==== The `@CacheConfig` Annotation So far, we have seen that caching operations offer many customization options and that you can set these options for each operation. However, some of the customization options From c5aa7830bce393bc5339a11137bcc9773407fe25 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 27 Jul 2023 21:47:54 +0200 Subject: [PATCH 18/47] Polishing (cherry picked from commit abbea398550300f96ce69e1ddf796d9fb5bca494) --- .../annotation/SchedulingConfigurer.java | 10 ++++---- .../core/task/SimpleAsyncTaskExecutor.java | 24 +++++++++---------- .../util/ConcurrencyThrottleSupport.java | 7 +++--- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/SchedulingConfigurer.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/SchedulingConfigurer.java index 26f0076077f7..7ad039fb8dd7 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/SchedulingConfigurer.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/SchedulingConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,10 +40,10 @@ public interface SchedulingConfigurer { /** - * Callback allowing a {@link org.springframework.scheduling.TaskScheduler - * TaskScheduler} and specific {@link org.springframework.scheduling.config.Task Task} - * instances to be registered against the given the {@link ScheduledTaskRegistrar}. - * @param taskRegistrar the registrar to be configured. + * Callback allowing a {@link org.springframework.scheduling.TaskScheduler} + * and specific {@link org.springframework.scheduling.config.Task} instances + * to be registered against the given the {@link ScheduledTaskRegistrar}. + * @param taskRegistrar the registrar to be configured */ void configureTasks(ScheduledTaskRegistrar taskRegistrar); diff --git a/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java b/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java index 072502a868c9..8540697a52b3 100644 --- a/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java +++ b/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,8 +33,8 @@ * {@link TaskExecutor} implementation that fires up a new Thread for each task, * executing it asynchronously. * - *

    Supports limiting concurrent threads through the "concurrencyLimit" - * bean property. By default, the number of concurrent threads is unlimited. + *

    Supports limiting concurrent threads through {@link #setConcurrencyLimit}. + * By default, the number of concurrent task executions is unlimited. * *

    NOTE: This implementation does not reuse threads! Consider a * thread-pooling TaskExecutor implementation instead, in particular for @@ -133,33 +133,31 @@ public final ThreadFactory getThreadFactory() { * have to cast it and call {@code Future#get} to evaluate exceptions. * @since 4.3 */ - public final void setTaskDecorator(TaskDecorator taskDecorator) { + public void setTaskDecorator(TaskDecorator taskDecorator) { this.taskDecorator = taskDecorator; } /** - * Set the maximum number of parallel accesses allowed. - * -1 indicates no concurrency limit at all. - *

    In principle, this limit can be changed at runtime, - * although it is generally designed as a config time setting. - * NOTE: Do not switch between -1 and any concrete limit at runtime, - * as this will lead to inconsistent concurrency counts: A limit - * of -1 effectively turns off concurrency counting completely. + * Set the maximum number of parallel task executions allowed. + * The default of -1 indicates no concurrency limit at all. + *

    This is the equivalent of a maximum pool size in a thread pool, + * preventing temporary overload of the thread management system. * @see #UNBOUNDED_CONCURRENCY + * @see org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor#setMaxPoolSize */ public void setConcurrencyLimit(int concurrencyLimit) { this.concurrencyThrottle.setConcurrencyLimit(concurrencyLimit); } /** - * Return the maximum number of parallel accesses allowed. + * Return the maximum number of parallel task executions allowed. */ public final int getConcurrencyLimit() { return this.concurrencyThrottle.getConcurrencyLimit(); } /** - * Return whether this throttle is currently active. + * Return whether the concurrency throttle is currently active. * @return {@code true} if the concurrency limit for this instance is active * @see #getConcurrencyLimit() * @see #setConcurrencyLimit diff --git a/spring-core/src/main/java/org/springframework/util/ConcurrencyThrottleSupport.java b/spring-core/src/main/java/org/springframework/util/ConcurrencyThrottleSupport.java index 6d7d272b4fda..370537bf9a38 100644 --- a/spring-core/src/main/java/org/springframework/util/ConcurrencyThrottleSupport.java +++ b/spring-core/src/main/java/org/springframework/util/ConcurrencyThrottleSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -69,7 +69,7 @@ public abstract class ConcurrencyThrottleSupport implements Serializable { /** * Set the maximum number of concurrent access attempts allowed. - * -1 indicates unbounded concurrency. + * The default of -1 indicates no concurrency limit at all. *

    In principle, this limit can be changed at runtime, * although it is generally designed as a config time setting. *

    NOTE: Do not switch between -1 and any concrete limit at runtime, @@ -143,9 +143,10 @@ protected void beforeAccess() { */ protected void afterAccess() { if (this.concurrencyLimit >= 0) { + boolean debug = logger.isDebugEnabled(); synchronized (this.monitor) { this.concurrencyCount--; - if (logger.isDebugEnabled()) { + if (debug) { logger.debug("Returning from throttle at concurrency count " + this.concurrencyCount); } this.monitor.notify(); From ad61fb75dac505c0fbe6ac3e803d74cf2dedd243 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 2 Aug 2023 01:24:11 +0200 Subject: [PATCH 19/47] Polishing --- .../SimpleApplicationEventMulticaster.java | 12 +++++----- .../annotation/EnableSchedulingTests.java | 16 +++++++------- .../connection/ConnectionFactoryUtils.java | 22 +++++++++---------- .../reactive/TransactionContextManager.java | 8 +++---- .../TransactionSynchronizationManager.java | 16 ++++++++------ .../TransactionalEventListenerTests.java | 11 +++------- .../servlet/handler/MappedInterceptor.java | 4 ++-- 7 files changed, 44 insertions(+), 45 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java b/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java index d1f9b8ca330a..2de53ce29021 100644 --- a/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java +++ b/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -79,10 +79,11 @@ public SimpleApplicationEventMulticaster(BeanFactory beanFactory) { * to invoke each listener with. *

    Default is equivalent to {@link org.springframework.core.task.SyncTaskExecutor}, * executing all listeners synchronously in the calling thread. - *

    Consider specifying an asynchronous task executor here to not block the - * caller until all listeners have been executed. However, note that asynchronous - * execution will not participate in the caller's thread context (class loader, - * transaction association) unless the TaskExecutor explicitly supports this. + *

    Consider specifying an asynchronous task executor here to not block the caller + * until all listeners have been executed. However, note that asynchronous execution + * will not participate in the caller's thread context (class loader, transaction context) + * unless the TaskExecutor explicitly supports this. + * @since 2.0 * @see org.springframework.core.task.SyncTaskExecutor * @see org.springframework.core.task.SimpleAsyncTaskExecutor */ @@ -92,6 +93,7 @@ public void setTaskExecutor(@Nullable Executor taskExecutor) { /** * Return the current task executor for this multicaster. + * @since 2.0 */ @Nullable protected Executor getTaskExecutor() { diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableSchedulingTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableSchedulingTests.java index 25f59ac8bddc..dc718fdeacef 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableSchedulingTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableSchedulingTests.java @@ -63,7 +63,7 @@ public void withFixedRateTask() throws InterruptedException { ctx = new AnnotationConfigApplicationContext(FixedRateTaskConfig.class); assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(2); - Thread.sleep(100); + Thread.sleep(110); assertThat(ctx.getBean(AtomicInteger.class).get()).isGreaterThanOrEqualTo(10); } @@ -73,7 +73,7 @@ public void withSubclass() throws InterruptedException { ctx = new AnnotationConfigApplicationContext(FixedRateTaskConfigSubclass.class); assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(2); - Thread.sleep(100); + Thread.sleep(110); assertThat(ctx.getBean(AtomicInteger.class).get()).isGreaterThanOrEqualTo(10); } @@ -83,7 +83,7 @@ public void withExplicitScheduler() throws InterruptedException { ctx = new AnnotationConfigApplicationContext(ExplicitSchedulerConfig.class); assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(1); - Thread.sleep(100); + Thread.sleep(110); assertThat(ctx.getBean(AtomicInteger.class).get()).isGreaterThanOrEqualTo(10); assertThat(ctx.getBean(ExplicitSchedulerConfig.class).threadName).startsWith("explicitScheduler-"); assertThat(Arrays.asList(ctx.getDefaultListableBeanFactory().getDependentBeans("myTaskScheduler")).contains( @@ -102,7 +102,7 @@ public void withExplicitScheduledTaskRegistrar() throws InterruptedException { ctx = new AnnotationConfigApplicationContext(ExplicitScheduledTaskRegistrarConfig.class); assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(1); - Thread.sleep(100); + Thread.sleep(110); assertThat(ctx.getBean(AtomicInteger.class).get()).isGreaterThanOrEqualTo(10); assertThat(ctx.getBean(ExplicitScheduledTaskRegistrarConfig.class).threadName).startsWith("explicitScheduler1"); } @@ -124,7 +124,7 @@ public void withAmbiguousTaskSchedulers_andSingleTask_disambiguatedByScheduledTa ctx = new AnnotationConfigApplicationContext( SchedulingEnabled_withAmbiguousTaskSchedulers_andSingleTask_disambiguatedByScheduledTaskRegistrar.class); - Thread.sleep(100); + Thread.sleep(110); assertThat(ctx.getBean(ThreadAwareWorker.class).executedByThread).startsWith("explicitScheduler2-"); } @@ -134,7 +134,7 @@ public void withAmbiguousTaskSchedulers_andSingleTask_disambiguatedBySchedulerNa ctx = new AnnotationConfigApplicationContext( SchedulingEnabled_withAmbiguousTaskSchedulers_andSingleTask_disambiguatedBySchedulerNameAttribute.class); - Thread.sleep(100); + Thread.sleep(110); assertThat(ctx.getBean(ThreadAwareWorker.class).executedByThread).startsWith("explicitScheduler2-"); } @@ -143,7 +143,7 @@ public void withAmbiguousTaskSchedulers_andSingleTask_disambiguatedBySchedulerNa public void withTaskAddedVia_configureTasks() throws InterruptedException { ctx = new AnnotationConfigApplicationContext(SchedulingEnabled_withTaskAddedVia_configureTasks.class); - Thread.sleep(100); + Thread.sleep(110); assertThat(ctx.getBean(ThreadAwareWorker.class).executedByThread).startsWith("taskScheduler-"); } @@ -152,7 +152,7 @@ public void withTaskAddedVia_configureTasks() throws InterruptedException { public void withTriggerTask() throws InterruptedException { ctx = new AnnotationConfigApplicationContext(TriggerTaskConfig.class); - Thread.sleep(100); + Thread.sleep(110); assertThat(ctx.getBean(AtomicInteger.class).get()).isGreaterThan(1); } diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionFactoryUtils.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionFactoryUtils.java index c69dc2f49e57..33a9b6bf8238 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionFactoryUtils.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionFactoryUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -85,7 +85,7 @@ public abstract class ConnectionFactoryUtils { */ public static Mono getConnection(ConnectionFactory connectionFactory) { return doGetConnection(connectionFactory) - .onErrorMap(e -> new DataAccessResourceFailureException("Failed to obtain R2DBC Connection", e)); + .onErrorMap(ex -> new DataAccessResourceFailureException("Failed to obtain R2DBC Connection", ex)); } /** @@ -124,17 +124,17 @@ public static Mono doGetConnection(ConnectionFactory connectionFacto holderToUse.setConnection(conn); } holderToUse.requested(); - synchronizationManager - .registerSynchronization(new ConnectionSynchronization(holderToUse, connectionFactory)); + synchronizationManager.registerSynchronization( + new ConnectionSynchronization(holderToUse, connectionFactory)); holderToUse.setSynchronizedWithTransaction(true); if (holderToUse != conHolder) { synchronizationManager.bindResource(connectionFactory, holderToUse); } }) // Unexpected exception from external delegation call -> close Connection and rethrow. - .onErrorResume(e -> releaseConnection(connection, connectionFactory).then(Mono.error(e)))); + .onErrorResume(ex -> releaseConnection(connection, connectionFactory).then(Mono.error(ex)))); } return con; - }).onErrorResume(NoTransactionException.class, e -> Mono.from(connectionFactory.create())); + }).onErrorResume(NoTransactionException.class, ex -> Mono.from(connectionFactory.create())); } /** @@ -159,7 +159,7 @@ private static Mono fetchConnection(ConnectionFactory connectionFact */ public static Mono releaseConnection(Connection con, ConnectionFactory connectionFactory) { return doReleaseConnection(con, connectionFactory) - .onErrorMap(e -> new DataAccessResourceFailureException("Failed to close R2DBC Connection", e)); + .onErrorMap(ex -> new DataAccessResourceFailureException("Failed to close R2DBC Connection", ex)); } /** @@ -171,15 +171,14 @@ public static Mono releaseConnection(Connection con, ConnectionFactory con * @see #doGetConnection */ public static Mono doReleaseConnection(Connection connection, ConnectionFactory connectionFactory) { - return TransactionSynchronizationManager.forCurrentTransaction() - .flatMap(synchronizationManager -> { + return TransactionSynchronizationManager.forCurrentTransaction().flatMap(synchronizationManager -> { ConnectionHolder conHolder = (ConnectionHolder) synchronizationManager.getResource(connectionFactory); if (conHolder != null && connectionEquals(conHolder, connection)) { // It's the transactional Connection: Don't close it. conHolder.released(); } return Mono.from(connection.close()); - }).onErrorResume(NoTransactionException.class, e -> Mono.from(connection.close())); + }).onErrorResume(NoTransactionException.class, ex -> Mono.from(connection.close())); } /** @@ -268,7 +267,8 @@ private static boolean connectionEquals(ConnectionHolder conHolder, Connection p Connection heldCon = conHolder.getConnection(); // Explicitly check for identity too: for Connection handles that do not implement // "equals" properly). - return (heldCon == passedInCon || heldCon.equals(passedInCon) || getTargetConnection(heldCon).equals(passedInCon)); + return (heldCon == passedInCon || heldCon.equals(passedInCon) || + getTargetConnection(heldCon).equals(passedInCon)); } /** diff --git a/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionContextManager.java b/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionContextManager.java index db6f4fb414ce..cac1f04133c7 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionContextManager.java +++ b/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionContextManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,10 +46,10 @@ private TransactionContextManager() { * transactional context holder. Context retrieval fails with NoTransactionException * if no context or context holder is registered. * @return the current {@link TransactionContext} - * @throws NoTransactionException if no TransactionContext was found in the subscriber context - * or no context found in a holder + * @throws NoTransactionException if no TransactionContext was found in the + * subscriber context or no context found in a holder */ - public static Mono currentContext() throws NoTransactionException { + public static Mono currentContext() { return Mono.deferContextual(ctx -> { if (ctx.hasKey(TransactionContext.class)) { return Mono.just(ctx.get(TransactionContext.class)); diff --git a/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionSynchronizationManager.java b/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionSynchronizationManager.java index 54f7cd17a03d..27ed7f41739b 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionSynchronizationManager.java +++ b/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionSynchronizationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,8 +32,8 @@ /** * Central delegate that manages resources and transaction synchronizations per - * subscriber context. - * To be used by resource management code but not by typical application code. + * subscriber context. To be used by resource management code but not by typical + * application code. * *

    Supports one resource per key without overwriting, that is, a resource needs * to be removed before a new one can be set for the same key. @@ -73,6 +73,7 @@ public class TransactionSynchronizationManager { public TransactionSynchronizationManager(TransactionContext transactionContext) { + Assert.notNull(transactionContext, "TransactionContext must not be null"); this.transactionContext = transactionContext; } @@ -88,10 +89,11 @@ public static Mono forCurrentTransaction() { return TransactionContextManager.currentContext().map(TransactionSynchronizationManager::new); } + /** - * Check if there is a resource for the given key bound to the current thread. + * Check if there is a resource for the given key bound to the current context. * @param key the key to check (usually the resource factory) - * @return if there is a value bound to the current thread + * @return if there is a value bound to the current context */ public boolean hasResource(Object key) { Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); @@ -100,9 +102,9 @@ public boolean hasResource(Object key) { } /** - * Retrieve a resource for the given key that is bound to the current thread. + * Retrieve a resource for the given key that is bound to the current context. * @param key the key to check (usually the resource factory) - * @return a value bound to the current thread (usually the active + * @return a value bound to the current context (usually the active * resource object), or {@code null} if none */ @Nullable diff --git a/spring-tx/src/test/java/org/springframework/transaction/event/TransactionalEventListenerTests.java b/spring-tx/src/test/java/org/springframework/transaction/event/TransactionalEventListenerTests.java index 22ed99a2b9aa..ff215f05133a 100644 --- a/spring-tx/src/test/java/org/springframework/transaction/event/TransactionalEventListenerTests.java +++ b/spring-tx/src/test/java/org/springframework/transaction/event/TransactionalEventListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,6 +57,7 @@ /** * Integration tests for {@link TransactionalEventListener} support + * with thread-bound transactions. * * @author Stephane Nicoll * @author Sam Brannen @@ -87,7 +88,6 @@ public void immediately() { getEventCollector().assertEvents(EventCollector.IMMEDIATELY, "test"); getEventCollector().assertTotalEventsCount(1); return null; - }); getEventCollector().assertEvents(EventCollector.IMMEDIATELY, "test"); getEventCollector().assertTotalEventsCount(1); @@ -115,7 +115,6 @@ public void afterCompletionCommit() { getContext().publishEvent("test"); getEventCollector().assertNoEventReceived(); return null; - }); getEventCollector().assertEvents(EventCollector.AFTER_COMPLETION, "test"); getEventCollector().assertTotalEventsCount(1); // After rollback not invoked @@ -129,7 +128,6 @@ public void afterCompletionRollback() { getEventCollector().assertNoEventReceived(); status.setRollbackOnly(); return null; - }); getEventCollector().assertEvents(EventCollector.AFTER_COMPLETION, "test"); getEventCollector().assertTotalEventsCount(1); // After rollback not invoked @@ -142,7 +140,6 @@ public void afterCommit() { getContext().publishEvent("test"); getEventCollector().assertNoEventReceived(); return null; - }); getEventCollector().assertEvents(EventCollector.AFTER_COMMIT, "test"); getEventCollector().assertTotalEventsCount(1); // After rollback not invoked @@ -307,13 +304,12 @@ public void conditionFoundOnTransactionalEventListener() { } @Test - public void afterCommitMetaAnnotation() throws Exception { + public void afterCommitMetaAnnotation() { load(AfterCommitMetaAnnotationTestListener.class); this.transactionTemplate.execute(status -> { getContext().publishEvent("test"); getEventCollector().assertNoEventReceived(); return null; - }); getEventCollector().assertEvents(EventCollector.AFTER_COMMIT, "test"); getEventCollector().assertTotalEventsCount(1); @@ -326,7 +322,6 @@ public void conditionFoundOnMetaAnnotation() { getContext().publishEvent("SKIP"); getEventCollector().assertNoEventReceived(); return null; - }); getEventCollector().assertNoEventReceived(); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MappedInterceptor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MappedInterceptor.java index 2467605c77f7..d7abc59b19da 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MappedInterceptor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MappedInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,7 +61,7 @@ */ public final class MappedInterceptor implements HandlerInterceptor { - private static PathMatcher defaultPathMatcher = new AntPathMatcher(); + private static final PathMatcher defaultPathMatcher = new AntPathMatcher(); @Nullable From b9482375b7183f4205e8550ed2fcdedf550b3cc5 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 4 Aug 2023 00:47:18 +0200 Subject: [PATCH 20/47] Sort multiple @Autowired methods on same bean class via ASM Closes gh-30359 (cherry picked from commit 7e6612a920219f2dd811f55ec0d6a1d282b15aee) --- .../AutowiredAnnotationBeanPostProcessor.java | 63 +++++++++++++++++-- ...wiredAnnotationBeanPostProcessorTests.java | 15 +++-- ...onfigurationClassBeanDefinitionReader.java | 3 +- .../annotation/ConfigurationClassParser.java | 7 ++- 4 files changed, 71 insertions(+), 17 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 4e0f365bc85c..95fa55766032 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 @@ -17,6 +17,7 @@ package org.springframework.beans.factory.annotation; import java.beans.PropertyDescriptor; +import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Constructor; @@ -62,6 +63,10 @@ import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.MethodMetadata; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -144,6 +149,9 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA @Nullable private ConfigurableListableBeanFactory beanFactory; + @Nullable + private MetadataReaderFactory metadataReaderFactory; + private final Set lookupMethodsChecked = Collections.newSetFromMap(new ConcurrentHashMap<>(256)); private final Map, Constructor[]> candidateConstructorsCache = new ConcurrentHashMap<>(256); @@ -238,6 +246,7 @@ public void setBeanFactory(BeanFactory beanFactory) { "AutowiredAnnotationBeanPostProcessor requires a ConfigurableListableBeanFactory: " + beanFactory); } this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; + this.metadataReaderFactory = new SimpleMetadataReaderFactory(this.beanFactory.getBeanClassLoader()); } @@ -463,12 +472,11 @@ private InjectionMetadata buildAutowiringMetadata(Class clazz) { return InjectionMetadata.EMPTY; } - List elements = new ArrayList<>(); + final List elements = new ArrayList<>(); Class targetClass = clazz; do { - final List currElements = new ArrayList<>(); - + final List fieldElements = new ArrayList<>(); ReflectionUtils.doWithLocalFields(targetClass, field -> { MergedAnnotation ann = findAutowiredAnnotation(field); if (ann != null) { @@ -479,10 +487,11 @@ private InjectionMetadata buildAutowiringMetadata(Class clazz) { return; } boolean required = determineRequiredStatus(ann); - currElements.add(new AutowiredFieldElement(field, required)); + fieldElements.add(new AutowiredFieldElement(field, required)); } }); + final List methodElements = new ArrayList<>(); ReflectionUtils.doWithLocalMethods(targetClass, method -> { Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) { @@ -504,11 +513,12 @@ private InjectionMetadata buildAutowiringMetadata(Class clazz) { } boolean required = determineRequiredStatus(ann); PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz); - currElements.add(new AutowiredMethodElement(method, required, pd)); + methodElements.add(new AutowiredMethodElement(method, required, pd)); } }); - elements.addAll(0, currElements); + elements.addAll(0, sortMethodElements(methodElements, targetClass)); + elements.addAll(0, fieldElements); targetClass = targetClass.getSuperclass(); } while (targetClass != null && targetClass != Object.class); @@ -573,6 +583,47 @@ protected Map findAutowireCandidates(Class type) throws BeansE return BeanFactoryUtils.beansOfTypeIncludingAncestors(this.beanFactory, type); } + /** + * Sort the method elements via ASM for deterministic declaration order if possible. + */ + private List sortMethodElements( + List methodElements, Class targetClass) { + + if (this.metadataReaderFactory != null && methodElements.size() > 1) { + // Try reading the class file via ASM for deterministic declaration order... + // Unfortunately, the JVM's standard reflection returns methods in arbitrary + // order, even between different runs of the same application on the same JVM. + try { + AnnotationMetadata asm = + this.metadataReaderFactory.getMetadataReader(targetClass.getName()).getAnnotationMetadata(); + Set asmMethods = asm.getAnnotatedMethods(Autowired.class.getName()); + if (asmMethods.size() >= methodElements.size()) { + List candidateMethods = new ArrayList<>(methodElements); + List selectedMethods = new ArrayList<>(asmMethods.size()); + for (MethodMetadata asmMethod : asmMethods) { + for (Iterator it = candidateMethods.iterator(); it.hasNext();) { + InjectionMetadata.InjectedElement element = it.next(); + if (element.getMember().getName().equals(asmMethod.getMethodName())) { + selectedMethods.add(element); + it.remove(); + break; + } + } + } + if (selectedMethods.size() == methodElements.size()) { + // All reflection-detected methods found in ASM method set -> proceed + return selectedMethods; + } + } + } + catch (IOException ex) { + logger.debug("Failed to read class file via ASM for determining @Autowired method order", ex); + // No worries, let's continue with the reflection metadata we started with... + } + } + return methodElements; + } + /** * Register the specified bean as dependent on the autowired beans. */ diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java index b7d89f13de04..45f59fb1f689 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java @@ -73,6 +73,7 @@ import org.springframework.core.annotation.Order; import org.springframework.core.testfixture.io.SerializationTestUtils; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; @@ -2620,13 +2621,12 @@ public static class ResourceInjectionBean { @Autowired(required = false) private TestBean testBean; - private TestBean testBean2; + TestBean testBean2; @Autowired public void setTestBean2(TestBean testBean2) { - if (this.testBean2 != null) { - throw new IllegalStateException("Already called"); - } + Assert.state(this.testBean != null, "Wrong initialization order"); + Assert.state(this.testBean2 == null, "Already called"); this.testBean2 = testBean2; } @@ -2661,7 +2661,7 @@ public NonPublicResourceInjectionBean() { @Required @SuppressWarnings("deprecation") public void setTestBean2(TestBean testBean2) { - super.setTestBean2(testBean2); + this.testBean2 = testBean2; } @Autowired @@ -2677,6 +2677,7 @@ private void inject(ITestBean testBean4) { @Autowired protected void initBeanFactory(BeanFactory beanFactory) { + Assert.state(this.baseInjected, "Wrong initialization order"); this.beanFactory = beanFactory; } @@ -4100,9 +4101,7 @@ public static abstract class Foo> { private RT obj; protected void setObj(RT obj) { - if (this.obj != null) { - throw new IllegalStateException("Already called"); - } + Assert.state(this.obj == null, "Already called"); this.obj = obj; } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java index 427e2bec83b2..fe050329e727 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -424,6 +424,7 @@ public ConfigurationClassBeanDefinition( public ConfigurationClassBeanDefinition(RootBeanDefinition original, ConfigurationClass configClass, MethodMetadata beanMethodMetadata, String derivedBeanName) { + super(original); this.annotationMetadata = configClass.getMetadata(); this.factoryMethodMetadata = beanMethodMetadata; diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index fe58c8f2d648..633780ec4a70 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -409,11 +409,14 @@ private Set retrieveBeanMethodMetadata(SourceClass sourceClass) this.metadataReaderFactory.getMetadataReader(original.getClassName()).getAnnotationMetadata(); Set asmMethods = asm.getAnnotatedMethods(Bean.class.getName()); if (asmMethods.size() >= beanMethods.size()) { + Set candidateMethods = new LinkedHashSet<>(beanMethods); Set selectedMethods = new LinkedHashSet<>(asmMethods.size()); for (MethodMetadata asmMethod : asmMethods) { - for (MethodMetadata beanMethod : beanMethods) { + for (Iterator it = candidateMethods.iterator(); it.hasNext();) { + MethodMetadata beanMethod = it.next(); if (beanMethod.getMethodName().equals(asmMethod.getMethodName())) { selectedMethods.add(beanMethod); + it.remove(); break; } } From 24893d038f6a03a6bb795aab56b9f50118aac171 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 4 Aug 2023 10:35:51 +0200 Subject: [PATCH 21/47] Polishing --- .../context/annotation/BeanMethod.java | 10 ++++++---- .../core/ParameterizedTypeReference.java | 6 ++---- .../springframework/core/env/ProfilesParser.java | 16 ++++++---------- .../SimpleAnnotationMetadataReadingVisitor.java | 14 +++++++------- .../hibernate5/SpringFlushSynchronization.java | 3 +-- .../DynamicPropertiesContextCustomizer.java | 16 +++++++--------- 6 files changed, 29 insertions(+), 36 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java b/spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java index 6d3459fb08c7..abe73dacc60f 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +39,7 @@ final class BeanMethod extends ConfigurationMethod { super(metadata, configurationClass); } + @Override public void validate(ProblemReporter problemReporter) { if (getMetadata().isStatic()) { @@ -55,9 +56,9 @@ public void validate(ProblemReporter problemReporter) { } @Override - public boolean equals(@Nullable Object obj) { - return ((this == obj) || ((obj instanceof BeanMethod) && - this.metadata.equals(((BeanMethod) obj).metadata))); + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof BeanMethod && + this.metadata.equals(((BeanMethod) other).metadata))); } @Override @@ -70,6 +71,7 @@ public String toString() { return "BeanMethod: " + this.metadata; } + private class NonOverridableMethodError extends Problem { NonOverridableMethodError() { diff --git a/spring-core/src/main/java/org/springframework/core/ParameterizedTypeReference.java b/spring-core/src/main/java/org/springframework/core/ParameterizedTypeReference.java index c10dd89aee9e..8e7bd2809f43 100644 --- a/spring-core/src/main/java/org/springframework/core/ParameterizedTypeReference.java +++ b/spring-core/src/main/java/org/springframework/core/ParameterizedTypeReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,6 @@ * limitations under the License. */ - package org.springframework.core; import java.lang.reflect.ParameterizedType; @@ -92,8 +91,7 @@ public String toString() { * @since 4.3.12 */ public static ParameterizedTypeReference forType(Type type) { - return new ParameterizedTypeReference(type) { - }; + return new ParameterizedTypeReference(type) {}; } private static Class findParameterizedTypeReferenceSubclass(Class child) { diff --git a/spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java b/spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java index 5e7fce7c6c41..9b666f91ed61 100644 --- a/spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java +++ b/spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java @@ -168,31 +168,27 @@ public boolean matches(Predicate activeProfiles) { return false; } - @Override - public int hashCode() { - return this.expressions.hashCode(); - } - @Override public boolean equals(Object obj) { if (this == obj) { return true; } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { + if (obj == null || getClass() != obj.getClass()) { return false; } ParsedProfiles that = (ParsedProfiles) obj; return this.expressions.equals(that.expressions); } + @Override + public int hashCode() { + return this.expressions.hashCode(); + } + @Override public String toString() { return StringUtils.collectionToDelimitedString(this.expressions, " or "); } - } } diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleAnnotationMetadataReadingVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleAnnotationMetadataReadingVisitor.java index a951194ae7dc..700fcc5138c1 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleAnnotationMetadataReadingVisitor.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleAnnotationMetadataReadingVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -171,6 +171,7 @@ private boolean isInterface(int access) { return (access & Opcodes.ACC_INTERFACE) != 0; } + /** * {@link MergedAnnotation} source. */ @@ -182,11 +183,6 @@ private static final class Source { this.className = className; } - @Override - public int hashCode() { - return this.className.hashCode(); - } - @Override public boolean equals(@Nullable Object obj) { if (this == obj) { @@ -198,11 +194,15 @@ public boolean equals(@Nullable Object obj) { return this.className.equals(((Source) obj).className); } + @Override + public int hashCode() { + return this.className.hashCode(); + } + @Override public String toString() { return this.className; } - } } diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringFlushSynchronization.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringFlushSynchronization.java index 94579851887d..cd64a15e80ed 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringFlushSynchronization.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringFlushSynchronization.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,6 @@ public void flush() { SessionFactoryUtils.flush(this.session, false); } - @Override public boolean equals(@Nullable Object other) { return (this == other || (other instanceof SpringFlushSynchronization && diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizer.java b/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizer.java index d7093a066a7a..b419f0b9e412 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizer.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,7 +46,6 @@ class DynamicPropertiesContextCustomizer implements ContextCustomizer { private static final String PROPERTY_SOURCE_NAME = "Dynamic Test Properties"; - private final Set methods; @@ -65,9 +64,7 @@ private void assertValid(Method method) { } @Override - public void customizeContext(ConfigurableApplicationContext context, - MergedContextConfiguration mergedConfig) { - + public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { MutablePropertySources sources = context.getEnvironment().getPropertySources(); sources.addFirst(new DynamicValuesPropertySource(PROPERTY_SOURCE_NAME, buildDynamicPropertiesMap())); } @@ -90,10 +87,6 @@ Set getMethods() { return this.methods; } - @Override - public int hashCode() { - return this.methods.hashCode(); - } @Override public boolean equals(Object obj) { @@ -106,4 +99,9 @@ public boolean equals(Object obj) { return this.methods.equals(((DynamicPropertiesContextCustomizer) obj).methods); } + @Override + public int hashCode() { + return this.methods.hashCode(); + } + } From 9d7154901ff56153b5f7d2b2a4d499963353478b Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sun, 6 Aug 2023 14:25:39 +0200 Subject: [PATCH 22/47] Polishing (cherry picked from commit 6e5af9dccb453dd9adfe997c219eef31fd7ab903) --- ...ansactionalAnnotationIntegrationTests.java | 12 +-- .../adapter/ThrowsAdviceInterceptorTests.java | 14 +-- .../scheduling/quartz/QuartzSupportTests.java | 26 ++--- .../ClassPathBeanDefinitionScanner.java | 18 ++-- .../ClassPathBeanDefinitionScannerTests.java | 2 + .../core/ReactiveAdapterRegistry.java | 23 ++-- .../core/ReactiveTypeDescriptor.java | 14 +-- .../core/ReactiveAdapterRegistryTests.java | 26 ++--- .../R2dbcTransactionManagerUnitTests.java | 100 +++++++++--------- .../PlatformTransactionManager.java | 3 +- .../transaction/ReactiveTransaction.java | 5 +- .../transaction/TransactionStatus.java | 5 +- .../support/SmartTransactionObject.java | 8 +- 13 files changed, 125 insertions(+), 131 deletions(-) diff --git a/integration-tests/src/test/java/org/springframework/scheduling/annotation/ScheduledAndTransactionalAnnotationIntegrationTests.java b/integration-tests/src/test/java/org/springframework/scheduling/annotation/ScheduledAndTransactionalAnnotationIntegrationTests.java index 9062d6e3f615..be62d81ee013 100644 --- a/integration-tests/src/test/java/org/springframework/scheduling/annotation/ScheduledAndTransactionalAnnotationIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/scheduling/annotation/ScheduledAndTransactionalAnnotationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -60,8 +60,8 @@ void failsWhenJdkProxyAndScheduledMethodNotPresentOnInterface() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(Config.class, JdkProxyTxConfig.class, RepoConfigA.class); assertThatExceptionOfType(BeanCreationException.class) - .isThrownBy(ctx::refresh) - .withCauseInstanceOf(IllegalStateException.class); + .isThrownBy(ctx::refresh) + .withCauseInstanceOf(IllegalStateException.class); } @Test @@ -70,7 +70,7 @@ void succeedsWhenSubclassProxyAndScheduledMethodNotPresentOnInterface() throws I ctx.register(Config.class, SubclassProxyTxConfig.class, RepoConfigA.class); ctx.refresh(); - Thread.sleep(100); // allow @Scheduled method to be called several times + Thread.sleep(200); // allow @Scheduled method to be called several times MyRepository repository = ctx.getBean(MyRepository.class); CallCountingTransactionManager txManager = ctx.getBean(CallCountingTransactionManager.class); @@ -85,7 +85,7 @@ void succeedsWhenJdkProxyAndScheduledMethodIsPresentOnInterface() throws Interru ctx.register(Config.class, JdkProxyTxConfig.class, RepoConfigB.class); ctx.refresh(); - Thread.sleep(100); // allow @Scheduled method to be called several times + Thread.sleep(200); // allow @Scheduled method to be called several times MyRepositoryWithScheduledMethod repository = ctx.getBean(MyRepositoryWithScheduledMethod.class); CallCountingTransactionManager txManager = ctx.getBean(CallCountingTransactionManager.class); @@ -100,7 +100,7 @@ void withAspectConfig() throws InterruptedException { ctx.register(AspectConfig.class, MyRepositoryWithScheduledMethodImpl.class); ctx.refresh(); - Thread.sleep(100); // allow @Scheduled method to be called several times + Thread.sleep(200); // allow @Scheduled method to be called several times MyRepositoryWithScheduledMethod repository = ctx.getBean(MyRepositoryWithScheduledMethod.class); assertThat(AopUtils.isCglibProxy(repository)).isTrue(); diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptorTests.java index 40b038b457ef..d1bfc13af5d5 100644 --- a/spring-aop/src/test/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -77,9 +77,7 @@ public void testCorrectHandlerUsed() throws Throwable { given(mi.getMethod()).willReturn(Object.class.getMethod("hashCode")); given(mi.getThis()).willReturn(new Object()); given(mi.proceed()).willThrow(ex); - assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(() -> - ti.invoke(mi)) - .isSameAs(ex); + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(() -> ti.invoke(mi)).isSameAs(ex); assertThat(th.getCalls()).isEqualTo(1); assertThat(th.getCalls("ioException")).isEqualTo(1); } @@ -92,9 +90,7 @@ public void testCorrectHandlerUsedForSubclass() throws Throwable { ConnectException ex = new ConnectException(""); MethodInvocation mi = mock(MethodInvocation.class); given(mi.proceed()).willThrow(ex); - assertThatExceptionOfType(ConnectException.class).isThrownBy(() -> - ti.invoke(mi)) - .isSameAs(ex); + assertThatExceptionOfType(ConnectException.class).isThrownBy(() -> ti.invoke(mi)).isSameAs(ex); assertThat(th.getCalls()).isEqualTo(1); assertThat(th.getCalls("remoteException")).isEqualTo(1); } @@ -117,9 +113,7 @@ public void afterThrowing(RemoteException ex) throws Throwable { ConnectException ex = new ConnectException(""); MethodInvocation mi = mock(MethodInvocation.class); given(mi.proceed()).willThrow(ex); - assertThatExceptionOfType(Throwable.class).isThrownBy(() -> - ti.invoke(mi)) - .isSameAs(t); + assertThatExceptionOfType(Throwable.class).isThrownBy(() -> ti.invoke(mi)).isSameAs(t); assertThat(th.getCalls()).isEqualTo(1); assertThat(th.getCalls("remoteException")).isEqualTo(1); } 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 9d461d2e400f..597633475112 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-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -114,7 +114,7 @@ void schedulerWithTaskExecutor() throws Exception { trigger.setName("myTrigger"); trigger.setJobDetail(jobDetail); trigger.setStartDelay(1); - trigger.setRepeatInterval(500); + trigger.setRepeatInterval(100); trigger.setRepeatCount(1); trigger.afterPropertiesSet(); @@ -126,14 +126,14 @@ void schedulerWithTaskExecutor() throws Exception { bean.start(); Thread.sleep(500); - assertThat(DummyJob.count > 0).as("DummyJob should have been executed at least once.").isTrue(); + assertThat(DummyJob.count).as("DummyJob should have been executed at least once.").isGreaterThan(0); assertThat(taskExecutor.count).isEqualTo(DummyJob.count); bean.destroy(); } @Test - @SuppressWarnings({ "unchecked", "rawtypes" }) + @SuppressWarnings({"unchecked", "rawtypes"}) void jobDetailWithRunnableInsteadOfJob() { JobDetailImpl jobDetail = new JobDetailImpl(); assertThatIllegalArgumentException().isThrownBy(() -> @@ -156,7 +156,7 @@ void schedulerWithQuartzJobBean() throws Exception { trigger.setName("myTrigger"); trigger.setJobDetail(jobDetail); trigger.setStartDelay(1); - trigger.setRepeatInterval(500); + trigger.setRepeatInterval(100); trigger.setRepeatCount(1); trigger.afterPropertiesSet(); @@ -168,7 +168,7 @@ void schedulerWithQuartzJobBean() throws Exception { Thread.sleep(500); assertThat(DummyJobBean.param).isEqualTo(10); - assertThat(DummyJobBean.count > 0).isTrue(); + assertThat(DummyJobBean.count).isGreaterThan(0); bean.destroy(); } @@ -190,7 +190,7 @@ void schedulerWithSpringBeanJobFactory() throws Exception { trigger.setName("myTrigger"); trigger.setJobDetail(jobDetail); trigger.setStartDelay(1); - trigger.setRepeatInterval(500); + trigger.setRepeatInterval(100); trigger.setRepeatCount(1); trigger.afterPropertiesSet(); @@ -203,7 +203,7 @@ void schedulerWithSpringBeanJobFactory() throws Exception { Thread.sleep(500); assertThat(DummyJob.param).isEqualTo(10); - assertThat(DummyJob.count > 0).as("DummyJob should have been executed at least once.").isTrue(); + assertThat(DummyJob.count).as("DummyJob should have been executed at least once.").isGreaterThan(0); bean.destroy(); } @@ -225,7 +225,7 @@ void schedulerWithSpringBeanJobFactoryAndParamMismatchNotIgnored() throws Except trigger.setName("myTrigger"); trigger.setJobDetail(jobDetail); trigger.setStartDelay(1); - trigger.setRepeatInterval(500); + trigger.setRepeatInterval(100); trigger.setRepeatCount(1); trigger.afterPropertiesSet(); @@ -239,7 +239,7 @@ void schedulerWithSpringBeanJobFactoryAndParamMismatchNotIgnored() throws Except Thread.sleep(500); assertThat(DummyJob.param).isEqualTo(0); - assertThat(DummyJob.count == 0).isTrue(); + assertThat(DummyJob.count).isEqualTo(0); bean.destroy(); } @@ -260,7 +260,7 @@ void schedulerWithSpringBeanJobFactoryAndQuartzJobBean() throws Exception { trigger.setName("myTrigger"); trigger.setJobDetail(jobDetail); trigger.setStartDelay(1); - trigger.setRepeatInterval(500); + trigger.setRepeatInterval(100); trigger.setRepeatCount(1); trigger.afterPropertiesSet(); @@ -273,7 +273,7 @@ void schedulerWithSpringBeanJobFactoryAndQuartzJobBean() throws Exception { Thread.sleep(500); assertThat(DummyJobBean.param).isEqualTo(10); - assertThat(DummyJobBean.count > 0).isTrue(); + assertThat(DummyJobBean.count).isGreaterThan(0); bean.destroy(); } @@ -292,7 +292,7 @@ void schedulerWithSpringBeanJobFactoryAndJobSchedulingData() throws Exception { Thread.sleep(500); assertThat(DummyJob.param).isEqualTo(10); - assertThat(DummyJob.count > 0).as("DummyJob should have been executed at least once.").isTrue(); + assertThat(DummyJob.count).as("DummyJob should have been executed at least once.").isGreaterThan(0); bean.destroy(); } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java index 65cbb9bdb9f2..7f41cd5962ce 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -329,8 +329,8 @@ protected void registerBeanDefinition(BeanDefinitionHolder definitionHolder, Bea * @return {@code true} if the bean can be registered as-is; * {@code false} if it should be skipped because there is an * existing, compatible bean definition for the specified name - * @throws ConflictingBeanDefinitionException if an existing, incompatible - * bean definition has been found for the specified name + * @throws IllegalStateException if an existing, incompatible bean definition + * has been found for the specified name */ protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) throws IllegalStateException { if (!this.registry.containsBeanDefinition(beanName)) { @@ -354,16 +354,16 @@ protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) * the given existing bean definition. *

    The default implementation considers them as compatible when the existing * bean definition comes from the same source or from a non-scanning source. - * @param newDefinition the new bean definition, originated from scanning - * @param existingDefinition the existing bean definition, potentially an + * @param newDef the new bean definition, originated from scanning + * @param existingDef the existing bean definition, potentially an * explicitly defined one or a previously generated one from scanning * @return whether the definitions are considered as compatible, with the * new definition to be skipped in favor of the existing definition */ - protected boolean isCompatible(BeanDefinition newDefinition, BeanDefinition existingDefinition) { - return (!(existingDefinition instanceof ScannedGenericBeanDefinition) || // explicitly registered overriding bean - (newDefinition.getSource() != null && newDefinition.getSource().equals(existingDefinition.getSource())) || // scanned same file twice - newDefinition.equals(existingDefinition)); // scanned equivalent class twice + protected boolean isCompatible(BeanDefinition newDef, BeanDefinition existingDef) { + return (!(existingDef instanceof ScannedGenericBeanDefinition) || // explicitly registered overriding bean + (newDef.getSource() != null && newDef.getSource().equals(existingDef.getSource())) || // scanned same file twice + newDef.equals(existingDef)); // scanned equivalent class twice } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java index 76b9fc53dd62..617b5579b7a6 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java @@ -197,6 +197,7 @@ public void testSimpleScanWithDefaultFiltersAndOverridingBean() { context.registerBeanDefinition("stubFooDao", new RootBeanDefinition(TestBean.class)); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setIncludeAnnotationConfig(false); + // should not fail! scanner.scan(BASE_PACKAGE); } @@ -207,6 +208,7 @@ public void testSimpleScanWithDefaultFiltersAndDefaultBeanNameClash() { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setIncludeAnnotationConfig(false); scanner.scan("org.springframework.context.annotation3"); + assertThatIllegalStateException().isThrownBy(() -> scanner.scan(BASE_PACKAGE)) .withMessageContaining("stubFooDao") .withMessageContaining(StubFooDao.class.getName()); diff --git a/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java b/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java index 9e56b5d5e37d..c7e74b2ab2bb 100644 --- a/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java +++ b/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,8 +24,6 @@ import java.util.concurrent.CompletionStage; import java.util.function.Function; -import kotlinx.coroutines.CompletableDeferredKt; -import kotlinx.coroutines.Deferred; import org.reactivestreams.Publisher; import reactor.blockhound.BlockHound; import reactor.blockhound.integration.BlockHoundIntegration; @@ -39,13 +37,14 @@ import org.springframework.util.ReflectionUtils; /** - * A registry of adapters to adapt Reactive Streams {@link Publisher} to/from - * various async/reactive types such as {@code CompletableFuture}, RxJava - * {@code Flowable}, and others. + * A registry of adapters to adapt Reactive Streams {@link Publisher} to/from various + * async/reactive types such as {@code CompletableFuture}, RxJava {@code Flowable}, etc. + * This is designed to complement Spring's Reactor {@code Mono}/{@code Flux} support while + * also being usable without Reactor, e.g. just for {@code org.reactivestreams} bridging. * - *

    By default, depending on classpath availability, adapters are registered - * for Reactor, RxJava 3, {@link CompletableFuture}, {@code Flow.Publisher}, - * and Kotlin Coroutines' {@code Deferred} and {@code Flow}. + *

    By default, depending on classpath availability, adapters are registered for Reactor + * (including {@code CompletableFuture} and {@code Flow.Publisher} adapters), RxJava 3, + * Kotlin Coroutines' {@code Deferred} (bridged via Reactor) and SmallRye Mutiny 1.x. * *

    Note: As of Spring Framework 5.3.11, support for * RxJava 1.x and 2.x is deprecated in favor of RxJava 3. @@ -401,9 +400,9 @@ private static class CoroutinesRegistrar { @SuppressWarnings("KotlinInternalInJava") void registerAdapters(ReactiveAdapterRegistry registry) { registry.registerReactiveType( - ReactiveTypeDescriptor.singleOptionalValue(Deferred.class, - () -> CompletableDeferredKt.CompletableDeferred(null)), - source -> CoroutinesUtils.deferredToMono((Deferred) source), + ReactiveTypeDescriptor.singleOptionalValue(kotlinx.coroutines.Deferred.class, + () -> kotlinx.coroutines.CompletableDeferredKt.CompletableDeferred(null)), + source -> CoroutinesUtils.deferredToMono((kotlinx.coroutines.Deferred) source), source -> CoroutinesUtils.monoToDeferred(Mono.from(source))); registry.registerReactiveType( diff --git a/spring-core/src/main/java/org/springframework/core/ReactiveTypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/ReactiveTypeDescriptor.java index f1d76c330e24..5e37afabf097 100644 --- a/spring-core/src/main/java/org/springframework/core/ReactiveTypeDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/ReactiveTypeDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ public final class ReactiveTypeDescriptor { private final boolean noValue; @Nullable - private final Supplier emptyValueSupplier; + private final Supplier emptySupplier; private final boolean deferred; @@ -55,7 +55,7 @@ private ReactiveTypeDescriptor(Class reactiveType, boolean multiValue, boolea this.reactiveType = reactiveType; this.multiValue = multiValue; this.noValue = noValue; - this.emptyValueSupplier = emptySupplier; + this.emptySupplier = emptySupplier; this.deferred = deferred; } @@ -89,16 +89,16 @@ public boolean isNoValue() { * Return {@code true} if the reactive type can complete with no values. */ public boolean supportsEmpty() { - return (this.emptyValueSupplier != null); + return (this.emptySupplier != null); } /** * Return an empty-value instance for the underlying reactive or async type. - * Use of this type implies {@link #supportsEmpty()} is {@code true}. + *

    Use of this type implies {@link #supportsEmpty()} is {@code true}. */ public Object getEmptyValue() { - Assert.state(this.emptyValueSupplier != null, "Empty values not supported"); - return this.emptyValueSupplier.get(); + Assert.state(this.emptySupplier != null, "Empty values not supported"); + return this.emptySupplier.get(); } /** diff --git a/spring-core/src/test/java/org/springframework/core/ReactiveAdapterRegistryTests.java b/spring-core/src/test/java/org/springframework/core/ReactiveAdapterRegistryTests.java index 35683ac0cbee..98d7d0f59d2b 100644 --- a/spring-core/src/test/java/org/springframework/core/ReactiveAdapterRegistryTests.java +++ b/spring-core/src/test/java/org/springframework/core/ReactiveAdapterRegistryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -100,7 +100,7 @@ void toFlux() { List sequence = Arrays.asList(1, 2, 3); Publisher source = io.reactivex.rxjava3.core.Flowable.fromIterable(sequence); Object target = getAdapter(Flux.class).fromPublisher(source); - assertThat(target instanceof Flux).isTrue(); + assertThat(target).isInstanceOf(Flux.class); assertThat(((Flux) target).collectList().block(ONE_SECOND)).isEqualTo(sequence); } @@ -108,7 +108,7 @@ void toFlux() { void toMono() { Publisher source = io.reactivex.rxjava3.core.Flowable.fromArray(1, 2, 3); Object target = getAdapter(Mono.class).fromPublisher(source); - assertThat(target instanceof Mono).isTrue(); + assertThat(target).isInstanceOf(Mono.class); assertThat(((Mono) target).block(ONE_SECOND)).isEqualTo(Integer.valueOf(1)); } @@ -116,7 +116,7 @@ void toMono() { void toCompletableFuture() throws Exception { Publisher source = Flux.fromArray(new Integer[] {1, 2, 3}); Object target = getAdapter(CompletableFuture.class).fromPublisher(source); - assertThat(target instanceof CompletableFuture).isTrue(); + assertThat(target).isInstanceOf(CompletableFuture.class); assertThat(((CompletableFuture) target).get()).isEqualTo(Integer.valueOf(1)); } @@ -125,7 +125,7 @@ void fromCompletableFuture() { CompletableFuture future = new CompletableFuture<>(); future.complete(1); Object target = getAdapter(CompletableFuture.class).toPublisher(future); - assertThat(target instanceof Mono).as("Expected Mono Publisher: " + target.getClass().getName()).isTrue(); + assertThat(target).as("Expected Mono Publisher: " + target.getClass().getName()).isInstanceOf(Mono.class); assertThat(((Mono) target).block(ONE_SECOND)).isEqualTo(Integer.valueOf(1)); } } @@ -294,7 +294,7 @@ void toFlowable() { List sequence = Arrays.asList(1, 2, 3); Publisher source = Flux.fromIterable(sequence); Object target = getAdapter(io.reactivex.rxjava3.core.Flowable.class).fromPublisher(source); - assertThat(target instanceof io.reactivex.rxjava3.core.Flowable).isTrue(); + assertThat(target).isInstanceOf(io.reactivex.rxjava3.core.Flowable.class); assertThat(((io.reactivex.rxjava3.core.Flowable) target).toList().blockingGet()).isEqualTo(sequence); } @@ -303,7 +303,7 @@ void toObservable() { List sequence = Arrays.asList(1, 2, 3); Publisher source = Flux.fromIterable(sequence); Object target = getAdapter(io.reactivex.rxjava3.core.Observable.class).fromPublisher(source); - assertThat(target instanceof io.reactivex.rxjava3.core.Observable).isTrue(); + assertThat(target).isInstanceOf(io.reactivex.rxjava3.core.Observable.class); assertThat(((io.reactivex.rxjava3.core.Observable) target).toList().blockingGet()).isEqualTo(sequence); } @@ -311,7 +311,7 @@ void toObservable() { void toSingle() { Publisher source = Flux.fromArray(new Integer[] {1}); Object target = getAdapter(io.reactivex.rxjava3.core.Single.class).fromPublisher(source); - assertThat(target instanceof io.reactivex.rxjava3.core.Single).isTrue(); + assertThat(target).isInstanceOf(io.reactivex.rxjava3.core.Single.class); assertThat(((io.reactivex.rxjava3.core.Single) target).blockingGet()).isEqualTo(Integer.valueOf(1)); } @@ -319,7 +319,7 @@ void toSingle() { void toCompletable() { Publisher source = Flux.fromArray(new Integer[] {1, 2, 3}); Object target = getAdapter(io.reactivex.rxjava3.core.Completable.class).fromPublisher(source); - assertThat(target instanceof io.reactivex.rxjava3.core.Completable).isTrue(); + assertThat(target).isInstanceOf(io.reactivex.rxjava3.core.Completable.class); ((io.reactivex.rxjava3.core.Completable) target).blockingAwait(); } @@ -328,7 +328,7 @@ void fromFlowable() { List sequence = Arrays.asList(1, 2, 3); Object source = io.reactivex.rxjava3.core.Flowable.fromIterable(sequence); Object target = getAdapter(io.reactivex.rxjava3.core.Flowable.class).toPublisher(source); - assertThat(target instanceof Flux).as("Expected Flux Publisher: " + target.getClass().getName()).isTrue(); + assertThat(target).as("Expected Flux Publisher: " + target.getClass().getName()).isInstanceOf(Flux.class); assertThat(((Flux) target).collectList().block(ONE_SECOND)).isEqualTo(sequence); } @@ -337,7 +337,7 @@ void fromObservable() { List sequence = Arrays.asList(1, 2, 3); Object source = io.reactivex.rxjava3.core.Observable.fromIterable(sequence); Object target = getAdapter(io.reactivex.rxjava3.core.Observable.class).toPublisher(source); - assertThat(target instanceof Flux).as("Expected Flux Publisher: " + target.getClass().getName()).isTrue(); + assertThat(target).as("Expected Flux Publisher: " + target.getClass().getName()).isInstanceOf(Flux.class); assertThat(((Flux) target).collectList().block(ONE_SECOND)).isEqualTo(sequence); } @@ -345,7 +345,7 @@ void fromObservable() { void fromSingle() { Object source = io.reactivex.rxjava3.core.Single.just(1); Object target = getAdapter(io.reactivex.rxjava3.core.Single.class).toPublisher(source); - assertThat(target instanceof Mono).as("Expected Mono Publisher: " + target.getClass().getName()).isTrue(); + assertThat(target).as("Expected Mono Publisher: " + target.getClass().getName()).isInstanceOf(Mono.class); assertThat(((Mono) target).block(ONE_SECOND)).isEqualTo(Integer.valueOf(1)); } @@ -353,7 +353,7 @@ void fromSingle() { void fromCompletable() { Object source = io.reactivex.rxjava3.core.Completable.complete(); Object target = getAdapter(io.reactivex.rxjava3.core.Completable.class).toPublisher(source); - assertThat(target instanceof Mono).as("Expected Mono Publisher: " + target.getClass().getName()).isTrue(); + assertThat(target).as("Expected Mono Publisher: " + target.getClass().getName()).isInstanceOf(Mono.class); ((Mono) target).block(ONE_SECOND); } } diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/R2dbcTransactionManagerUnitTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/R2dbcTransactionManagerUnitTests.java index 9f6cfd890549..1c99636646c8 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/R2dbcTransactionManagerUnitTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/R2dbcTransactionManagerUnitTests.java @@ -24,7 +24,6 @@ import io.r2dbc.spi.R2dbcBadGrammarException; import io.r2dbc.spi.R2dbcTimeoutException; import io.r2dbc.spi.Statement; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; @@ -55,6 +54,7 @@ * Unit tests for {@link R2dbcTransactionManager}. * * @author Mark Paluch + * @author Juergen Hoeller */ class R2dbcTransactionManagerUnitTests { @@ -85,8 +85,7 @@ void testSimpleTransaction() { ConnectionFactoryUtils.getConnection(connectionFactoryMock) .flatMap(connection -> TransactionSynchronizationManager.forCurrentTransaction() - .doOnNext(synchronizationManager -> synchronizationManager.registerSynchronization( - sync))) + .doOnNext(synchronizationManager -> synchronizationManager.registerSynchronization(sync))) .as(operator::transactional) .as(StepVerifier::create) .expectNextCount(1) @@ -118,12 +117,11 @@ void testBeginFails() { TransactionalOperator operator = TransactionalOperator.create(tm, definition); - ConnectionFactoryUtils.getConnection(connectionFactoryMock).as( - operator::transactional) + ConnectionFactoryUtils.getConnection(connectionFactoryMock) + .as(operator::transactional) .as(StepVerifier::create) .expectErrorSatisfies(actual -> assertThat(actual).isInstanceOf( - CannotCreateTransactionException.class).hasCauseInstanceOf( - R2dbcBadGrammarException.class)) + CannotCreateTransactionException.class).hasCauseInstanceOf(R2dbcBadGrammarException.class)) .verify(); } @@ -139,8 +137,8 @@ void appliesIsolationLevel() { TransactionalOperator operator = TransactionalOperator.create(tm, definition); - ConnectionFactoryUtils.getConnection(connectionFactoryMock).as( - operator::transactional) + ConnectionFactoryUtils.getConnection(connectionFactoryMock) + .as(operator::transactional) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); @@ -164,8 +162,8 @@ void doesNotSetIsolationLevelIfMatch() { TransactionalOperator operator = TransactionalOperator.create(tm, definition); - ConnectionFactoryUtils.getConnection(connectionFactoryMock).as( - operator::transactional) + ConnectionFactoryUtils.getConnection(connectionFactoryMock) + .as(operator::transactional) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); @@ -184,8 +182,8 @@ void doesNotSetAutoCommitDisabled() { TransactionalOperator operator = TransactionalOperator.create(tm, definition); - ConnectionFactoryUtils.getConnection(connectionFactoryMock).as( - operator::transactional) + ConnectionFactoryUtils.getConnection(connectionFactoryMock) + .as(operator::transactional) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); @@ -232,8 +230,8 @@ void appliesReadOnly() { TransactionalOperator operator = TransactionalOperator.create(tm, definition); - ConnectionFactoryUtils.getConnection(connectionFactoryMock).as( - operator::transactional) + ConnectionFactoryUtils.getConnection(connectionFactoryMock) + .as(operator::transactional) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); @@ -249,7 +247,6 @@ void appliesReadOnly() { @Test void testCommitFails() { when(connectionMock.commitTransaction()).thenReturn(Mono.defer(() -> Mono.error(new R2dbcBadGrammarException("Commit should fail")))); - when(connectionMock.rollbackTransaction()).thenReturn(Mono.empty()); TransactionalOperator operator = TransactionalOperator.create(tm); @@ -270,7 +267,6 @@ void testCommitFails() { @Test void testRollback() { - AtomicInteger commits = new AtomicInteger(); when(connectionMock.commitTransaction()).thenReturn( Mono.fromRunnable(commits::incrementAndGet)); @@ -282,11 +278,9 @@ void testRollback() { TransactionalOperator operator = TransactionalOperator.create(tm); ConnectionFactoryUtils.getConnection(connectionFactoryMock) - .doOnNext(connection -> { - throw new IllegalStateException(); - }).as(operator::transactional) - .as(StepVerifier::create) - .verifyError(IllegalStateException.class); + .doOnNext(connection -> { throw new IllegalStateException(); }) + .as(operator::transactional) + .as(StepVerifier::create).verifyError(IllegalStateException.class); assertThat(commits).hasValue(0); assertThat(rollbacks).hasValue(1); @@ -303,15 +297,11 @@ void testRollbackFails() { when(connectionMock.rollbackTransaction()).thenReturn(Mono.defer(() -> Mono.error(new R2dbcBadGrammarException("Commit should fail"))), Mono.empty()); TransactionalOperator operator = TransactionalOperator.create(tm); - operator.execute(reactiveTransaction -> { - reactiveTransaction.setRollbackOnly(); - return ConnectionFactoryUtils.getConnection(connectionFactoryMock) .doOnNext(connection -> connection.createStatement("foo")).then(); - }).as(StepVerifier::create) - .verifyError(IllegalTransactionStateException.class); + }).as(StepVerifier::create).verifyError(IllegalTransactionStateException.class); verify(connectionMock).isAutoCommit(); verify(connectionMock).beginTransaction(); @@ -338,7 +328,7 @@ void testConnectionReleasedWhenRollbackFails() { .doOnNext(connection -> { throw new IllegalStateException("Intentional error to trigger rollback"); }).then()).as(StepVerifier::create) - .verifyErrorSatisfies(e -> Assertions.assertThat(e) + .verifyErrorSatisfies(ex -> assertThat(ex) .isInstanceOf(BadSqlGrammarException.class) .hasCause(new R2dbcBadGrammarException("Rollback should fail")) ); @@ -357,19 +347,15 @@ void testTransactionSetRollbackOnly() { TransactionSynchronization.STATUS_ROLLED_BACK); TransactionalOperator operator = TransactionalOperator.create(tm); - operator.execute(tx -> { - tx.setRollbackOnly(); assertThat(tx.isNewTransaction()).isTrue(); - return TransactionSynchronizationManager.forCurrentTransaction().doOnNext( synchronizationManager -> { assertThat(synchronizationManager.hasResource(connectionFactoryMock)).isTrue(); synchronizationManager.registerSynchronization(sync); }).then(); - }).as(StepVerifier::create) - .verifyComplete(); + }).as(StepVerifier::create).verifyComplete(); verify(connectionMock).isAutoCommit(); verify(connectionMock).beginTransaction(); @@ -389,20 +375,16 @@ void testPropagationNeverWithExistingTransaction() { DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); - TransactionalOperator operator = TransactionalOperator.create(tm, definition); + TransactionalOperator operator = TransactionalOperator.create(tm, definition); operator.execute(tx1 -> { - assertThat(tx1.isNewTransaction()).isTrue(); - definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_NEVER); return operator.execute(tx2 -> { - fail("Should have thrown IllegalTransactionStateException"); return Mono.empty(); }); - }).as(StepVerifier::create) - .verifyError(IllegalTransactionStateException.class); + }).as(StepVerifier::create).verifyError(IllegalTransactionStateException.class); verify(connectionMock).rollbackTransaction(); verify(connectionMock).close(); @@ -414,32 +396,49 @@ void testPropagationSupportsAndRequiresNew() { DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); - TransactionalOperator operator = TransactionalOperator.create(tm, definition); + TransactionalOperator operator = TransactionalOperator.create(tm, definition); operator.execute(tx1 -> { - assertThat(tx1.isNewTransaction()).isFalse(); - DefaultTransactionDefinition innerDef = new DefaultTransactionDefinition(); - innerDef.setPropagationBehavior( - TransactionDefinition.PROPAGATION_REQUIRES_NEW); + innerDef.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); TransactionalOperator inner = TransactionalOperator.create(tm, innerDef); - return inner.execute(tx2 -> { - assertThat(tx2.isNewTransaction()).isTrue(); return Mono.empty(); }); - }).as(StepVerifier::create) - .verifyComplete(); + }).as(StepVerifier::create).verifyComplete(); verify(connectionMock).commitTransaction(); verify(connectionMock).close(); } + @Test + void testPropagationSupportsAndRequiresNewWithRollback() { + when(connectionMock.rollbackTransaction()).thenReturn(Mono.empty()); - private static class TestTransactionSynchronization - implements TransactionSynchronization { + DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); + definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); + + TransactionalOperator operator = TransactionalOperator.create(tm, definition); + operator.execute(tx1 -> { + assertThat(tx1.isNewTransaction()).isFalse(); + DefaultTransactionDefinition innerDef = new DefaultTransactionDefinition(); + innerDef.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + TransactionalOperator inner = TransactionalOperator.create(tm, innerDef); + return inner.execute(tx2 -> { + assertThat(tx2.isNewTransaction()).isTrue(); + tx2.setRollbackOnly(); + return Mono.empty(); + }); + }).as(StepVerifier::create).verifyComplete(); + + verify(connectionMock).rollbackTransaction(); + verify(connectionMock).close(); + } + + + private static class TestTransactionSynchronization implements TransactionSynchronization { private int status; @@ -512,7 +511,6 @@ protected void doAfterCompletion(int status) { this.afterCompletionCalled = true; assertThat(status).isEqualTo(this.status); } - } } diff --git a/spring-tx/src/main/java/org/springframework/transaction/PlatformTransactionManager.java b/spring-tx/src/main/java/org/springframework/transaction/PlatformTransactionManager.java index 6122d0906d4e..ae192da79c18 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/PlatformTransactionManager.java +++ b/spring-tx/src/main/java/org/springframework/transaction/PlatformTransactionManager.java @@ -68,8 +68,7 @@ public interface PlatformTransactionManager extends TransactionManager { * @see TransactionDefinition#getTimeout * @see TransactionDefinition#isReadOnly */ - TransactionStatus getTransaction(@Nullable TransactionDefinition definition) - throws TransactionException; + TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException; /** * Commit the given transaction, with regard to its status. If the transaction diff --git a/spring-tx/src/main/java/org/springframework/transaction/ReactiveTransaction.java b/spring-tx/src/main/java/org/springframework/transaction/ReactiveTransaction.java index 11a33e681b52..2c3a03e37855 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/ReactiveTransaction.java +++ b/spring-tx/src/main/java/org/springframework/transaction/ReactiveTransaction.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ package org.springframework.transaction; /** - * Representation of an ongoing reactive transaction. + * Representation of an ongoing {@link ReactiveTransactionManager} transaction. * This is currently a marker interface extending {@link TransactionExecution} * but may acquire further methods in a future revision. * @@ -30,6 +30,7 @@ * @since 5.2 * @see #setRollbackOnly() * @see ReactiveTransactionManager#getReactiveTransaction + * @see org.springframework.transaction.reactive.TransactionCallback#doInTransaction */ public interface ReactiveTransaction extends TransactionExecution { diff --git a/spring-tx/src/main/java/org/springframework/transaction/TransactionStatus.java b/spring-tx/src/main/java/org/springframework/transaction/TransactionStatus.java index 5968c57fd7bb..d61ed25f618d 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/TransactionStatus.java +++ b/spring-tx/src/main/java/org/springframework/transaction/TransactionStatus.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,8 @@ import java.io.Flushable; /** - * Representation of the status of a transaction. + * Representation of an ongoing {@link PlatformTransactionManager} transaction. + * Extends the common {@link TransactionExecution} interface. * *

    Transactional code can use this to retrieve status information, * and to programmatically request a rollback (instead of throwing diff --git a/spring-tx/src/main/java/org/springframework/transaction/support/SmartTransactionObject.java b/spring-tx/src/main/java/org/springframework/transaction/support/SmartTransactionObject.java index 1bb2bf173173..d774381ccbde 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/support/SmartTransactionObject.java +++ b/spring-tx/src/main/java/org/springframework/transaction/support/SmartTransactionObject.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,13 +23,13 @@ * return an internal rollback-only marker, typically from another * transaction that has participated and marked it as rollback-only. * - *

    Autodetected by DefaultTransactionStatus, to always return a - * current rollbackOnly flag even if not resulting from the current + *

    Autodetected by {@link DefaultTransactionStatus} in order to always + * return a current rollbackOnly flag even if not resulting from the current * TransactionStatus. * * @author Juergen Hoeller * @since 1.1 - * @see DefaultTransactionStatus#isRollbackOnly + * @see DefaultTransactionStatus#isGlobalRollbackOnly() */ public interface SmartTransactionObject extends Flushable { From fc085e8663a0fa654830b8333f8881f429678cda Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 7 Aug 2023 15:07:33 +0200 Subject: [PATCH 23/47] Reinstate Introspector.flushFromCaches() call for JDK ClassInfo cache Closes gh-27781 --- .../beans/CachedIntrospectionResults.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java index 42e9acca27f1..1af0bde135c6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java +++ b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -248,9 +248,22 @@ private static BeanInfo getBeanInfo(Class beanClass) throws IntrospectionExce return beanInfo; } } - return (shouldIntrospectorIgnoreBeaninfoClasses ? + + BeanInfo beanInfo = (shouldIntrospectorIgnoreBeaninfoClasses ? Introspector.getBeanInfo(beanClass, Introspector.IGNORE_ALL_BEANINFO) : Introspector.getBeanInfo(beanClass)); + + // Immediately remove class from Introspector cache to allow for proper garbage + // collection on class loader shutdown; we cache it in CachedIntrospectionResults + // in a GC-friendly manner. This is necessary (again) for the JDK ClassInfo cache. + Class classToFlush = beanClass; + do { + Introspector.flushFromCaches(classToFlush); + classToFlush = classToFlush.getSuperclass(); + } + while (classToFlush != null && classToFlush != Object.class); + + return beanInfo; } From 9931f442e49647c81cccd6019f49ca0142ea917f Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 7 Aug 2023 14:51:58 +0200 Subject: [PATCH 24/47] Polishing (cherry picked from commit 2aae0a4e0cc7fc281408511e28bff023708aabb8) --- .../jdbc/core/BeanPropertyRowMapperTests.java | 6 +-- .../jdbc/core/support/LobSupportTests.java | 51 +++++++++---------- 2 files changed, 26 insertions(+), 31 deletions(-) diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java index 4fc63340bda8..b83219be06f2 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java @@ -50,7 +50,7 @@ class BeanPropertyRowMapperTests extends AbstractRowMapperTests { void overridingDifferentClassDefinedForMapping() { BeanPropertyRowMapper mapper = new BeanPropertyRowMapper(Person.class); assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) - .isThrownBy(() -> mapper.setMappedClass(Long.class)); + .isThrownBy(() -> mapper.setMappedClass(Long.class)); } @Test @@ -104,7 +104,7 @@ void mappingWithUnpopulatedFieldsNotAccepted() throws Exception { BeanPropertyRowMapper mapper = new BeanPropertyRowMapper<>(ExtendedPerson.class, true); Mock mock = new Mock(); assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) - .isThrownBy(() -> mock.getJdbcTemplate().query("select name, age, birth_date, balance from people", mapper)); + .isThrownBy(() -> mock.getJdbcTemplate().query("select name, age, birth_date, balance from people", mapper)); } @Test @@ -112,7 +112,7 @@ void mappingNullValue() throws Exception { BeanPropertyRowMapper mapper = new BeanPropertyRowMapper<>(Person.class); Mock mock = new Mock(MockType.TWO); assertThatExceptionOfType(TypeMismatchException.class) - .isThrownBy(() -> mock.getJdbcTemplate().query(SELECT_NULL_AS_AGE, mapper)); + .isThrownBy(() -> mock.getJdbcTemplate().query(SELECT_NULL_AS_AGE, mapper)); } @Test diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/LobSupportTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/LobSupportTests.java index d0ba6edf8f9c..da07e652edb7 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/LobSupportTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/LobSupportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,6 @@ import org.junit.jupiter.api.Test; -import org.springframework.dao.DataAccessException; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.jdbc.LobRetrievalFailureException; import org.springframework.jdbc.support.lob.LobCreator; @@ -55,11 +54,9 @@ class SetValuesCalled { final SetValuesCalled svc = new SetValuesCalled(); - AbstractLobCreatingPreparedStatementCallback psc = new AbstractLobCreatingPreparedStatementCallback( - handler) { + AbstractLobCreatingPreparedStatementCallback psc = new AbstractLobCreatingPreparedStatementCallback(handler) { @Override - protected void setValues(PreparedStatement ps, LobCreator lobCreator) - throws SQLException, DataAccessException { + protected void setValues(PreparedStatement ps, LobCreator lobCreator) { svc.b = true; } }; @@ -73,46 +70,43 @@ protected void setValues(PreparedStatement ps, LobCreator lobCreator) @Test public void testAbstractLobStreamingResultSetExtractorNoRows() throws SQLException { - ResultSet rset = mock(ResultSet.class); + ResultSet rs = mock(ResultSet.class); AbstractLobStreamingResultSetExtractor lobRse = getResultSetExtractor(false); - assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class).isThrownBy(() -> - lobRse.extractData(rset)); - verify(rset).next(); + assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class) + .isThrownBy(() -> lobRse.extractData(rs)); + verify(rs).next(); } @Test public void testAbstractLobStreamingResultSetExtractorOneRow() throws SQLException { - ResultSet rset = mock(ResultSet.class); - given(rset.next()).willReturn(true, false); + ResultSet rs = mock(ResultSet.class); + given(rs.next()).willReturn(true, false); AbstractLobStreamingResultSetExtractor lobRse = getResultSetExtractor(false); - lobRse.extractData(rset); - verify(rset).clearWarnings(); + lobRse.extractData(rs); + verify(rs).clearWarnings(); } @Test - public void testAbstractLobStreamingResultSetExtractorMultipleRows() - throws SQLException { - ResultSet rset = mock(ResultSet.class); - given(rset.next()).willReturn(true, true, false); + public void testAbstractLobStreamingResultSetExtractorMultipleRows() throws SQLException { + ResultSet rs = mock(ResultSet.class); + given(rs.next()).willReturn(true, true, false); AbstractLobStreamingResultSetExtractor lobRse = getResultSetExtractor(false); - assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class).isThrownBy(() -> - lobRse.extractData(rset)); - verify(rset).clearWarnings(); + assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class) + .isThrownBy(() -> lobRse.extractData(rs)); + verify(rs).clearWarnings(); } @Test - public void testAbstractLobStreamingResultSetExtractorCorrectException() - throws SQLException { - ResultSet rset = mock(ResultSet.class); - given(rset.next()).willReturn(true); + public void testAbstractLobStreamingResultSetExtractorCorrectException() throws SQLException { + ResultSet rs = mock(ResultSet.class); + given(rs.next()).willReturn(true); AbstractLobStreamingResultSetExtractor lobRse = getResultSetExtractor(true); - assertThatExceptionOfType(LobRetrievalFailureException.class).isThrownBy(() -> - lobRse.extractData(rset)); + assertThatExceptionOfType(LobRetrievalFailureException.class) + .isThrownBy(() -> lobRse.extractData(rs)); } private AbstractLobStreamingResultSetExtractor getResultSetExtractor(final boolean ex) { AbstractLobStreamingResultSetExtractor lobRse = new AbstractLobStreamingResultSetExtractor() { - @Override protected void streamData(ResultSet rs) throws SQLException, IOException { if (ex) { @@ -125,4 +119,5 @@ protected void streamData(ResultSet rs) throws SQLException, IOException { }; return lobRse; } + } From 0c275107ea45334ae5806d287f5b4eef55f84bee Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 9 Aug 2023 23:53:40 +0200 Subject: [PATCH 25/47] Cancel without interruption of currently running tasks Leave potential interruption up to scheduler shutdown. Closes gh-31019 (cherry picked from commit 6fc5a782524881d27c0c2f35b425fdecf61f463f) --- .../annotation/ScheduledAnnotationBeanPostProcessor.java | 8 ++++---- .../scheduling/config/ScheduledTaskRegistrar.java | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java index f0aae203468a..db1372b70cba 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -160,7 +160,7 @@ public ScheduledAnnotationBeanPostProcessor() { * @since 5.1 */ public ScheduledAnnotationBeanPostProcessor(ScheduledTaskRegistrar registrar) { - Assert.notNull(registrar, "ScheduledTaskRegistrar is required"); + Assert.notNull(registrar, "ScheduledTaskRegistrar must not be null"); this.registrar = registrar; } @@ -580,7 +580,7 @@ public void postProcessBeforeDestruction(Object bean, String beanName) { } if (tasks != null) { for (ScheduledTask task : tasks) { - task.cancel(); + task.cancel(false); } } } @@ -598,7 +598,7 @@ public void destroy() { Collection> allTasks = this.scheduledTasks.values(); for (Set tasks : allTasks) { for (ScheduledTask task : tasks) { - task.cancel(); + task.cancel(false); } } this.scheduledTasks.clear(); diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java index 3b9cc27aa16c..9f141a33cc51 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,8 +41,8 @@ * Helper bean for registering tasks with a {@link TaskScheduler}, typically using cron * expressions. * - *

    As of Spring 3.1, {@code ScheduledTaskRegistrar} has a more prominent user-facing - * role when used in conjunction with the {@link + *

    {@code ScheduledTaskRegistrar} has a more prominent user-facing role when used in + * conjunction with the {@link * org.springframework.scheduling.annotation.EnableAsync @EnableAsync} annotation and its * {@link org.springframework.scheduling.annotation.SchedulingConfigurer * SchedulingConfigurer} callback interface. @@ -552,7 +552,7 @@ public Set getScheduledTasks() { @Override public void destroy() { for (ScheduledTask task : this.scheduledTasks) { - task.cancel(); + task.cancel(false); } if (this.localExecutor != null) { this.localExecutor.shutdownNow(); From bb46b31925f7a800e1453be7bbf2116d6fb2419c Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sat, 12 Aug 2023 11:19:21 +0200 Subject: [PATCH 26/47] Find TransactionalEventListener annotation on target method Closes gh-31034 (cherry picked from commit 6fc4898a1b5d663c6948d6c66291b34de134a246) --- ...ionalApplicationListenerMethodAdapter.java | 12 ++++---- .../TransactionalEventListenerTests.java | 30 +++++++++++++++++++ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java index ce39e136c1a0..7811aeda7f48 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java +++ b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,13 +63,13 @@ public class TransactionalApplicationListenerMethodAdapter extends ApplicationLi */ public TransactionalApplicationListenerMethodAdapter(String beanName, Class targetClass, Method method) { super(beanName, targetClass, method); - TransactionalEventListener ann = - AnnotatedElementUtils.findMergedAnnotation(method, TransactionalEventListener.class); - if (ann == null) { + TransactionalEventListener eventAnn = + AnnotatedElementUtils.findMergedAnnotation(getTargetMethod(), TransactionalEventListener.class); + if (eventAnn == null) { throw new IllegalStateException("No TransactionalEventListener annotation found on method: " + method); } - this.annotation = ann; - this.transactionPhase = ann.phase(); + this.annotation = eventAnn; + this.transactionPhase = eventAnn.phase(); } diff --git a/spring-tx/src/test/java/org/springframework/transaction/event/TransactionalEventListenerTests.java b/spring-tx/src/test/java/org/springframework/transaction/event/TransactionalEventListenerTests.java index ff215f05133a..a8091e0ce762 100644 --- a/spring-tx/src/test/java/org/springframework/transaction/event/TransactionalEventListenerTests.java +++ b/spring-tx/src/test/java/org/springframework/transaction/event/TransactionalEventListenerTests.java @@ -156,6 +156,17 @@ public void afterCommitWithTransactionalComponentListenerProxiedViaDynamicProxy( getEventCollector().assertNoEventReceived(); } + @Test + public void afterCommitWithTransactionalComponentListenerWithInterfaceProxy() { + load(TransactionalComponentTestListenerWithInterface.class); + this.transactionTemplate.execute(status -> { + getContext().publishEvent("SKIP"); + getEventCollector().assertNoEventReceived(); + return null; + }); + getEventCollector().assertNoEventReceived(); + } + @Test public void afterRollback() { load(AfterCompletionExplicitTestListener.class); @@ -525,6 +536,25 @@ public void handleAfterCommit(String data) { } + interface TransactionalComponentTestInterface { + + void handleAfterCommit(String data); + } + + + @Transactional + @Component + static class TransactionalComponentTestListenerWithInterface extends BaseTransactionalTestListener implements + TransactionalComponentTestInterface { + + @TransactionalEventListener(condition = "!'SKIP'.equals(#data)") + @Override + public void handleAfterCommit(String data) { + handleEvent(EventCollector.AFTER_COMMIT, data); + } + } + + @Component static class BeforeCommitTestListener extends BaseTransactionalTestListener { From 2b48254268fd386c51a58e17da97b4e50310485f Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sat, 12 Aug 2023 11:34:25 +0200 Subject: [PATCH 27/47] Use extracted attributes instead of annotation access See gh-31034 (cherry picked from commit d781f299c0a38ed9a29001920e811165e2054773) --- .../TransactionalApplicationListenerMethodAdapter.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java index 7811aeda7f48..b12aff8e17b0 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java +++ b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java @@ -48,10 +48,10 @@ public class TransactionalApplicationListenerMethodAdapter extends ApplicationListenerMethodAdapter implements TransactionalApplicationListener { - private final TransactionalEventListener annotation; - private final TransactionPhase transactionPhase; + private final boolean fallbackExecution; + private final List callbacks = new CopyOnWriteArrayList<>(); @@ -68,8 +68,8 @@ public TransactionalApplicationListenerMethodAdapter(String beanName, Class t if (eventAnn == null) { throw new IllegalStateException("No TransactionalEventListener annotation found on method: " + method); } - this.annotation = eventAnn; this.transactionPhase = eventAnn.phase(); + this.fallbackExecution = eventAnn.fallbackExecution(); } @@ -92,8 +92,8 @@ public void onApplicationEvent(ApplicationEvent event) { TransactionSynchronizationManager.registerSynchronization( new TransactionalApplicationListenerSynchronization<>(event, this, this.callbacks)); } - else if (this.annotation.fallbackExecution()) { - if (this.annotation.phase() == TransactionPhase.AFTER_ROLLBACK && logger.isWarnEnabled()) { + else if (this.fallbackExecution) { + if (getTransactionPhase() == TransactionPhase.AFTER_ROLLBACK && logger.isWarnEnabled()) { logger.warn("Processing " + event + " as a fallback execution on AFTER_ROLLBACK phase"); } processEvent(event); From f7d4bd176efc2d74d3c25c4e3a4b239154340801 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sat, 12 Aug 2023 14:59:32 +0200 Subject: [PATCH 28/47] Explicit note on connection pool deadlock with REQUIRES_NEW Closes gh-26250 --- src/docs/asciidoc/data-access.adoc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/docs/asciidoc/data-access.adoc b/src/docs/asciidoc/data-access.adoc index 46d97b22e909..7dbe197d6466 100644 --- a/src/docs/asciidoc/data-access.adoc +++ b/src/docs/asciidoc/data-access.adoc @@ -1993,6 +1993,14 @@ status and with an inner transaction's locks released immediately after its comp Such an independent inner transaction can also declare its own isolation level, timeout, and read-only settings and not inherit an outer transaction's characteristics. +NOTE: The resources attached to the outer transaction will remain bound there while +the inner transaction acquires its own resources such as a new database connection. +This may lead to exhaustion of the connection pool and potentially to a deadlock if +several threads have an active outer transaction and wait to acquire a new connection +for their inner transaction, with the pool not being able to hand out any such inner +connection anymore. Do not use `PROPAGATION_REQUIRES_NEW` unless your connection pool +is appropriately sized, exceeding the number of concurrent threads by at least 1. + [[tx-propagation-nested]] ===== Understanding `PROPAGATION_NESTED` From b9be40ccd24a367bd2df69379a8318b6822bce1e Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 14 Aug 2023 15:14:22 +0200 Subject: [PATCH 29/47] Add registerReactiveTypeOverride method to ReactiveAdapterRegistry Closes gh-31047 (cherry picked from commit 389238f6229eccfd9b8a56e580762c521990bc44) --- .../core/ReactiveAdapterRegistry.java | 41 ++++++++++++++++--- .../core/ReactiveAdapterRegistryTests.java | 31 ++++++++++++++ 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java b/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java index c7e74b2ab2bb..28a1152b5876 100644 --- a/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java +++ b/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java @@ -135,16 +135,45 @@ public boolean hasAdapters() { * Register a reactive type along with functions to adapt to and from a * Reactive Streams {@link Publisher}. The function arguments assume that * their input is neither {@code null} nor {@link Optional}. + *

    This variant registers the new adapter after existing adapters. + * It will be matched for the exact reactive type if no earlier adapter was + * registered for the specific type, and it will be matched for assignability + * in a second pass if no earlier adapter had an assignable type before. + * @see #registerReactiveTypeOverride + * @see #getAdapter */ public void registerReactiveType(ReactiveTypeDescriptor descriptor, Function> toAdapter, Function, Object> fromAdapter) { - if (reactorPresent) { - this.adapters.add(new ReactorAdapter(descriptor, toAdapter, fromAdapter)); - } - else { - this.adapters.add(new ReactiveAdapter(descriptor, toAdapter, fromAdapter)); - } + this.adapters.add(buildAdapter(descriptor, toAdapter, fromAdapter)); + } + + /** + * Register a reactive type along with functions to adapt to and from a + * Reactive Streams {@link Publisher}. The function arguments assume that + * their input is neither {@code null} nor {@link Optional}. + *

    This variant registers the new adapter first, effectively overriding + * any previously registered adapters for the same reactive type. This allows + * for overriding existing adapters, in particular default adapters. + *

    Note that existing adapters for specific types will still match before + * an assignability match with the new adapter. In order to override all + * existing matches, a new reactive type adapter needs to be registered + * for every specific type, not relying on subtype assignability matches. + * @since 5.3.30 + * @see #registerReactiveType + * @see #getAdapter + */ + public void registerReactiveTypeOverride(ReactiveTypeDescriptor descriptor, + Function> toAdapter, Function, Object> fromAdapter) { + + this.adapters.add(0, buildAdapter(descriptor, toAdapter, fromAdapter)); + } + + private ReactiveAdapter buildAdapter(ReactiveTypeDescriptor descriptor, + Function> toAdapter, Function, Object> fromAdapter) { + + return (reactorPresent ? new ReactorAdapter(descriptor, toAdapter, fromAdapter) : + new ReactiveAdapter(descriptor, toAdapter, fromAdapter)); } /** diff --git a/spring-core/src/test/java/org/springframework/core/ReactiveAdapterRegistryTests.java b/spring-core/src/test/java/org/springframework/core/ReactiveAdapterRegistryTests.java index 98d7d0f59d2b..8d4728b2e69b 100644 --- a/spring-core/src/test/java/org/springframework/core/ReactiveAdapterRegistryTests.java +++ b/spring-core/src/test/java/org/springframework/core/ReactiveAdapterRegistryTests.java @@ -37,6 +37,7 @@ * Unit tests for {@link ReactiveAdapterRegistry}. * * @author Rossen Stoyanchev + * @author Juergen Hoeller */ @SuppressWarnings("unchecked") class ReactiveAdapterRegistryTests { @@ -52,14 +53,40 @@ void getAdapterForReactiveSubType() { ReactiveAdapter adapter2 = getAdapter(ExtendedFlux.class); assertThat(adapter2).isSameAs(adapter1); + // Register regular reactive type (after existing adapters) this.registry.registerReactiveType( ReactiveTypeDescriptor.multiValue(ExtendedFlux.class, ExtendedFlux::empty), o -> (ExtendedFlux) o, ExtendedFlux::from); + // Matches for ExtendedFlux itself ReactiveAdapter adapter3 = getAdapter(ExtendedFlux.class); assertThat(adapter3).isNotNull(); assertThat(adapter3).isNotSameAs(adapter1); + + // Does not match for ExtendedFlux subclass since the default Flux adapter + // is being assignability-checked first when no specific match was found + ReactiveAdapter adapter4 = getAdapter(ExtendedExtendedFlux.class); + assertThat(adapter4).isSameAs(adapter1); + + // Register reactive type override (before existing adapters) + this.registry.registerReactiveTypeOverride( + ReactiveTypeDescriptor.multiValue(Flux.class, ExtendedFlux::empty), + o -> (ExtendedFlux) o, + ExtendedFlux::from); + + // Override match for Flux + ReactiveAdapter adapter5 = getAdapter(Flux.class); + assertThat(adapter5).isNotNull(); + assertThat(adapter5).isNotSameAs(adapter1); + + // Initially registered adapter specifically matches for ExtendedFlux + ReactiveAdapter adapter6 = getAdapter(ExtendedFlux.class); + assertThat(adapter6).isSameAs(adapter3); + + // Override match for ExtendedFlux subclass + ReactiveAdapter adapter7 = getAdapter(ExtendedExtendedFlux.class); + assertThat(adapter7).isSameAs(adapter5); } @@ -79,6 +106,10 @@ public void subscribe(CoreSubscriber actual) { } + private static class ExtendedExtendedFlux extends ExtendedFlux { + } + + @Nested class Reactor { From 3da7a35a9185f1741e873f4871e560a4692a2970 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 14 Aug 2023 19:28:12 +0200 Subject: [PATCH 30/47] Test factory-bean/method placeholders as well See gh-20189 (cherry picked from commit 8b3ddeed056c743ab8e5845a37cbfb06a775d1a5) --- .../FactoryBeanTests-withAutowiring.xml | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/FactoryBeanTests-withAutowiring.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/FactoryBeanTests-withAutowiring.xml index 90ce2158a93a..1d7d5cdedcf5 100644 --- a/spring-beans/src/test/resources/org/springframework/beans/factory/FactoryBeanTests-withAutowiring.xml +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/FactoryBeanTests-withAutowiring.xml @@ -4,25 +4,27 @@ - + - - - + + + - + - - - - yourName - - - + + + + yourName + betaFactory + getGamma + + + From 4326c5322267ab47191a0b8ee418574c2a2b0011 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 14 Aug 2023 19:28:19 +0200 Subject: [PATCH 31/47] Polishing (cherry picked from commit 2ce75dc415c0c6d1a91b5d7d3fa37b5fb01c92f1) --- .../org/springframework/jdbc/core/BeanPropertyRowMapper.java | 2 +- .../annotation/support/PayloadMethodArgumentResolver.java | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java index cd51ba286baa..47d4f4955f98 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java @@ -343,7 +343,7 @@ public T mapRow(ResultSet rs, int rowNumber) throws SQLException { bw.setPropertyValue(pd.getName(), value); } catch (TypeMismatchException ex) { - if (value == null && this.primitivesDefaultedForNullValue) { + if (value == null && isPrimitivesDefaultedForNullValue()) { if (logger.isDebugEnabled()) { String propertyType = ClassUtils.getQualifiedName(pd.getPropertyType()); logger.debug(String.format( diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PayloadMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PayloadMethodArgumentResolver.java index 2ced8c86cd45..103a3c1b8ec3 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PayloadMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PayloadMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -191,8 +191,7 @@ protected Class resolveTargetClass(MethodParameter parameter, Message mess /** * Validate the payload if applicable. *

    The default implementation checks for {@code @javax.validation.Valid}, - * Spring's {@link Validated}, - * and custom annotations whose name starts with "Valid". + * Spring's {@link Validated}, and custom annotations whose name starts with "Valid". * @param message the currently processed message * @param parameter the method parameter * @param target the target payload object From 5f7a6a0f38cc8c5a9eb3dda9137a0bc4afac7201 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 16 Aug 2023 12:48:06 +0200 Subject: [PATCH 32/47] Align validation metadata handling in PayloadMethodArgumentResolver Reuses ValidationAnnotationUtils which is slightly optimized for the detection of Spring's Validated annotation now, also to the benefit of common web scenarios. Closes gh-21852 (cherry picked from commit c7269feeaae8da24c39fd7deae206d6fd0ce196d) --- .../annotation/ValidationAnnotationUtils.java | 29 ++++++++++++------- .../PayloadMethodArgumentResolver.java | 21 ++++++-------- .../PayloadMethodArgumentResolver.java | 19 ++++++------ 3 files changed, 37 insertions(+), 32 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/validation/annotation/ValidationAnnotationUtils.java b/spring-context/src/main/java/org/springframework/validation/annotation/ValidationAnnotationUtils.java index 196f6fc6c74e..0842b812ba0e 100644 --- a/spring-context/src/main/java/org/springframework/validation/annotation/ValidationAnnotationUtils.java +++ b/spring-context/src/main/java/org/springframework/validation/annotation/ValidationAnnotationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,37 +26,46 @@ * Mainly for internal use within the framework. * * @author Christoph Dreis + * @author Juergen Hoeller * @since 5.3.7 */ public abstract class ValidationAnnotationUtils { private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; + /** * Determine any validation hints by the given annotation. - *

    This implementation checks for {@code @javax.validation.Valid}, - * Spring's {@link org.springframework.validation.annotation.Validated}, - * and custom annotations whose name starts with "Valid". + *

    This implementation checks for Spring's + * {@link org.springframework.validation.annotation.Validated}, + * {@code @javax.validation.Valid}, and custom annotations whose + * name starts with "Valid" which may optionally declare validation + * hints through the "value" attribute. * @param ann the annotation (potentially a validation annotation) * @return the validation hints to apply (possibly an empty array), * or {@code null} if this annotation does not trigger any validation */ @Nullable public static Object[] determineValidationHints(Annotation ann) { + // Direct presence of @Validated ? + if (ann instanceof Validated) { + return ((Validated) ann).value(); + } + // Direct presence of @Valid ? Class annotationType = ann.annotationType(); - String annotationName = annotationType.getName(); - if ("javax.validation.Valid".equals(annotationName)) { + if ("javax.validation.Valid".equals(annotationType.getName())) { return EMPTY_OBJECT_ARRAY; } + // Meta presence of @Validated ? Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); if (validatedAnn != null) { - Object hints = validatedAnn.value(); - return convertValidationHints(hints); + return validatedAnn.value(); } + // Custom validation annotation ? if (annotationType.getSimpleName().startsWith("Valid")) { - Object hints = AnnotationUtils.getValue(ann); - return convertValidationHints(hints); + return convertValidationHints(AnnotationUtils.getValue(ann)); } + // No validation triggered return null; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/PayloadMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/PayloadMethodArgumentResolver.java index 3e3c930f05e5..4912e672d873 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/PayloadMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/PayloadMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,6 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.codec.Decoder; import org.springframework.core.codec.DecodingException; import org.springframework.core.io.buffer.DataBuffer; @@ -55,17 +54,17 @@ import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.SmartValidator; import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; /** * A resolver to extract and decode the payload of a message using a - * {@link Decoder}, where the payload is expected to be a {@link Publisher} of - * {@link DataBuffer DataBuffer}. + * {@link Decoder}, where the payload is expected to be a {@link Publisher} + * of {@link DataBuffer DataBuffer}. * *

    Validation is applied if the method argument is annotated with - * {@code @javax.validation.Valid} or - * {@link org.springframework.validation.annotation.Validated}. Validation - * failure results in an {@link MethodArgumentNotValidException}. + * {@link org.springframework.validation.annotation.Validated} or + * {@code @javax.validation.Valid}. Validation failure results in an + * {@link MethodArgumentNotValidException}. * *

    This resolver should be ordered last if {@link #useDefaultResolution} is * set to {@code true} since in that case it supports all types and does not @@ -287,10 +286,8 @@ private Consumer getValidator(Message message, MethodParameter parame return null; } for (Annotation ann : parameter.getParameterAnnotations()) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { String name = Conventions.getVariableNameForParameter(parameter); return target -> { BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(target, name); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PayloadMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PayloadMethodArgumentResolver.java index 103a3c1b8ec3..3ef7b86c189d 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PayloadMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PayloadMethodArgumentResolver.java @@ -19,7 +19,6 @@ import java.lang.annotation.Annotation; import org.springframework.core.MethodParameter; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.converter.MessageConversionException; @@ -36,12 +35,16 @@ import org.springframework.validation.ObjectError; import org.springframework.validation.SmartValidator; import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; /** * A resolver to extract and convert the payload of a message using a - * {@link MessageConverter}. It also validates the payload using a - * {@link Validator} if the argument is annotated with a Validation annotation. + * {@link MessageConverter}. + * + *

    Validation is applied if the method argument is annotated with + * {@link org.springframework.validation.annotation.Validated} or + * {@code @javax.validation.Valid}. Validation failure results in an + * {@link MethodArgumentNotValidException}. * *

    This {@link HandlerMethodArgumentResolver} should be ordered last as it * supports all types and does not require the {@link Payload} annotation. @@ -190,8 +193,6 @@ protected Class resolveTargetClass(MethodParameter parameter, Message mess /** * Validate the payload if applicable. - *

    The default implementation checks for {@code @javax.validation.Valid}, - * Spring's {@link Validated}, and custom annotations whose name starts with "Valid". * @param message the currently processed message * @param parameter the method parameter * @param target the target payload object @@ -202,10 +203,8 @@ protected void validate(Message message, MethodParameter parameter, Object ta return; } for (Annotation ann : parameter.getParameterAnnotations()) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(target, getParameterName(parameter)); if (!ObjectUtils.isEmpty(validationHints) && this.validator instanceof SmartValidator) { From df066d81909766481d74e8c1e637ae4285dc9b22 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 16 Aug 2023 13:03:23 +0200 Subject: [PATCH 33/47] Fix accidental javadoc references to jakarta packages --- .../web/accept/AbstractMappingContentNegotiationStrategy.java | 2 +- .../org/springframework/web/bind/ServletRequestDataBinder.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategy.java b/spring-web/src/main/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategy.java index 62a650d3c4d3..6ccedfd8105a 100644 --- a/spring-web/src/main/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategy.java +++ b/spring-web/src/main/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategy.java @@ -42,7 +42,7 @@ * *

    The method {@link #handleNoMatch} allow subclasses to plug in additional * ways of looking up media types (e.g. through the Java Activation framework, - * or {@link jakarta.servlet.ServletContext#getMimeType}). Media types resolved + * or {@link javax.servlet.ServletContext#getMimeType}). Media types resolved * via base classes are then added to the base class * {@link MappingMediaTypeFileExtensionResolver}, i.e. cached for new lookups. * diff --git a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java index 1c6f0218d2e7..753e0a2caa79 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java @@ -110,7 +110,7 @@ public ServletRequestDataBinder(@Nullable Object target, String objectName) { * @see org.springframework.web.multipart.MultipartHttpServletRequest * @see org.springframework.web.multipart.MultipartRequest * @see org.springframework.web.multipart.MultipartFile - * @see jakarta.servlet.http.Part + * @see javax.servlet.http.Part * @see #bind(org.springframework.beans.PropertyValues) */ public void bind(ServletRequest request) { From 493f75e89255f719c3163fd686ef5244f54cdd18 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 18 Aug 2023 11:40:19 +0200 Subject: [PATCH 34/47] Optimize whitespace checks in StringUtils (as far as possible on JDK 8) Closes gh-31067 --- .../org/springframework/util/StringUtils.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/StringUtils.java b/spring-core/src/main/java/org/springframework/util/StringUtils.java index e803a05db2d2..96290a3cea61 100644 --- a/spring-core/src/main/java/org/springframework/util/StringUtils.java +++ b/spring-core/src/main/java/org/springframework/util/StringUtils.java @@ -247,26 +247,26 @@ public static String trimWhitespace(String str) { /** * Trim all whitespace from the given {@code CharSequence}: * leading, trailing, and in between characters. - * @param text the {@code CharSequence} to check + * @param str the {@code CharSequence} to check * @return the trimmed {@code CharSequence} * @since 5.3.22 * @see #trimAllWhitespace(String) * @see java.lang.Character#isWhitespace */ - public static CharSequence trimAllWhitespace(CharSequence text) { - if (!hasLength(text)) { - return text; + public static CharSequence trimAllWhitespace(CharSequence str) { + if (!hasLength(str)) { + return str; } - int len = text.length(); - StringBuilder sb = new StringBuilder(text.length()); + int len = str.length(); + StringBuilder sb = new StringBuilder(str.length()); for (int i = 0; i < len; i++) { - char c = text.charAt(i); + char c = str.charAt(i); if (!Character.isWhitespace(c)) { sb.append(c); } } - return sb.toString(); + return sb; } /** @@ -278,9 +278,10 @@ public static CharSequence trimAllWhitespace(CharSequence text) { * @see java.lang.Character#isWhitespace */ public static String trimAllWhitespace(String str) { - if (str == null) { - return null; + if (!hasLength(str)) { + return str; } + return trimAllWhitespace((CharSequence) str).toString(); } From 9894174960aa7fc79087b7e271c794101c6735de Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Mon, 21 Aug 2023 17:37:57 +0800 Subject: [PATCH 35/47] Allow overriding dynamic property from enclosing class in nested test class Prior to this commit, a dynamic property registered via a @DynamicPropertySource method in a @Nested test class was not able to override a property registered via a @DynamicPropertySource method in the enclosing class. See gh-26091 Closes gh-31083 --- ...namicPropertiesContextCustomizerFactory.java | 3 ++- .../DynamicPropertySourceNestedTests.java | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactory.java b/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactory.java index 11779cc12339..b0e636dc7e1c 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactory.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactory.java @@ -35,6 +35,7 @@ * * @author Phillip Webb * @author Sam Brannen + * @author Yanming Zhou * @since 5.2.5 * @see DynamicPropertiesContextCustomizer */ @@ -54,10 +55,10 @@ public DynamicPropertiesContextCustomizer createContextCustomizer(Class testC } private void findMethods(Class testClass, Set methods) { - methods.addAll(MethodIntrospector.selectMethods(testClass, this::isAnnotated)); if (TestContextAnnotationUtils.searchEnclosingClass(testClass)) { findMethods(testClass.getEnclosingClass(), methods); } + methods.addAll(MethodIntrospector.selectMethods(testClass, this::isAnnotated)); } private boolean isAnnotated(Method method) { diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/DynamicPropertySourceNestedTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/DynamicPropertySourceNestedTests.java index 08113cee82cd..06412e7eec87 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/DynamicPropertySourceNestedTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/DynamicPropertySourceNestedTests.java @@ -39,6 +39,7 @@ * {@link SpringExtension} in a JUnit Jupiter environment. * * @author Sam Brannen + * @author Yanming Zhou * @since 5.3.2 */ @SpringJUnitConfig @@ -125,6 +126,22 @@ void serviceHasInjectedValues(@Autowired Service service) { } } + @Nested + class DynamicPropertySourceOverrideEnclosingClassTests { + + @DynamicPropertySource + static void overrideDynamicPropertyFromEnclosingClass(DynamicPropertyRegistry registry) { + registry.add(TEST_CONTAINER_PORT, () -> -999); + } + + @Test + @DisplayName("@Service has values injected from @DynamicPropertySource in enclosing class and nested class") + void serviceHasInjectedValues(@Autowired Service service) { + assertThat(service.getIp()).isEqualTo("127.0.0.1"); + assertThat(service.getPort()).isEqualTo(-999); + } + + } static abstract class DynamicPropertySourceSuperclass { From d7ac89ecc99174b8a7ec174f01bd7025e62624e3 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 21 Aug 2023 14:17:05 +0200 Subject: [PATCH 36/47] Revise contribution Beginning with Java 16, inner classes may contain static members. We therefore need to search for @DynamicPropertySource methods in the current class after searching enclosing classes so that a local @DynamicPropertySource method can override properties registered in an enclosing class. However, since Spring Framework 5.3.x is built using Java 8, this commit removes DynamicPropertySourceOverridesEnclosingClassTests since it declares a static method in a @Nested (inner) test class, which results in a compiler error on Java 8. See https://bugs.openjdk.org/browse/JDK-8254321 See gh-31085 --- ...namicPropertiesContextCustomizerFactory.java | 7 ++++++- .../DynamicPropertySourceNestedTests.java | 17 ----------------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactory.java b/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactory.java index b0e636dc7e1c..7f9e3da7798a 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactory.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,6 +55,11 @@ public DynamicPropertiesContextCustomizer createContextCustomizer(Class testC } private void findMethods(Class testClass, Set methods) { + // Beginning with Java 16, inner classes may contain static members. + // We therefore need to search for @DynamicPropertySource methods in the + // current class after searching enclosing classes so that a local + // @DynamicPropertySource method can override properties registered in + // an enclosing class. if (TestContextAnnotationUtils.searchEnclosingClass(testClass)) { findMethods(testClass.getEnclosingClass(), methods); } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/DynamicPropertySourceNestedTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/DynamicPropertySourceNestedTests.java index 06412e7eec87..08113cee82cd 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/DynamicPropertySourceNestedTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/DynamicPropertySourceNestedTests.java @@ -39,7 +39,6 @@ * {@link SpringExtension} in a JUnit Jupiter environment. * * @author Sam Brannen - * @author Yanming Zhou * @since 5.3.2 */ @SpringJUnitConfig @@ -126,22 +125,6 @@ void serviceHasInjectedValues(@Autowired Service service) { } } - @Nested - class DynamicPropertySourceOverrideEnclosingClassTests { - - @DynamicPropertySource - static void overrideDynamicPropertyFromEnclosingClass(DynamicPropertyRegistry registry) { - registry.add(TEST_CONTAINER_PORT, () -> -999); - } - - @Test - @DisplayName("@Service has values injected from @DynamicPropertySource in enclosing class and nested class") - void serviceHasInjectedValues(@Autowired Service service) { - assertThat(service.getIp()).isEqualTo("127.0.0.1"); - assertThat(service.getPort()).isEqualTo(-999); - } - - } static abstract class DynamicPropertySourceSuperclass { From 88c3a788f3616c1da84d9f4e9955b7b24d38bcfc Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 21 Aug 2023 15:12:54 +0200 Subject: [PATCH 37/47] Update copyright headers --- .../cache/caffeine/CaffeineCacheManagerTests.java | 2 +- .../test/java/org/springframework/cache/CacheReproTests.java | 2 +- .../context/annotation/EnableLoadTimeWeavingTests.java | 2 +- .../concurrent/ScheduledExecutorFactoryBeanTests.java | 2 +- .../java/org/springframework/core/SerializableTypeWrapper.java | 2 +- .../convert/support/CollectionToCollectionConverterTests.java | 2 +- .../core/convert/support/MapToMapConverterTests.java | 2 +- .../jms/listener/AbstractJmsListeningContainer.java | 2 +- .../transaction/interceptor/RollbackRuleAttribute.java | 2 +- .../web/accept/AbstractMappingContentNegotiationStrategy.java | 2 +- .../org/springframework/web/bind/ServletRequestDataBinder.java | 2 +- .../web/bind/support/WebExchangeBindException.java | 2 +- .../web/reactive/handler/AbstractUrlHandlerMapping.java | 2 +- .../web/servlet/mvc/condition/PathPatternsRequestCondition.java | 2 +- .../web/servlet/mvc/condition/PatternsRequestCondition.java | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java index 94ffbd245c2e..4c1589e97d15 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java b/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java index 87862aa84401..31e2d02a40d2 100644 --- a/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java +++ b/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/EnableLoadTimeWeavingTests.java b/spring-context/src/test/java/org/springframework/context/annotation/EnableLoadTimeWeavingTests.java index 42ff4229f3c2..95de2db864c6 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/EnableLoadTimeWeavingTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/EnableLoadTimeWeavingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBeanTests.java index 610fd595c435..6be90f35ce6a 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBeanTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java b/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java index 8a4643ea6475..bbd1399d5a7d 100644 --- a/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java +++ b/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-core/src/test/java/org/springframework/core/convert/support/CollectionToCollectionConverterTests.java b/spring-core/src/test/java/org/springframework/core/convert/support/CollectionToCollectionConverterTests.java index 931a59374551..dfcb5e61a436 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/support/CollectionToCollectionConverterTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/support/CollectionToCollectionConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-core/src/test/java/org/springframework/core/convert/support/MapToMapConverterTests.java b/spring-core/src/test/java/org/springframework/core/convert/support/MapToMapConverterTests.java index 9a753dd7eaf0..14542589901e 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/support/MapToMapConverterTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/support/MapToMapConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/AbstractJmsListeningContainer.java b/spring-jms/src/main/java/org/springframework/jms/listener/AbstractJmsListeningContainer.java index 0be44d55b725..21193f3b6401 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/AbstractJmsListeningContainer.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/AbstractJmsListeningContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java index 76754ce77a87..6f5c419aa998 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-web/src/main/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategy.java b/spring-web/src/main/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategy.java index 6ccedfd8105a..22c1d1e3f67f 100644 --- a/spring-web/src/main/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategy.java +++ b/spring-web/src/main/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java index 753e0a2caa79..b2f7b8cd3fcf 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java index d89aac94d1d1..f330ff8b4cb5 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractUrlHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractUrlHandlerMapping.java index 043be1d7f60e..b4e722787caa 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractUrlHandlerMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractUrlHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PathPatternsRequestCondition.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PathPatternsRequestCondition.java index c58f08ecc112..3c1c0e00298a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PathPatternsRequestCondition.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PathPatternsRequestCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java index 5a17b285cad8..335e62cbcb0f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From a4fc7d3c117c40d71046850a56957a229ba48524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Wed, 23 Aug 2023 18:07:54 +0200 Subject: [PATCH 38/47] Optimize ClassUtils#getMostSpecificMethod This commit optimizes ClassUtils#getMostSpecificMethod which is a method frequently invoked in typical Spring applications. It refines ClassUtils#isOverridable by considering static and final modifiers as non overridable and optimizes its implementation. Closes gh-31100 --- .../java/org/springframework/util/ClassUtils.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/ClassUtils.java b/spring-core/src/main/java/org/springframework/util/ClassUtils.java index d5858c7399c7..9669443b56d4 100644 --- a/spring-core/src/main/java/org/springframework/util/ClassUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ClassUtils.java @@ -50,6 +50,7 @@ * @author Keith Donald * @author Rob Harrop * @author Sam Brannen + * @author Sebastien Deleuze * @since 1.1 * @see TypeUtils * @see ReflectionUtils @@ -83,6 +84,12 @@ public abstract class ClassUtils { /** The ".class" file suffix. */ public static final String CLASS_FILE_SUFFIX = ".class"; + /** Precomputed value for the combination of private, static and final modifiers. */ + private static final int NON_OVERRIDABLE_MODIFIER = Modifier.PRIVATE | Modifier.STATIC | Modifier.FINAL; + + /** Precomputed value for the combination of public and protected modifiers. */ + private static final int OVERRIDABLE_MODIFIER = Modifier.PUBLIC | Modifier.PROTECTED; + /** * Map with primitive wrapper type as key and corresponding primitive @@ -1379,10 +1386,10 @@ private static boolean isGroovyObjectMethod(Method method) { * @param targetClass the target class to check against */ private static boolean isOverridable(Method method, @Nullable Class targetClass) { - if (Modifier.isPrivate(method.getModifiers())) { + if ((method.getModifiers() & NON_OVERRIDABLE_MODIFIER) != 0) { return false; } - if (Modifier.isPublic(method.getModifiers()) || Modifier.isProtected(method.getModifiers())) { + if ((method.getModifiers() & OVERRIDABLE_MODIFIER) != 0) { return true; } return (targetClass == null || From afb378a59fb4bbc24af0cacc0e7acb86e170d66c Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 23 Aug 2023 18:50:52 +0200 Subject: [PATCH 39/47] Consistently throw ParseException instead of IllegalStateException Closes gh-31097 --- .../InternalSpelExpressionParser.java | 84 ++++++++++--------- .../spel/standard/SpelParserTests.java | 21 +++-- 2 files changed, 61 insertions(+), 44 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java index 54275a974e4f..cf135d08ed23 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java @@ -77,7 +77,6 @@ import org.springframework.expression.spel.ast.TypeReference; import org.springframework.expression.spel.ast.VariableReference; import org.springframework.lang.Nullable; -import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** @@ -137,12 +136,13 @@ protected SpelExpression doParseExpression(String expressionString, @Nullable Pa this.tokenStreamPointer = 0; this.constructedNodes.clear(); SpelNodeImpl ast = eatExpression(); - Assert.state(ast != null, "No node"); + if (ast == null) { + throw new SpelParseException(this.expressionString, 0, SpelMessage.OOD); + } Token t = peekToken(); if (t != null) { - throw new SpelParseException(t.startPos, SpelMessage.MORE_INPUT, toString(nextToken())); + throw new SpelParseException(this.expressionString, t.startPos, SpelMessage.MORE_INPUT, toString(nextToken())); } - Assert.isTrue(this.constructedNodes.isEmpty(), "At least one node expected"); return new SpelExpression(expressionString, ast, this.configuration); } catch (InternalParseException ex) { @@ -254,20 +254,20 @@ private SpelNodeImpl eatRelationalExpression() { if (tk == TokenKind.EQ) { return new OpEQ(t.startPos, t.endPos, expr, rhExpr); } - Assert.isTrue(tk == TokenKind.NE, "Not-equals token expected"); - return new OpNE(t.startPos, t.endPos, expr, rhExpr); + if (tk == TokenKind.NE) { + return new OpNE(t.startPos, t.endPos, expr, rhExpr); + } } if (tk == TokenKind.INSTANCEOF) { return new OperatorInstanceof(t.startPos, t.endPos, expr, rhExpr); } - if (tk == TokenKind.MATCHES) { return new OperatorMatches(this.patternCache, t.startPos, t.endPos, expr, rhExpr); } - - Assert.isTrue(tk == TokenKind.BETWEEN, "Between token expected"); - return new OperatorBetween(t.startPos, t.endPos, expr, rhExpr); + if (tk == TokenKind.BETWEEN) { + return new OperatorBetween(t.startPos, t.endPos, expr, rhExpr); + } } return expr; } @@ -304,8 +304,7 @@ private SpelNodeImpl eatProductExpression() { else if (t.kind == TokenKind.DIV) { expr = new OpDivide(t.startPos, t.endPos, expr, rhExpr); } - else { - Assert.isTrue(t.kind == TokenKind.MOD, "Mod token expected"); + else if (t.kind == TokenKind.MOD) { expr = new OpModulus(t.startPos, t.endPos, expr, rhExpr); } } @@ -335,18 +334,21 @@ private SpelNodeImpl eatPowerIncDecExpression() { // unaryExpression: (PLUS^ | MINUS^ | BANG^ | INC^ | DEC^) unaryExpression | primaryExpression ; @Nullable private SpelNodeImpl eatUnaryExpression() { - if (peekToken(TokenKind.PLUS, TokenKind.MINUS, TokenKind.NOT)) { + if (peekToken(TokenKind.NOT, TokenKind.PLUS, TokenKind.MINUS)) { Token t = takeToken(); SpelNodeImpl expr = eatUnaryExpression(); - Assert.state(expr != null, "No node"); + if (expr == null) { + throw internalException(t.startPos, SpelMessage.OOD); + } if (t.kind == TokenKind.NOT) { return new OperatorNot(t.startPos, t.endPos, expr); } if (t.kind == TokenKind.PLUS) { return new OpPlus(t.startPos, t.endPos, expr); } - Assert.isTrue(t.kind == TokenKind.MINUS, "Minus token expected"); - return new OpMinus(t.startPos, t.endPos, expr); + if (t.kind == TokenKind.MINUS) { + return new OpMinus(t.startPos, t.endPos, expr); + } } if (peekToken(TokenKind.INC, TokenKind.DEC)) { Token t = takeToken(); @@ -354,7 +356,9 @@ private SpelNodeImpl eatUnaryExpression() { if (t.getKind() == TokenKind.INC) { return new OpInc(t.startPos, t.endPos, false, expr); } - return new OpDec(t.startPos, t.endPos, false, expr); + if (t.kind == TokenKind.DEC) { + return new OpDec(t.startPos, t.endPos, false, expr); + } } return eatPrimaryExpression(); } @@ -414,7 +418,6 @@ private SpelNodeImpl eatDottedNode() { return pop(); } if (peekToken() == null) { - // unexpectedly ran out of data throw internalException(t.startPos, SpelMessage.OOD); } else { @@ -460,8 +463,7 @@ private SpelNodeImpl[] maybeEatMethodArgs() { private void eatConstructorArgs(List accumulatedArguments) { if (!peekToken(TokenKind.LPAREN)) { - throw new InternalParseException(new SpelParseException(this.expressionString, - positionOf(peekToken()), SpelMessage.MISSING_CONSTRUCTOR_ARGS)); + throw internalException(positionOf(peekToken()), SpelMessage.MISSING_CONSTRUCTOR_ARGS); } consumeArguments(accumulatedArguments); eatToken(TokenKind.RPAREN); @@ -472,7 +474,9 @@ private void eatConstructorArgs(List accumulatedArguments) { */ private void consumeArguments(List accumulatedArguments) { Token t = peekToken(); - Assert.state(t != null, "Expected token"); + if (t == null) { + return; + } int pos = t.startPos; Token next; do { @@ -575,8 +579,7 @@ else if (peekToken(TokenKind.LITERAL_STRING)) { private boolean maybeEatTypeReference() { if (peekToken(TokenKind.IDENTIFIER)) { Token typeName = peekToken(); - Assert.state(typeName != null, "Expected token"); - if (!"T".equals(typeName.stringValue())) { + if (typeName == null || !"T".equals(typeName.stringValue())) { return false; } // It looks like a type reference but is T being used as a map key? @@ -605,8 +608,7 @@ private boolean maybeEatTypeReference() { private boolean maybeEatNullReference() { if (peekToken(TokenKind.IDENTIFIER)) { Token nullToken = peekToken(); - Assert.state(nullToken != null, "Expected token"); - if (!"null".equalsIgnoreCase(nullToken.stringValue())) { + if (nullToken == null || !"null".equalsIgnoreCase(nullToken.stringValue())) { return false; } nextToken(); @@ -619,12 +621,13 @@ private boolean maybeEatNullReference() { //projection: PROJECT^ expression RCURLY!; private boolean maybeEatProjection(boolean nullSafeNavigation) { Token t = peekToken(); - if (!peekToken(TokenKind.PROJECT, true)) { + if (t == null || !peekToken(TokenKind.PROJECT, true)) { return false; } - Assert.state(t != null, "No token"); SpelNodeImpl expr = eatExpression(); - Assert.state(expr != null, "No node"); + if (expr == null) { + throw internalException(t.startPos, SpelMessage.OOD); + } eatToken(TokenKind.RSQUARE); this.constructedNodes.push(new Projection(nullSafeNavigation, t.startPos, t.endPos, expr)); return true; @@ -634,15 +637,13 @@ private boolean maybeEatProjection(boolean nullSafeNavigation) { // map = LCURLY (key ':' value (COMMA key ':' value)*) RCURLY private boolean maybeEatInlineListOrMap() { Token t = peekToken(); - if (!peekToken(TokenKind.LCURLY, true)) { + if (t == null || !peekToken(TokenKind.LCURLY, true)) { return false; } - Assert.state(t != null, "No token"); SpelNodeImpl expr = null; Token closingCurly = peekToken(); - if (peekToken(TokenKind.RCURLY, true)) { + if (closingCurly != null && peekToken(TokenKind.RCURLY, true)) { // empty list '{}' - Assert.state(closingCurly != null, "No token"); expr = new InlineList(t.startPos, closingCurly.endPos); } else if (peekToken(TokenKind.COLON, true)) { @@ -695,12 +696,13 @@ else if (peekToken(TokenKind.COLON, true)) { // map! private boolean maybeEatIndexer() { Token t = peekToken(); - if (!peekToken(TokenKind.LSQUARE, true)) { + if (t == null || !peekToken(TokenKind.LSQUARE, true)) { return false; } - Assert.state(t != null, "No token"); SpelNodeImpl expr = eatExpression(); - Assert.state(expr != null, "No node"); + if (expr == null) { + throw internalException(t.startPos, SpelMessage.MISSING_SELECTION_EXPRESSION); + } eatToken(TokenKind.RSQUARE); this.constructedNodes.push(new Indexer(t.startPos, t.endPos, expr)); return true; @@ -708,10 +710,9 @@ private boolean maybeEatIndexer() { private boolean maybeEatSelection(boolean nullSafeNavigation) { Token t = peekToken(); - if (!peekSelectToken()) { + if (t == null || !peekSelectToken()) { return false; } - Assert.state(t != null, "No token"); nextToken(); SpelNodeImpl expr = eatExpression(); if (expr == null) { @@ -889,9 +890,14 @@ else if (t.kind == TokenKind.LITERAL_STRING) { //parenExpr : LPAREN! expression RPAREN!; private boolean maybeEatParenExpression() { if (peekToken(TokenKind.LPAREN)) { - nextToken(); + Token t = nextToken(); + if (t == null) { + return false; + } SpelNodeImpl expr = eatExpression(); - Assert.state(expr != null, "No node"); + if (expr == null) { + throw internalException(t.startPos, SpelMessage.OOD); + } eatToken(TokenKind.RPAREN); push(expr); return true; diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelParserTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelParserTests.java index 33b6e7298e23..bccc0a5b8c2b 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelParserTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelParserTests.java @@ -39,7 +39,9 @@ import static org.springframework.expression.spel.SpelMessage.NON_TERMINATING_QUOTED_STRING; import static org.springframework.expression.spel.SpelMessage.NOT_AN_INTEGER; import static org.springframework.expression.spel.SpelMessage.NOT_A_LONG; +import static org.springframework.expression.spel.SpelMessage.OOD; import static org.springframework.expression.spel.SpelMessage.REAL_CANNOT_BE_LONG; +import static org.springframework.expression.spel.SpelMessage.RIGHT_OPERAND_PROBLEM; import static org.springframework.expression.spel.SpelMessage.RUN_OUT_OF_ARGUMENTS; import static org.springframework.expression.spel.SpelMessage.UNEXPECTED_DATA_AFTER_DOT; import static org.springframework.expression.spel.SpelMessage.UNEXPECTED_ESCAPE_CHAR; @@ -76,8 +78,8 @@ void blankExpressionIsRejected() { private static void assertNullOrEmptyExpressionIsRejected(ThrowingCallable throwingCallable) { assertThatIllegalArgumentException() - .isThrownBy(throwingCallable) - .withMessage("'expressionString' must not be null or blank"); + .isThrownBy(throwingCallable) + .withMessage("'expressionString' must not be null or blank"); } @Test @@ -152,7 +154,13 @@ void parseExceptions() { assertParseException(() -> parser.parseRaw("new String(3"), RUN_OUT_OF_ARGUMENTS, 10); assertParseException(() -> parser.parseRaw("new String("), RUN_OUT_OF_ARGUMENTS, 10); assertParseException(() -> parser.parseRaw("\"abc"), NON_TERMINATING_DOUBLE_QUOTED_STRING, 0); + assertParseException(() -> parser.parseRaw("abc\""), NON_TERMINATING_DOUBLE_QUOTED_STRING, 3); assertParseException(() -> parser.parseRaw("'abc"), NON_TERMINATING_QUOTED_STRING, 0); + assertParseException(() -> parser.parseRaw("abc'"), NON_TERMINATING_QUOTED_STRING, 3); + assertParseException(() -> parser.parseRaw("("), OOD, 0); + assertParseException(() -> parser.parseRaw(")"), OOD, 0); + assertParseException(() -> parser.parseRaw("+"), OOD, 0); + assertParseException(() -> parser.parseRaw("1+"), RIGHT_OPERAND_PROBLEM, 1); } @Test @@ -377,7 +385,7 @@ private void checkNumber(String expression, Object value, Class type) { private void checkNumberError(String expression, SpelMessage expectedMessage) { assertParseExceptionThrownBy(() -> parser.parseRaw(expression)) - .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(expectedMessage)); + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(expectedMessage)); } private static ThrowableAssertAlternative assertParseExceptionThrownBy(ThrowingCallable throwingCallable) { @@ -386,15 +394,18 @@ private static ThrowableAssertAlternative assertParseExcepti private static void assertParseException(ThrowingCallable throwingCallable, SpelMessage expectedMessage, int expectedPosition) { assertParseExceptionThrownBy(throwingCallable) - .satisfies(parseExceptionRequirements(expectedMessage, expectedPosition)); + .satisfies(parseExceptionRequirements(expectedMessage, expectedPosition)); } private static Consumer parseExceptionRequirements( SpelMessage expectedMessage, int expectedPosition) { + return ex -> { assertThat(ex.getMessageCode()).isEqualTo(expectedMessage); assertThat(ex.getPosition()).isEqualTo(expectedPosition); - assertThat(ex.getMessage()).contains(ex.getExpressionString()); + if (ex.getExpressionString() != null) { + assertThat(ex.getMessage()).contains(ex.getExpressionString()); + } }; } From 994bbec0c3ae081b1c81aa5d9335bf5f47964dbf Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 23 Aug 2023 18:52:55 +0200 Subject: [PATCH 40/47] Polishing --- .../org/springframework/util/ClassUtils.java | 2 +- .../expression/spel/standard/Token.java | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/ClassUtils.java b/spring-core/src/main/java/org/springframework/util/ClassUtils.java index 9669443b56d4..4a84db6733b7 100644 --- a/spring-core/src/main/java/org/springframework/util/ClassUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ClassUtils.java @@ -1410,7 +1410,7 @@ public static Method getStaticMethod(Class clazz, String methodName, Class Assert.notNull(methodName, "Method name must not be null"); try { Method method = clazz.getMethod(methodName, args); - return Modifier.isStatic(method.getModifiers()) ? method : null; + return (Modifier.isStatic(method.getModifiers()) ? method : null); } catch (NoSuchMethodException ex) { return null; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/Token.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/Token.java index dda0ff2152c0..f00f26a30037 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/Token.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/Token.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -65,7 +65,7 @@ public boolean isIdentifier() { public boolean isNumericRelationalOperator() { return (this.kind == TokenKind.GT || this.kind == TokenKind.GE || this.kind == TokenKind.LT || - this.kind == TokenKind.LE || this.kind==TokenKind.EQ || this.kind==TokenKind.NE); + this.kind == TokenKind.LE || this.kind == TokenKind.EQ || this.kind == TokenKind.NE); } public String stringValue() { @@ -87,14 +87,14 @@ public Token asBetweenToken() { @Override public String toString() { - StringBuilder s = new StringBuilder(); - s.append('[').append(this.kind.toString()); + StringBuilder sb = new StringBuilder(); + sb.append('[').append(this.kind); if (this.kind.hasPayload()) { - s.append(':').append(this.data); + sb.append(':').append(this.data); } - s.append(']'); - s.append('(').append(this.startPos).append(',').append(this.endPos).append(')'); - return s.toString(); + sb.append(']'); + sb.append('(').append(this.startPos).append(',').append(this.endPos).append(')'); + return sb.toString(); } } From ddcae04ad57ffb2e03f28fa56ff258d5e0e02b1b Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Fri, 8 Sep 2023 16:21:39 +0200 Subject: [PATCH 41/47] Do not invoke [Map|Collection].isEmpty() in nullSafeConciseToString() gh-30811 introduced explicit support for collections and maps in ObjectUtils.nullSafeConciseToString() by invoking isEmpty() on a Map or Collection to determine which concise string representation should be used. However, this caused a regression in which an exception was thrown if the Map or Collection was a proxy generated by AbstractFactoryBean to support , , and in XML configuration. This commit addresses this set of regressions by always returning "[...]" or "{...}" for a Collection or Map, respectively, disregarding whether the map is empty or not. Closes gh-31156 --- .../org/springframework/util/ObjectUtils.java | 20 +++++++++---------- .../util/ObjectUtilsTests.java | 6 +++--- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/ObjectUtils.java b/spring-core/src/main/java/org/springframework/util/ObjectUtils.java index 4475152488e1..64aef0d25a7a 100644 --- a/spring-core/src/main/java/org/springframework/util/ObjectUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ObjectUtils.java @@ -70,8 +70,8 @@ public abstract class ObjectUtils { private static final String ARRAY_ELEMENT_SEPARATOR = ", "; private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; private static final String NON_EMPTY_ARRAY = ARRAY_START + "..." + ARRAY_END; - private static final String EMPTY_COLLECTION = "[]"; - private static final String NON_EMPTY_COLLECTION = "[...]"; + private static final String COLLECTION = "[...]"; + private static final String MAP = NON_EMPTY_ARRAY; /** @@ -938,10 +938,9 @@ public static String nullSafeToString(@Nullable short[] array) { *

  • {@code"Optional[]"} if {@code obj} is a non-empty {@code Optional}, * where {@code } is the result of invoking {@link #nullSafeConciseToString} * on the object contained in the {@code Optional}
  • - *
  • {@code "{}"} if {@code obj} is an empty array or {@link Map}
  • - *
  • {@code "{...}"} if {@code obj} is a non-empty array or {@link Map}
  • - *
  • {@code "[]"} if {@code obj} is an empty {@link Collection}
  • - *
  • {@code "[...]"} if {@code obj} is a non-empty {@link Collection}
  • + *
  • {@code "{}"} if {@code obj} is an empty array
  • + *
  • {@code "{...}"} if {@code obj} is a {@link Map} or a non-empty array
  • + *
  • {@code "[...]"} if {@code obj} is a {@link Collection}
  • *
  • {@linkplain Class#getName() Class name} if {@code obj} is a {@link Class}
  • *
  • {@linkplain Charset#name() Charset name} if {@code obj} is a {@link Charset}
  • *
  • {@linkplain TimeZone#getID() TimeZone ID} if {@code obj} is a {@link TimeZone}
  • @@ -977,12 +976,11 @@ public static String nullSafeConciseToString(@Nullable Object obj) { if (obj.getClass().isArray()) { return (Array.getLength(obj) == 0 ? EMPTY_ARRAY : NON_EMPTY_ARRAY); } - if (obj instanceof Collection) { - return (((Collection) obj).isEmpty() ? EMPTY_COLLECTION : NON_EMPTY_COLLECTION); + if (obj instanceof Collection) { + return COLLECTION; } - if (obj instanceof Map) { - // EMPTY_ARRAY and NON_EMPTY_ARRAY are also used for maps. - return (((Map) obj).isEmpty() ? EMPTY_ARRAY : NON_EMPTY_ARRAY); + if (obj instanceof Map) { + return MAP; } if (obj instanceof Class) { return ((Class) obj).getName(); diff --git a/spring-core/src/test/java/org/springframework/util/ObjectUtilsTests.java b/spring-core/src/test/java/org/springframework/util/ObjectUtilsTests.java index a4d3b8cee39c..a17f0bd10e0f 100644 --- a/spring-core/src/test/java/org/springframework/util/ObjectUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/ObjectUtilsTests.java @@ -1078,8 +1078,8 @@ void nullSafeConciseToStringForNonEmptyArrays() { void nullSafeConciseToStringForEmptyCollections() { List list = Collections.emptyList(); Set set = Collections.emptySet(); - assertThat(ObjectUtils.nullSafeConciseToString(list)).isEqualTo("[]"); - assertThat(ObjectUtils.nullSafeConciseToString(set)).isEqualTo("[]"); + assertThat(ObjectUtils.nullSafeConciseToString(list)).isEqualTo("[...]"); + assertThat(ObjectUtils.nullSafeConciseToString(set)).isEqualTo("[...]"); } @Test @@ -1094,7 +1094,7 @@ void nullSafeConciseToStringForNonEmptyCollections() { @Test void nullSafeConciseToStringForEmptyMaps() { Map map = Collections.emptyMap(); - assertThat(ObjectUtils.nullSafeConciseToString(map)).isEqualTo("{}"); + assertThat(ObjectUtils.nullSafeConciseToString(map)).isEqualTo("{...}"); } @Test From 0c3d8d7a44fa057dd1c8bf62732cd23dc6220303 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 11 Sep 2023 17:36:07 +0200 Subject: [PATCH 42/47] Align abstract method signatures with original Commons Logging API Closes gh-31166 (cherry picked from commit 268043e9c9013b41c44861562bacf09b2f5e3085) --- .../apache/commons/logging/LogFactory.java | 40 +++++++++++-------- .../commons/logging/LogFactoryService.java | 4 ++ 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/LogFactory.java b/spring-jcl/src/main/java/org/apache/commons/logging/LogFactory.java index e67eb5a4342a..34426cc33f48 100644 --- a/spring-jcl/src/main/java/org/apache/commons/logging/LogFactory.java +++ b/spring-jcl/src/main/java/org/apache/commons/logging/LogFactory.java @@ -77,7 +77,25 @@ public static Log getLog(String name) { */ @Deprecated public static LogFactory getFactory() { - return new LogFactory() {}; + return new LogFactory() { + @Override + public Object getAttribute(String name) { + return null; + } + @Override + public String[] getAttributeNames() { + return new String[0]; + } + @Override + public void removeAttribute(String name) { + } + @Override + public void setAttribute(String name, Object value) { + } + @Override + public void release() { + } + }; } /** @@ -106,29 +124,19 @@ public Log getInstance(String name) { // Just in case some code happens to call uncommon Commons Logging methods... @Deprecated - public Object getAttribute(String name) { - return null; - } + public abstract Object getAttribute(String name); @Deprecated - public String[] getAttributeNames() { - return new String[0]; - } + public abstract String[] getAttributeNames(); @Deprecated - public void removeAttribute(String name) { - // do nothing - } + public abstract void removeAttribute(String name); @Deprecated - public void setAttribute(String name, Object value) { - // do nothing - } + public abstract void setAttribute(String name, Object value); @Deprecated - public void release() { - // do nothing - } + public abstract void release(); @Deprecated public static void release(ClassLoader classLoader) { diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/LogFactoryService.java b/spring-jcl/src/main/java/org/apache/commons/logging/LogFactoryService.java index bf2379b5693e..c40a928a0272 100644 --- a/spring-jcl/src/main/java/org/apache/commons/logging/LogFactoryService.java +++ b/spring-jcl/src/main/java/org/apache/commons/logging/LogFactoryService.java @@ -74,4 +74,8 @@ public String[] getAttributeNames() { return this.attributes.keySet().toArray(new String[0]); } + @Override + public void release() { + } + } From 39c225c813f67c9e45dee755c1a297a82f97d1c6 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 11 Sep 2023 17:36:32 +0200 Subject: [PATCH 43/47] AnnotationUtils.clearCache() includes all annotation caches Closes gh-31170 (cherry picked from commit 78fce80c430c6f1e75b060c192ea4986729e68f1) --- .../context/support/AbstractApplicationContext.java | 3 +++ .../springframework/core/annotation/AnnotationUtils.java | 3 +++ .../springframework/core/annotation/AttributeMethods.java | 6 ++---- .../org/springframework/core/annotation/OrderUtils.java | 4 ++-- .../core/annotation/RepeatableContainers.java | 4 ++-- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java index 5d69a6533504..a92455608fc5 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java @@ -1083,6 +1083,9 @@ protected void doClose() { // Let subclasses do some final clean-up if they wish... onClose(); + // Reset common introspection caches to avoid class reference leaks. + resetCommonCaches(); + // Reset local application listeners to pre-refresh state. if (this.earlyApplicationListeners != null) { this.applicationListeners.clear(); diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java index dd51fd45e447..a18c0c3212a8 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java @@ -1321,6 +1321,9 @@ public static boolean isSynthesizedAnnotation(@Nullable Annotation annotation) { public static void clearCache() { AnnotationTypeMappings.clearCache(); AnnotationsScanner.clearCache(); + AttributeMethods.cache.clear(); + RepeatableContainers.cache.clear(); + OrderUtils.orderCache.clear(); } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java b/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java index dc122981aa0d..a828ebe44b5a 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,9 +39,7 @@ final class AttributeMethods { static final AttributeMethods NONE = new AttributeMethods(null, new Method[0]); - - private static final Map, AttributeMethods> cache = - new ConcurrentReferenceHashMap<>(); + static final Map, AttributeMethods> cache = new ConcurrentReferenceHashMap<>(); private static final Comparator methodComparator = (m1, m2) -> { if (m1 != null && m2 != null) { diff --git a/spring-core/src/main/java/org/springframework/core/annotation/OrderUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/OrderUtils.java index 25acbc848161..62a135842b7a 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/OrderUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/OrderUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ public abstract class OrderUtils { private static final String JAVAX_PRIORITY_ANNOTATION = "javax.annotation.Priority"; /** Cache for @Order value (or NOT_ANNOTATED marker) per Class. */ - private static final Map orderCache = new ConcurrentReferenceHashMap<>(64); + static final Map orderCache = new ConcurrentReferenceHashMap<>(64); /** diff --git a/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java b/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java index e163f7d92928..8460018c5bd6 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java @@ -43,6 +43,8 @@ */ public abstract class RepeatableContainers { + static final Map, Object> cache = new ConcurrentReferenceHashMap<>(); + @Nullable private final RepeatableContainers parent; @@ -137,8 +139,6 @@ public static RepeatableContainers none() { */ private static class StandardRepeatableContainers extends RepeatableContainers { - private static final Map, Object> cache = new ConcurrentReferenceHashMap<>(); - private static final Object NONE = new Object(); private static StandardRepeatableContainers INSTANCE = new StandardRepeatableContainers(); From 75faf698afd2dd0f93fe3b03cc896e94085328d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Mon, 11 Sep 2023 15:34:46 +0200 Subject: [PATCH 44/47] Refine CORS documentation for wildcard processing This commit refines CORS wildcard processing Javadoc to provides more details on how wildcards are handled for Access-Control-Allow-Methods, Access-Control-Allow-Headers and Access-Control-Expose-Headers CORS headers. For Access-Control-Expose-Headers, it is not possible to copy the response headers which are not available at the point when the CorsProcessor is invoked. Since all the major browsers seem to support wildcard including on requests with credentials, and since this is ultimately the user-agent responsibility to check on client-side what is authorized or not, Spring Framework continues to support this use case. See gh-31168 --- .../web/bind/annotation/CrossOrigin.java | 27 +++------ .../web/cors/CorsConfiguration.java | 58 +++++++++++++------ .../web/cors/DefaultCorsProcessor.java | 2 +- .../cors/reactive/DefaultCorsProcessor.java | 2 +- .../web/reactive/config/CorsRegistration.java | 28 ++++----- .../config/annotation/CorsRegistration.java | 28 ++++----- 6 files changed, 79 insertions(+), 66 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java index 0a568d25db2b..68fbee19e9b9 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -97,31 +97,23 @@ /** * The list of request headers that are permitted in actual requests, - * possibly {@code "*"} to allow all headers. - *

    Allowed headers are listed in the {@code Access-Control-Allow-Headers} - * response header of preflight requests. - *

    A header name is not required to be listed if it is one of: - * {@code Cache-Control}, {@code Content-Language}, {@code Expires}, - * {@code Last-Modified}, or {@code Pragma} as per the CORS spec. + * possibly {@code "*"} to allow all headers. Please, see + * {@link CorsConfiguration#setAllowedHeaders(List)} for details. *

    By default all requested headers are allowed. */ String[] allowedHeaders() default {}; /** * The List of response headers that the user-agent will allow the client - * to access on an actual response, other than "simple" headers, i.e. - * {@code Cache-Control}, {@code Content-Language}, {@code Content-Type}, - * {@code Expires}, {@code Last-Modified}, or {@code Pragma}, - *

    Exposed headers are listed in the {@code Access-Control-Expose-Headers} - * response header of actual CORS requests. - *

    The special value {@code "*"} allows all headers to be exposed for - * non-credentialed requests. + * to access on an actual response, possibly {@code "*"} to expose all headers. + * Please, see {@link CorsConfiguration#setExposedHeaders(List)} for details. *

    By default no headers are listed as exposed. */ String[] exposedHeaders() default {}; /** - * The list of supported HTTP request methods. + * The list of supported HTTP request methods. Please, see + * {@link CorsConfiguration#setAllowedMethods(List)} for details. *

    By default the supported methods are the same as the ones to which a * controller method is mapped. */ @@ -129,9 +121,8 @@ /** * Whether the browser should send credentials, such as cookies along with - * cross domain requests, to the annotated endpoint. The configured value is - * set on the {@code Access-Control-Allow-Credentials} response header of - * preflight requests. + * cross domain requests, to the annotated endpoint. Please, see + * {@link CorsConfiguration#setAllowCredentials(Boolean)} for details. *

    NOTE: Be aware that this option establishes a high * level of trust with the configured domains and also increases the surface * attack of the web application by exposing sensitive user-specific diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java index f88db838bbb9..f36edf31e0ec 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java +++ b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java @@ -237,8 +237,12 @@ public void addAllowedOriginPattern(@Nullable String originPattern) { /** * Set the HTTP methods to allow, e.g. {@code "GET"}, {@code "POST"}, - * {@code "PUT"}, etc. - *

    The special value {@code "*"} allows all methods. + * {@code "PUT"}, etc. The special value {@code "*"} allows all methods. + *

    {@code Access-Control-Allow-Methods} response header is set either + * to the configured method or to {@code "*"}. Keep in mind however that the + * CORS spec does not allow {@code "*"} when {@link #setAllowCredentials + * allowCredentials} is set to {@code true}, that combination is handled + * by copying the method specified in the CORS preflight request. *

    If not set, only {@code "GET"} and {@code "HEAD"} are allowed. *

    By default this is not set. *

    Note: CORS checks use values from "Forwarded" @@ -269,9 +273,9 @@ public void setAllowedMethods(@Nullable List allowedMethods) { /** * Return the allowed HTTP methods, or {@code null} in which case * only {@code "GET"} and {@code "HEAD"} allowed. + * @see #setAllowedMethods(List) * @see #addAllowedMethod(HttpMethod) * @see #addAllowedMethod(String) - * @see #setAllowedMethods(List) */ @Nullable public List getAllowedMethods() { @@ -279,14 +283,14 @@ public List getAllowedMethods() { } /** - * Add an HTTP method to allow. + * Variant of {@link #setAllowedMethods} for adding one allowed method at a time. */ public void addAllowedMethod(HttpMethod method) { addAllowedMethod(method.name()); } /** - * Add an HTTP method to allow. + * Variant of {@link #setAllowedMethods} for adding one allowed method at a time. */ public void addAllowedMethod(String method) { if (StringUtils.hasText(method)) { @@ -309,9 +313,13 @@ else if (this.resolvedMethods != null) { /** * Set the list of headers that a pre-flight request can list as allowed - * for use during an actual request. - *

    The special value {@code "*"} allows actual requests to send any - * header. + * for use during an actual request. The special value {@code "*"} allows + * actual requests to send any header. + *

    {@code Access-Control-Allow-Headers} response header is set either + * to the configured list of headers or to {@code "*"}. Keep in mind however + * that the CORS spec does not allow {@code "*"} when {@link #setAllowCredentials + * allowCredentials} is set to {@code true}, that combination is handled by + * copying the headers specified in the CORS preflight request. *

    A header name is not required to be listed if it is one of: * {@code Cache-Control}, {@code Content-Language}, {@code Expires}, * {@code Last-Modified}, or {@code Pragma}. @@ -332,7 +340,7 @@ public List getAllowedHeaders() { } /** - * Add an actual request header to allow. + * Variant of {@link #setAllowedHeaders(List)} for adding one allowed header at a time. */ public void addAllowedHeader(String allowedHeader) { if (this.allowedHeaders == null) { @@ -345,12 +353,19 @@ else if (this.allowedHeaders == DEFAULT_PERMIT_ALL) { } /** - * Set the list of response headers other than simple headers (i.e. - * {@code Cache-Control}, {@code Content-Language}, {@code Content-Type}, - * {@code Expires}, {@code Last-Modified}, or {@code Pragma}) that an - * actual response might have and can be exposed. - *

    The special value {@code "*"} allows all headers to be exposed for - * non-credentialed requests. + * Set the list of response headers that an actual response might have + * and can be exposed to the client. The special value {@code "*"} + * allows all headers to be exposed. + *

    {@code Access-Control-Expose-Headers} response header is set either + * to the configured list of headers or to {@code "*"}. While the CORS + * spec does not allow {@code "*"} when {@code Access-Control-Allow-Credentials} + * is set to {@code true}, most browsers support it and + * the response headers are not all available during the CORS processing, + * so as a consequence {@code "*"} is the header value used when specified + * regardless of the value of the `allowCredentials` property. + *

    A header name is not required to be listed if it is one of: + * {@code Cache-Control}, {@code Content-Language}, {@code Expires}, + * {@code Last-Modified}, or {@code Pragma}. *

    By default this is not set. */ public void setExposedHeaders(@Nullable List exposedHeaders) { @@ -368,9 +383,7 @@ public List getExposedHeaders() { } /** - * Add a response header to expose. - *

    The special value {@code "*"} allows all headers to be exposed for - * non-credentialed requests. + * Variant of {@link #setExposedHeaders} for adding one exposed header at a time. */ public void addExposedHeader(String exposedHeader) { if (this.exposedHeaders == null) { @@ -381,6 +394,15 @@ public void addExposedHeader(String exposedHeader) { /** * Whether user credentials are supported. + *

    Setting this property has an impact on how {@link #setAllowedOrigins(List) + * origins}, {@link #setAllowedOriginPatterns(List) originPatterns}, + * {@link #setAllowedMethods(List) allowedMethods} and + * {@link #setAllowedHeaders(List) allowedHeaders} are processed, see related + * API documentation for more details. + *

    NOTE: Be aware that this option establishes a high + * level of trust with the configured domains and also increases the surface + * attack of the web application by exposing sensitive user-specific + * information such as cookies and CSRF tokens. *

    By default this is not set (i.e. user credentials are not supported). */ public void setAllowCredentials(@Nullable Boolean allowCredentials) { diff --git a/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java b/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java index 37d3fb8c9dd9..fcff9e4be4ea 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java @@ -192,7 +192,7 @@ private HttpMethod getMethodToUse(ServerHttpRequest request, boolean isPreFlight /** * Check the headers and determine the headers for the response of a * pre-flight request. The default implementation simply delegates to - * {@link org.springframework.web.cors.CorsConfiguration#checkOrigin(String)}. + * {@link org.springframework.web.cors.CorsConfiguration#checkHeaders(List)}. */ @Nullable protected List checkHeaders(CorsConfiguration config, List requestHeaders) { diff --git a/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java b/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java index 70796c384808..d1e34dcbfb84 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java @@ -190,7 +190,7 @@ private HttpMethod getMethodToUse(ServerHttpRequest request, boolean isPreFlight /** * Check the headers and determine the headers for the response of a * pre-flight request. The default implementation simply delegates to - * {@link CorsConfiguration#checkOrigin(String)}. + * {@link CorsConfiguration#checkHeaders(List)}. */ @Nullable diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java index 327c83ff8177..383505c4c7fa 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,9 +76,11 @@ public CorsRegistration allowedOriginPatterns(String... patterns) { /** * Set the HTTP methods to allow, e.g. {@code "GET"}, {@code "POST"}, etc. - *

    The special value {@code "*"} allows all methods. - *

    By default "simple" methods {@code GET}, {@code HEAD}, and {@code POST} + * The special value {@code "*"} allows all methods. By default, + * "simple" methods {@code GET}, {@code HEAD}, and {@code POST} * are allowed. + *

    Please, see {@link CorsConfiguration#setAllowedMethods(List)} for + * details. */ public CorsRegistration allowedMethods(String... methods) { this.config.setAllowedMethods(Arrays.asList(methods)); @@ -87,11 +89,10 @@ public CorsRegistration allowedMethods(String... methods) { /** * Set the list of headers that a pre-flight request can list as allowed - * for use during an actual request. - *

    The special value {@code "*"} may be used to allow all headers. - *

    A header name is not required to be listed if it is one of: - * {@code Cache-Control}, {@code Content-Language}, {@code Expires}, - * {@code Last-Modified}, or {@code Pragma} as per the CORS spec. + * for use during an actual request. The special value {@code "*"} + * may be used to allow all headers. + *

    Please, see {@link CorsConfiguration#setAllowedHeaders(List)} for + * details. *

    By default all headers are allowed. */ public CorsRegistration allowedHeaders(String... headers) { @@ -100,12 +101,11 @@ public CorsRegistration allowedHeaders(String... headers) { } /** - * Set the list of response headers other than "simple" headers, i.e. - * {@code Cache-Control}, {@code Content-Language}, {@code Content-Type}, - * {@code Expires}, {@code Last-Modified}, or {@code Pragma}, that an - * actual response might have and can be exposed. - *

    The special value {@code "*"} allows all headers to be exposed for - * non-credentialed requests. + * Set the list of response headers that an actual response might have and + * can be exposed. The special value {@code "*"} allows all headers to be + * exposed. + *

    Please, see {@link CorsConfiguration#setExposedHeaders(List)} for + * details. *

    By default this is not set. */ public CorsRegistration exposedHeaders(String... headers) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java index 523f5dcc0c5c..e1a25396c7cf 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -77,9 +77,11 @@ public CorsRegistration allowedOriginPatterns(String... patterns) { /** * Set the HTTP methods to allow, e.g. {@code "GET"}, {@code "POST"}, etc. - *

    The special value {@code "*"} allows all methods. - *

    By default "simple" methods {@code GET}, {@code HEAD}, and {@code POST} + * The special value {@code "*"} allows all methods. By default, + * "simple" methods {@code GET}, {@code HEAD}, and {@code POST} * are allowed. + *

    Please, see {@link CorsConfiguration#setAllowedMethods(List)} for + * details. */ public CorsRegistration allowedMethods(String... methods) { this.config.setAllowedMethods(Arrays.asList(methods)); @@ -88,11 +90,10 @@ public CorsRegistration allowedMethods(String... methods) { /** * Set the list of headers that a pre-flight request can list as allowed - * for use during an actual request. - *

    The special value {@code "*"} may be used to allow all headers. - *

    A header name is not required to be listed if it is one of: - * {@code Cache-Control}, {@code Content-Language}, {@code Expires}, - * {@code Last-Modified}, or {@code Pragma} as per the CORS spec. + * for use during an actual request. The special value {@code "*"} + * may be used to allow all headers. + *

    Please, see {@link CorsConfiguration#setAllowedHeaders(List)} for + * details. *

    By default all headers are allowed. */ public CorsRegistration allowedHeaders(String... headers) { @@ -101,12 +102,11 @@ public CorsRegistration allowedHeaders(String... headers) { } /** - * Set the list of response headers other than "simple" headers, i.e. - * {@code Cache-Control}, {@code Content-Language}, {@code Content-Type}, - * {@code Expires}, {@code Last-Modified}, or {@code Pragma}, that an - * actual response might have and can be exposed. - *

    The special value {@code "*"} allows all headers to be exposed for - * non-credentialed requests. + * Set the list of response headers that an actual response might have and + * can be exposed. The special value {@code "*"} allows all headers to be + * exposed. + *

    Please, see {@link CorsConfiguration#setExposedHeaders(List)} for + * details. *

    By default this is not set. */ public CorsRegistration exposedHeaders(String... headers) { From 40678bb981bf5f8c0127bdd54976df6ede08b1ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Mon, 11 Sep 2023 18:22:03 +0200 Subject: [PATCH 45/47] Refine CORS documentation for wildcard processing This commit adds a reference documentation section dedicated to CORS credentialed requests and related wildcard processing. Closes gh-31168 --- src/docs/asciidoc/web/webflux-cors.adoc | 29 +++++++++++++++++++++++++ src/docs/asciidoc/web/webmvc-cors.adoc | 29 +++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/src/docs/asciidoc/web/webflux-cors.adoc b/src/docs/asciidoc/web/webflux-cors.adoc index f1877b2cf7c1..0ace450945a2 100644 --- a/src/docs/asciidoc/web/webflux-cors.adoc +++ b/src/docs/asciidoc/web/webflux-cors.adoc @@ -76,6 +76,35 @@ To learn more from the source or to make advanced customizations, see: +[[webflux-cors-credentialed-requests]] +== Credentialed Requests +[.small]#<># + +Using CORS with credentialed requests requires enabling `allowedCredentials`. Be aware that +this option establishes a high level of trust with the configured domains and also increases +the surface of attack of the web application by exposing sensitive user-specific information +such as cookies and CSRF tokens. + +Enabling credentials also impacts how the configured `"*"` CORS wildcards are processed: + +* Wildcards are not authorized in `allowOrigins`, but alternatively +the `allowOriginPatterns` property may be used to match to a dynamic set of origins. +* When set on `allowedHeaders` or `allowedMethods`, the `Access-Control-Allow-Headers` +and `Access-Control-Allow-Methods` response headers are handled by copying the related +headers and method specified in the CORS preflight request. +* When set on `exposedHeaders`, `Access-Control-Expose-Headers` response header is set +either to the configured list of headers or to the wildcard character. While the CORS spec +does not allow the wildcard character when `Access-Control-Allow-Credentials` is set to +`true`, most browsers support it and the response headers are not all available during the +CORS processing, so as a consequence the wildcard character is the header value used when +specified regardless of the value of the `allowCredentials` property. + +WARNING: While such wildcard configuration can be handy, it is recommended when possible to configure +a finite set of values instead to provide a higher level of security. + + + + [[webflux-cors-controller]] == `@CrossOrigin` [.small]#<># diff --git a/src/docs/asciidoc/web/webmvc-cors.adoc b/src/docs/asciidoc/web/webmvc-cors.adoc index 007c8dada078..dda489516d93 100644 --- a/src/docs/asciidoc/web/webmvc-cors.adoc +++ b/src/docs/asciidoc/web/webmvc-cors.adoc @@ -76,6 +76,35 @@ To learn more from the source or make advanced customizations, check the code be +[[mvc-cors-credentialed-requests]] +== Credentialed Requests +[.small]#<># + +Using CORS with credentialed requests requires enabling `allowedCredentials`. Be aware that +this option establishes a high level of trust with the configured domains and also increases +the surface of attack of the web application by exposing sensitive user-specific information +such as cookies and CSRF tokens. + +Enabling credentials also impacts how the configured `"*"` CORS wildcards are processed: + +* Wildcards are not authorized in `allowOrigins`, but alternatively +the `allowOriginPatterns` property may be used to match to a dynamic set of origins. +* When set on `allowedHeaders` or `allowedMethods`, the `Access-Control-Allow-Headers` +and `Access-Control-Allow-Methods` response headers are handled by copying the related +headers and method specified in the CORS preflight request. +* When set on `exposedHeaders`, `Access-Control-Expose-Headers` response header is set +either to the configured list of headers or to the wildcard character. While the CORS spec +does not allow the wildcard character when `Access-Control-Allow-Credentials` is set to +`true`, most browsers support it and the response headers are not all available during the +CORS processing, so as a consequence the wildcard character is the header value used when +specified regardless of the value of the `allowCredentials` property. + +WARNING: While such wildcard configuration can be handy, it is recommended when possible to configure +a finite set of values instead to provide a higher level of security. + + + + [[mvc-cors-controller]] == `@CrossOrigin` [.small]#<># From f7bf2431fb6f923ae484d6b5cdc5547c3fe04c72 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 14 Sep 2023 09:28:54 +0200 Subject: [PATCH 46/47] Clarify IN clause resolution with List/Iterable parameter Closes gh-31228 --- src/docs/asciidoc/data-access.adoc | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/docs/asciidoc/data-access.adoc b/src/docs/asciidoc/data-access.adoc index 7dbe197d6466..85be0f0c1b18 100644 --- a/src/docs/asciidoc/data-access.adoc +++ b/src/docs/asciidoc/data-access.adoc @@ -6035,18 +6035,17 @@ The following example shows how to create and insert a BLOB: <2> Using the method `setClobAsCharacterStream` to pass in the contents of the CLOB. <3> Using the method `setBlobAsBinaryStream` to pass in the contents of the BLOB. - [NOTE] ==== If you invoke the `setBlobAsBinaryStream`, `setClobAsAsciiStream`, or `setClobAsCharacterStream` method on the `LobCreator` returned from -`DefaultLobHandler.getLobCreator()`, you can optionally specify a negative value for the -`contentLength` argument. If the specified content length is negative, the +`DefaultLobHandler.getLobCreator()`, you can optionally specify a negative value +for the `contentLength` argument. If the specified content length is negative, the `DefaultLobHandler` uses the JDBC 4.0 variants of the set-stream methods without a length parameter. Otherwise, it passes the specified length on to the driver. -See the documentation for the JDBC driver you use to verify that it supports streaming a -LOB without providing the content length. +See the documentation for the JDBC driver you use to verify that it supports streaming +a LOB without providing the content length. ==== Now it is time to read the LOB data from the database. Again, you use a `JdbcTemplate` @@ -6093,15 +6092,15 @@ variable list of values. A typical example would be `select * from t_actor where JDBC standard. You cannot declare a variable number of placeholders. You need a number of variations with the desired number of placeholders prepared, or you need to generate the SQL string dynamically once you know how many placeholders are required. The named -parameter support provided in the `NamedParameterJdbcTemplate` and `JdbcTemplate` takes -the latter approach. You can pass in the values as a `java.util.List` of primitive objects. This -list is used to insert the required placeholders and pass in the values during -statement execution. - -NOTE: Be careful when passing in many values. The JDBC standard does not guarantee that you -can use more than 100 values for an `in` expression list. Various databases exceed this -number, but they usually have a hard limit for how many values are allowed. For example, Oracle's -limit is 1000. +parameter support provided in the `NamedParameterJdbcTemplate` takes the latter approach. +You can pass in the values as a `java.util.List` (or any `Iterable`) of simple values. +This list is used to insert the required placeholders into the actual SQL statement +and pass in the values during statement execution. + +NOTE: Be careful when passing in many values. The JDBC standard does not guarantee that +you can use more than 100 values for an `IN` expression list. Various databases exceed +this number, but they usually have a hard limit for how many values are allowed. +For example, Oracle's limit is 1000. In addition to the primitive values in the value list, you can create a `java.util.List` of object arrays. This list can support multiple expressions being defined for the `in` From e5d99ecf984537ab52825292d5ce76130b425e3e Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Thu, 14 Sep 2023 07:43:24 +0000 Subject: [PATCH 47/47] Release v5.3.30 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 61922c8fd00f..9ae12b8dfed3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=5.3.30-SNAPSHOT +version=5.3.30 org.gradle.jvmargs=-Xmx2048m org.gradle.caching=true org.gradle.parallel=true