From 0b4b313baeddb4edee26d0678f040f0696c4873e Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sat, 15 Jul 2023 14:17:52 +0200 Subject: [PATCH] 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 d77880c012c..4e0f365bc85 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 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA 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 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA 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 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA 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 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA 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 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA 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 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA 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 fcb2ef67e36..fb0ba14b0be 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 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac 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 be9667b19a3..a15ebb5a1ab 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 @@ class BeanDefinitionValueResolver { 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 dd268fc517f..b1ce9e61faf 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.TypeConverter; 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 @@ class ConstructorResolver { 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 @@ class ConstructorResolver { 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 @@ class ConstructorResolver { "] - 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 @@ class ConstructorResolver { } } - 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 @@ class ConstructorResolver { 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 @@ class ConstructorResolver { } /** - * 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 @@ class ConstructorResolver { } } + 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 @@ class ConstructorResolver { } } + + /** + * 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 f3ec59432dd..9170a4abe6f 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.ResolvableType; 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 class AutowiredAnnotationBeanPostProcessorTests { 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 class AutowiredAnnotationBeanPostProcessorTests { 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 @@ public class AutowiredAnnotationBeanPostProcessorTests { 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 @@ public class AutowiredAnnotationBeanPostProcessorTests { 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 @@ public class AutowiredAnnotationBeanPostProcessorTests { 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 class AutowiredAnnotationBeanPostProcessorTests { 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 class AutowiredAnnotationBeanPostProcessorTests { 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 class AutowiredAnnotationBeanPostProcessorTests { .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 class AutowiredAnnotationBeanPostProcessorTests { } + 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 b3e0c555480..0443c5699ca 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 class MethodInvoker { /** * 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 class MethodInvoker { 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; }