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" diff --git a/gradle.properties b/gradle.properties index 6089d4d958d3..9ae12b8dfed3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=5.3.29-SNAPSHOT +version=5.3.30 org.gradle.jvmargs=-Xmx2048m org.gradle.caching=true org.gradle.parallel=true 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/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-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-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; } 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..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. */ @@ -651,7 +702,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 +721,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 +804,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 +813,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 +828,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 +863,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..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 @@ -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,61 @@ 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 + Set autowiredBeanNames = null; + if (descriptor.hasShortcut()) { + // Reset shortcut and try to re-resolve it in this thread... + descriptor.setShortcut(null); + 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); + } + 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 +893,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 +931,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 +1054,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/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/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-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..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 @@ -72,6 +72,9 @@ 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.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -131,6 +134,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 +157,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 +177,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 +207,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 +255,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 +285,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 @@ -398,6 +447,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(); @@ -455,6 +520,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(); @@ -719,6 +800,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 @@ -740,6 +824,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); @@ -778,6 +873,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); @@ -881,6 +987,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( @@ -2441,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; } @@ -2482,7 +2661,7 @@ public NonPublicResourceInjectionBean() { @Required @SuppressWarnings("deprecation") public void setTestBean2(TestBean testBean2) { - super.setTestBean2(testBean2); + this.testBean2 = testBean2; } @Autowired @@ -2498,6 +2677,7 @@ private void inject(ITestBean testBean4) { @Autowired protected void initBeanFactory(BeanFactory beanFactory) { + Assert.state(this.baseInjected, "Wrong initialization order"); this.beanFactory = beanFactory; } @@ -2804,6 +2984,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; @@ -3906,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-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 + + + 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-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); } } } 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..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. @@ -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-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/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/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/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 c528a83206bd..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 @@ -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,21 +388,20 @@ 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); - // 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); @@ -469,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)) { @@ -522,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 @@ -549,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, @@ -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; } @@ -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()); } @@ -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/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/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/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-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/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; } } 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/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-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/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-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/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-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(); 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/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-context/src/test/java/org/springframework/cache/CacheReproTests.java b/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java index 7b4911a8b49a..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. @@ -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(); } 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 { 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-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/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-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-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-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-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-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); } } 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/ReactiveAdapterRegistry.java b/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java index 9e56b5d5e37d..28a1152b5876 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. @@ -136,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)); } /** @@ -401,9 +429,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/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/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(); 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-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/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/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/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-core/src/main/java/org/springframework/util/ClassUtils.java b/spring-core/src/main/java/org/springframework/util/ClassUtils.java index d5858c7399c7..4a84db6733b7 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 || @@ -1403,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-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(); 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/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; } 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/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(); } 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/test/java/org/springframework/core/ReactiveAdapterRegistryTests.java b/spring-core/src/test/java/org/springframework/core/ReactiveAdapterRegistryTests.java index 35683ac0cbee..8d4728b2e69b 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. @@ -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 { @@ -100,7 +131,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 +139,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 +147,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 +156,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 +325,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 +334,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 +342,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 +350,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 +359,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 +368,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 +376,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 +384,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-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..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. @@ -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..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. @@ -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()); } - } } 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 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-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/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();
     	}
     
     }
    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());
    +			}
     		};
     	}
     
    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() {
    +	}
    +
     }
    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-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 225ab2b12235..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 { @@ -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/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/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-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; } } } 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/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); } 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; } + } 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-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-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 2ced8c86cd45..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 @@ -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,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,9 +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 @@ -203,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) { 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-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-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)); 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/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-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-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(); + } + } 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..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. @@ -35,6 +35,7 @@ * * @author Phillip Webb * @author Sam Brannen + * @author Yanming Zhou * @since 5.2.5 * @see DynamicPropertiesContextCustomizer */ @@ -54,10 +55,15 @@ public DynamicPropertiesContextCustomizer createContextCustomizer(Class testC } private void findMethods(Class testClass, Set methods) { - methods.addAll(MethodIntrospector.selectMethods(testClass, this::isAnnotated)); + // 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); } + methods.addAll(MethodIntrospector.selectMethods(testClass, this::isAnnotated)); } private boolean isAnnotated(Method method) { 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 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/event/TransactionalApplicationListenerMethodAdapter.java b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java index ce39e136c1a0..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 @@ -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,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<>(); @@ -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.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); 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..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. @@ -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-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/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 { 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..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 @@ -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 @@ -159,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); @@ -307,13 +315,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 +333,6 @@ public void conditionFoundOnMetaAnnotation() { getContext().publishEvent("SKIP"); getEventCollector().assertNoEventReceived(); return null; - }); getEventCollector().assertNoEventReceived(); } @@ -530,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 { 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/accept/AbstractMappingContentNegotiationStrategy.java b/spring-web/src/main/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategy.java index 62a650d3c4d3..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. @@ -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/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/ServletRequestDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java index 1c6f0218d2e7..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. @@ -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) { 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/bind/support/WebExchangeBindException.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java index 8c605fc66254..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-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,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(); 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/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-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/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-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-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/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) { 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 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. 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 4df6bbf5210a..e020f6290b10 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 @@ -11031,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 @@ -11043,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 diff --git a/src/docs/asciidoc/data-access.adoc b/src/docs/asciidoc/data-access.adoc index 88ddf5c2fe2f..85be0f0c1b18 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` @@ -3636,11 +3644,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 +3656,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 +3684,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 +3696,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 +3763,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 +3779,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)!! } @@ -6036,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` @@ -6089,24 +6087,24 @@ 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 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. +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. +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` -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 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]#<>#