From f9d63e7bb1acaa4c752ab73a53513c08e3817021 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 23 Sep 2021 15:53:54 +0200 Subject: [PATCH 1/5] BeanUtils.getResolvableConstructor falls back to single non-public constructor Closes gh-27437 --- .../org/springframework/beans/BeanUtils.java | 44 ++++++++++++------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java index 8ddd6ff74467..f7708247d786 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java @@ -226,32 +226,46 @@ public static T instantiateClass(Constructor ctor, Object... args) throws } /** - * Return a resolvable constructor for the provided class, either a primary constructor - * or single public constructor or simply a default constructor. Callers have to be - * prepared to resolve arguments for the returned constructor's parameters, if any. + * Return a resolvable constructor for the provided class, either a primary or single + * public constructor with arguments, or a single non-public constructor with arguments, + * or simply a default constructor. Callers have to be prepared to resolve arguments + * for the returned constructor's parameters, if any. * @param clazz the class to check + * @throws IllegalStateException in case of no unique constructor found at all * @since 5.3 * @see #findPrimaryConstructor */ @SuppressWarnings("unchecked") public static Constructor getResolvableConstructor(Class clazz) { Constructor ctor = findPrimaryConstructor(clazz); - if (ctor == null) { - Constructor[] ctors = clazz.getConstructors(); + if (ctor != null) { + return ctor; + } + + Constructor[] ctors = clazz.getConstructors(); + if (ctors.length == 1) { + // A single public constructor + return (Constructor) ctors[0]; + } + else if (ctors.length == 0){ + ctors = clazz.getDeclaredConstructors(); if (ctors.length == 1) { - ctor = (Constructor) ctors[0]; + // A single non-public constructor, e.g. from a non-public record type + return (Constructor) ctors[0]; } - else { - try { - ctor = clazz.getDeclaredConstructor(); - } - catch (NoSuchMethodException ex) { - throw new IllegalStateException("No primary or single public constructor found for " + - clazz + " - and no default constructor found either"); - } + } + else { + // Several public constructors -> let's take the default constructor + try { + return clazz.getDeclaredConstructor(); + } + catch (NoSuchMethodException ex) { + // Giving up... } } - return ctor; + + // No unique constructor at all + throw new IllegalStateException("No primary or single unique constructor found for " + clazz); } /** From 58898de5429c43ae676a7643fcd20d684af1c9ec Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 23 Sep 2021 15:54:40 +0200 Subject: [PATCH 2/5] Provide accessors for externallyManagedConfigMembers and Init/DestroyMethods Closes gh-27449 --- .../factory/support/RootBeanDefinition.java | 63 +++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java index 863873499266..f4fdafa3767e 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java @@ -21,7 +21,8 @@ import java.lang.reflect.Executable; import java.lang.reflect.Member; import java.lang.reflect.Method; -import java.util.HashSet; +import java.util.Collections; +import java.util.LinkedHashSet; import java.util.Set; import java.util.function.Supplier; @@ -422,15 +423,21 @@ public Method getResolvedFactoryMethod() { return this.factoryMethodToIntrospect; } + /** + * Register an externally managed configuration method or field. + */ public void registerExternallyManagedConfigMember(Member configMember) { synchronized (this.postProcessingLock) { if (this.externallyManagedConfigMembers == null) { - this.externallyManagedConfigMembers = new HashSet<>(1); + this.externallyManagedConfigMembers = new LinkedHashSet<>(1); } this.externallyManagedConfigMembers.add(configMember); } } + /** + * Check whether the given method or field is an externally managed configuration member. + */ public boolean isExternallyManagedConfigMember(Member configMember) { synchronized (this.postProcessingLock) { return (this.externallyManagedConfigMembers != null && @@ -438,15 +445,33 @@ public boolean isExternallyManagedConfigMember(Member configMember) { } } + /** + * Return all externally managed configuration methods and fields (as an immutable Set). + * @since 5.3.11 + */ + public Set getExternallyManagedConfigMembers() { + synchronized (this.postProcessingLock) { + return (this.externallyManagedConfigMembers != null ? + Collections.unmodifiableSet(new LinkedHashSet<>(this.externallyManagedConfigMembers)) : + Collections.emptySet()); + } + } + + /** + * Register an externally managed configuration initialization method. + */ public void registerExternallyManagedInitMethod(String initMethod) { synchronized (this.postProcessingLock) { if (this.externallyManagedInitMethods == null) { - this.externallyManagedInitMethods = new HashSet<>(1); + this.externallyManagedInitMethods = new LinkedHashSet<>(1); } this.externallyManagedInitMethods.add(initMethod); } } + /** + * Check whether the given method name indicates an externally managed initialization method. + */ public boolean isExternallyManagedInitMethod(String initMethod) { synchronized (this.postProcessingLock) { return (this.externallyManagedInitMethods != null && @@ -454,15 +479,33 @@ public boolean isExternallyManagedInitMethod(String initMethod) { } } + /** + * Return all externally managed initialization methods (as an immutable Set). + * @since 5.3.11 + */ + public Set getExternallyManagedInitMethods() { + synchronized (this.postProcessingLock) { + return (this.externallyManagedInitMethods != null ? + Collections.unmodifiableSet(new LinkedHashSet<>(this.externallyManagedInitMethods)) : + Collections.emptySet()); + } + } + + /** + * Register an externally managed configuration destruction method. + */ public void registerExternallyManagedDestroyMethod(String destroyMethod) { synchronized (this.postProcessingLock) { if (this.externallyManagedDestroyMethods == null) { - this.externallyManagedDestroyMethods = new HashSet<>(1); + this.externallyManagedDestroyMethods = new LinkedHashSet<>(1); } this.externallyManagedDestroyMethods.add(destroyMethod); } } + /** + * Check whether the given method name indicates an externally managed destruction method. + */ public boolean isExternallyManagedDestroyMethod(String destroyMethod) { synchronized (this.postProcessingLock) { return (this.externallyManagedDestroyMethods != null && @@ -470,6 +513,18 @@ public boolean isExternallyManagedDestroyMethod(String destroyMethod) { } } + /** + * Return all externally managed destruction methods (as an immutable Set). + * @since 5.3.11 + */ + public Set getExternallyManagedDestroyMethods() { + synchronized (this.postProcessingLock) { + return (this.externallyManagedDestroyMethods != null ? + Collections.unmodifiableSet(new LinkedHashSet<>(this.externallyManagedDestroyMethods)) : + Collections.emptySet()); + } + } + @Override public RootBeanDefinition cloneBeanDefinition() { From 5cbc972a0de195ebb09645f65a1dea8fb85880c9 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 23 Sep 2021 15:56:06 +0200 Subject: [PATCH 3/5] Log rejected listener container tasks at warn level Closes gh-27451 --- .../jms/listener/AbstractJmsListeningContainer.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 b7e1734bb99f..a9061e162fc5 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-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -582,13 +582,13 @@ protected void doRescheduleTask(Object task) { /** * Log a task that has been rejected by {@link #doRescheduleTask}. *

The default implementation simply logs a corresponding message - * at debug level. + * at warn level. * @param task the rejected task object * @param ex the exception thrown from {@link #doRescheduleTask} */ protected void logRejectedTask(Object task, RuntimeException ex) { - if (logger.isDebugEnabled()) { - logger.debug("Listener container task [" + task + "] has been rejected and paused: " + ex); + if (logger.isWarnEnabled()) { + logger.warn("Listener container task [" + task + "] has been rejected and paused: " + ex); } } From 208fafa4a3b358519eb2005868dee437e28df22a Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 23 Sep 2021 15:56:49 +0200 Subject: [PATCH 4/5] Fix contract violations in ConcurrentReferenceHashMap's EntrySet/Iterator Closes gh-27454 --- .../util/ConcurrentReferenceHashMap.java | 3 ++- .../util/ConcurrentReferenceHashMapTests.java | 25 ++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) 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 f3af11b50a97..3291ca6e079a 100644 --- a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java +++ b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java @@ -858,7 +858,7 @@ public boolean contains(@Nullable Object o) { Reference ref = ConcurrentReferenceHashMap.this.getReference(entry.getKey(), Restructure.NEVER); Entry otherEntry = (ref != null ? ref.get() : null); if (otherEntry != null) { - return ObjectUtils.nullSafeEquals(otherEntry.getValue(), otherEntry.getValue()); + return ObjectUtils.nullSafeEquals(entry.getValue(), otherEntry.getValue()); } } return false; @@ -966,6 +966,7 @@ private void moveToNextSegment() { public void remove() { Assert.state(this.last != null, "No element to remove"); ConcurrentReferenceHashMap.this.remove(this.last.getKey()); + this.last = null; } } diff --git a/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java b/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java index b8ddb11aa0c8..e50168ee4e9e 100644 --- a/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java +++ b/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,11 +41,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link ConcurrentReferenceHashMap}. * * @author Phillip Webb + * @author Juergen Hoeller */ class ConcurrentReferenceHashMapTests { @@ -466,6 +468,7 @@ void shouldRemoveViaEntrySet() { iterator.next(); iterator.next(); iterator.remove(); + assertThatIllegalStateException().isThrownBy(iterator::remove); iterator.next(); assertThat(iterator.hasNext()).isFalse(); assertThat(this.map).hasSize(2); @@ -486,6 +489,26 @@ void shouldSetViaEntrySet() { assertThat(this.map.get(2)).isEqualTo("2b"); } + @Test + void containsViaEntrySet() { + this.map.put(1, "1"); + this.map.put(2, "2"); + this.map.put(3, "3"); + Set> entrySet = this.map.entrySet(); + Set> copy = new HashMap<>(this.map).entrySet(); + copy.forEach(entry -> assertThat(entrySet.contains(entry)).isTrue()); + this.map.put(1, "A"); + this.map.put(2, "B"); + this.map.put(3, "C"); + copy.forEach(entry -> assertThat(entrySet.contains(entry)).isFalse()); + this.map.put(1, "1"); + this.map.put(2, "2"); + this.map.put(3, "3"); + copy.forEach(entry -> assertThat(entrySet.contains(entry)).isTrue()); + entrySet.clear(); + copy.forEach(entry -> assertThat(entrySet.contains(entry)).isFalse()); + } + @Test @Disabled("Intended for use during development only") void shouldBeFasterThanSynchronizedMap() throws InterruptedException { From e29cfa350106d8811072159748e41a4ef0d0b697 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 23 Sep 2021 15:57:43 +0200 Subject: [PATCH 5/5] Polishing --- spring-core/spring-core.gradle | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spring-core/spring-core.gradle b/spring-core/spring-core.gradle index a6e5e309b073..e4c568f84b13 100644 --- a/spring-core/spring-core.gradle +++ b/spring-core/spring-core.gradle @@ -50,21 +50,21 @@ dependencies { optional("io.reactivex.rxjava3:rxjava") optional("io.smallrye.reactive:mutiny") optional("io.netty:netty-buffer") - testImplementation("io.projectreactor:reactor-test") - testImplementation("com.google.code.findbugs:jsr305") testImplementation("javax.annotation:javax.annotation-api") testImplementation("javax.xml.bind:jaxb-api") + testImplementation("com.google.code.findbugs:jsr305") testImplementation("com.fasterxml.woodstox:woodstox-core") testImplementation("org.xmlunit:xmlunit-assertj") testImplementation("org.xmlunit:xmlunit-matchers") + testImplementation("io.projectreactor:reactor-test") testImplementation("io.projectreactor.tools:blockhound") - testFixturesImplementation("io.projectreactor:reactor-test") testFixturesImplementation("com.google.code.findbugs:jsr305") testFixturesImplementation("org.junit.platform:junit-platform-launcher") testFixturesImplementation("org.junit.jupiter:junit-jupiter-api") testFixturesImplementation("org.junit.jupiter:junit-jupiter-params") testFixturesImplementation("org.assertj:assertj-core") testFixturesImplementation("org.xmlunit:xmlunit-assertj") + testFixturesImplementation("io.projectreactor:reactor-test") } jar { @@ -92,7 +92,7 @@ jar { } test { - // Make sure the classes dir is used on the test classpath (required by ResourceTests) - // When test fixtures are involved, the JAR is used by default + // Make sure the classes dir is used on the test classpath (required by ResourceTests). + // When test fixtures are involved, the JAR is used by default. classpath = sourceSets.main.output.classesDirs + classpath - files(jar.archiveFile) }