diff --git a/build.gradle b/build.gradle index 7b125b06debc..974b07de3de3 100644 --- a/build.gradle +++ b/build.gradle @@ -28,24 +28,24 @@ ext { !it.name.equals("spring-build-src") && !it.name.equals("spring-framework-bom") } - aspectjVersion = "1.9.4" + aspectjVersion = "1.9.5" freemarkerVersion = "2.3.28" groovyVersion = "2.5.8" hsqldbVersion = "2.4.1" jackson2Version = "2.9.9" - jettyVersion = "9.4.21.v20190926" + jettyVersion = "9.4.24.v20191120" junit5Version = "5.3.2" kotlinVersion = "1.2.71" log4jVersion = "2.11.2" nettyVersion = "4.1.43.Final" - reactorVersion = "Californium-SR13" + reactorVersion = "Californium-SR14" rxjavaVersion = "1.3.8" rxjavaAdapterVersion = "1.2.1" - rxjava2Version = "2.2.14" + rxjava2Version = "2.2.15" slf4jVersion = "1.7.28" // spring-jcl + consistent 3rd party deps tiles3Version = "3.0.8" - tomcatVersion = "9.0.27" - undertowVersion = "2.0.27.Final" + tomcatVersion = "9.0.29" + undertowVersion = "2.0.28.Final" gradleScriptDir = "${rootProject.projectDir}/gradle" withoutJclOverSlf4j = { @@ -142,7 +142,7 @@ configure(allprojects) { project -> } checkstyle { - toolVersion = "8.26" + toolVersion = "8.27" configDir = rootProject.file("src/checkstyle") } diff --git a/gradle.properties b/gradle.properties index 52cd8c3529f2..47ad59cda14b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=5.1.12.BUILD-SNAPSHOT +version=5.1.13.BUILD-SNAPSHOT diff --git a/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java index 3ca7fc95c7e8..f5c165d5cfdb 100644 --- a/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java +++ b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 the original author 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 @@ /** * Decorator for a standard {@link BeanInfo} object, e.g. as created by - * {@link Introspector#getBeanInfo(Class)}, designed to discover and register static - * and/or non-void returning setter methods. For example: + * {@link Introspector#getBeanInfo(Class)}, designed to discover and register + * static and/or non-void returning setter methods. For example: * *
  * public class Bean {
@@ -145,11 +145,10 @@ private List findCandidateWriteMethods(MethodDescriptor[] methodDescript
 
 	public static boolean isCandidateWriteMethod(Method method) {
 		String methodName = method.getName();
-		Class[] parameterTypes = method.getParameterTypes();
-		int nParams = parameterTypes.length;
+		int nParams = method.getParameterCount();
 		return (methodName.length() > 3 && methodName.startsWith("set") && Modifier.isPublic(method.getModifiers()) &&
 				(!void.class.isAssignableFrom(method.getReturnType()) || Modifier.isStatic(method.getModifiers())) &&
-				(nParams == 1 || (nParams == 2 && int.class == parameterTypes[0])));
+				(nParams == 1 || (nParams == 2 && int.class == method.getParameterTypes()[0])));
 	}
 
 	private void handleCandidateWriteMethod(Method method) throws IntrospectionException {
@@ -209,7 +208,7 @@ private PropertyDescriptor findExistingPropertyDescriptor(String propertyName, C
 	}
 
 	private String propertyNameFor(Method method) {
-		return Introspector.decapitalize(method.getName().substring(3, method.getName().length()));
+		return Introspector.decapitalize(method.getName().substring(3));
 	}
 
 
@@ -488,7 +487,7 @@ public void setPropertyEditorClass(@Nullable Class propertyEditorClass) {
 		}
 
 		/*
-		 * See java.beans.IndexedPropertyDescriptor#equals(java.lang.Object)
+		 * See java.beans.IndexedPropertyDescriptor#equals
 		 */
 		@Override
 		public boolean equals(Object other) {
@@ -535,11 +534,13 @@ static class PropertyDescriptorComparator implements Comparator beans = new Closure(this) {
 			@Override
-			public Object call(Object[] args) {
-				invokeBeanDefiningClosure((Closure) args[0]);
+			public Object call(Object... args) {
+				invokeBeanDefiningClosure((Closure) args[0]);
 				return null;
 			}
 		};
@@ -285,7 +289,7 @@ public void setVariable(String name, Object value) {
 	 * @param closure the block or closure
 	 * @return this {@code GroovyBeanDefinitionReader} instance
 	 */
-	public GroovyBeanDefinitionReader beans(Closure closure) {
+	public GroovyBeanDefinitionReader beans(Closure closure) {
 		return invokeBeanDefiningClosure(closure);
 	}
 
@@ -309,25 +313,22 @@ public GenericBeanDefinition bean(Class type) {
 	public AbstractBeanDefinition bean(Class type, Object...args) {
 		GroovyBeanDefinitionWrapper current = this.currentBeanDefinition;
 		try {
-			Closure callable = null;
-			Collection constructorArgs = null;
+			Closure callable = null;
+			Collection constructorArgs = null;
 			if (!ObjectUtils.isEmpty(args)) {
 				int index = args.length;
 				Object lastArg = args[index - 1];
-				if (lastArg instanceof Closure) {
-					callable = (Closure) lastArg;
+				if (lastArg instanceof Closure) {
+					callable = (Closure) lastArg;
 					index--;
 				}
-				if (index > -1) {
-					constructorArgs = resolveConstructorArguments(args, 0, index);
-				}
+				constructorArgs = resolveConstructorArguments(args, 0, index);
 			}
 			this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(null, type, constructorArgs);
 			if (callable != null) {
 				callable.call(this.currentBeanDefinition);
 			}
 			return this.currentBeanDefinition.getBeanDefinition();
-
 		}
 		finally {
 			this.currentBeanDefinition = current;
@@ -373,10 +374,11 @@ public void importBeans(String resourcePattern) throws IOException {
 	 * This method overrides method invocation to create beans for each method name that
 	 * takes a class argument.
 	 */
+	@Override
 	public Object invokeMethod(String name, Object arg) {
 		Object[] args = (Object[])arg;
 		if ("beans".equals(name) && args.length == 1 && args[0] instanceof Closure) {
-			return beans((Closure) args[0]);
+			return beans((Closure) args[0]);
 		}
 		else if ("ref".equals(name)) {
 			String refName;
@@ -429,10 +431,10 @@ private boolean addDeferredProperty(String property, Object newValue) {
 	private void finalizeDeferredProperties() {
 		for (DeferredProperty dp : this.deferredProperties.values()) {
 			if (dp.value instanceof List) {
-				dp.value = manageListIfNecessary((List) dp.value);
+				dp.value = manageListIfNecessary((List) dp.value);
 			}
 			else if (dp.value instanceof Map) {
-				dp.value = manageMapIfNecessary((Map) dp.value);
+				dp.value = manageMapIfNecessary((Map) dp.value);
 			}
 			dp.apply();
 		}
@@ -444,7 +446,7 @@ else if (dp.value instanceof Map) {
 	 * @param callable the closure argument
 	 * @return this {@code GroovyBeanDefinitionReader} instance
 	 */
-	protected GroovyBeanDefinitionReader invokeBeanDefiningClosure(Closure callable) {
+	protected GroovyBeanDefinitionReader invokeBeanDefiningClosure(Closure callable) {
 		callable.setDelegate(this);
 		callable.call();
 		finalizeDeferredProperties();
@@ -483,9 +485,10 @@ else if (args[0] instanceof RuntimeBeanReference) {
 		else if (args[0] instanceof Map) {
 			// named constructor arguments
 			if (args.length > 1 && args[1] instanceof Class) {
-				List constructorArgs = resolveConstructorArguments(args, 2, hasClosureArgument ? args.length - 1 : args.length);
-				this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName, (Class)args[1], constructorArgs);
-				Map namedArgs = (Map)args[0];
+				List constructorArgs =
+						resolveConstructorArguments(args, 2, hasClosureArgument ? args.length - 1 : args.length);
+				this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName, (Class) args[1], constructorArgs);
+				Map namedArgs = (Map) args[0];
 				for (Object o : namedArgs.keySet()) {
 					String propName = (String) o;
 					setProperty(propName, namedArgs.get(propName));
@@ -494,8 +497,8 @@ else if (args[0] instanceof Map) {
 			// factory method syntax
 			else {
 				this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName);
-				//First arg is the map containing factoryBean : factoryMethod
-				Map.Entry factoryBeanEntry = (Map.Entry) ((Map) args[0]).entrySet().iterator().next();
+				// First arg is the map containing factoryBean : factoryMethod
+				Map.Entry factoryBeanEntry = ((Map) args[0]).entrySet().iterator().next();
 				// If we have a closure body, that will be the last argument.
 				// In between are the constructor args
 				int constructorArgsTest = (hasClosureArgument ? 2 : 1);
@@ -519,12 +522,13 @@ else if (args[0] instanceof Closure) {
 			this.currentBeanDefinition.getBeanDefinition().setAbstract(true);
 		}
 		else {
-			List constructorArgs = resolveConstructorArguments(args, 0, hasClosureArgument ? args.length - 1 : args.length);
+			List constructorArgs =
+					resolveConstructorArguments(args, 0, hasClosureArgument ? args.length - 1 : args.length);
 			this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName, null, constructorArgs);
 		}
 
 		if (hasClosureArgument) {
-			Closure callable = (Closure) args[args.length - 1];
+			Closure callable = (Closure) args[args.length - 1];
 			callable.setDelegate(this);
 			callable.setResolveStrategy(Closure.DELEGATE_FIRST);
 			callable.call(this.currentBeanDefinition);
@@ -544,10 +548,10 @@ protected List resolveConstructorArguments(Object[] args, int start, int
 				constructorArgs[i] = constructorArgs[i].toString();
 			}
 			else if (constructorArgs[i] instanceof List) {
-				constructorArgs[i] = manageListIfNecessary((List) constructorArgs[i]);
+				constructorArgs[i] = manageListIfNecessary((List) constructorArgs[i]);
 			}
 			else if (constructorArgs[i] instanceof Map){
-				constructorArgs[i] = manageMapIfNecessary((Map) constructorArgs[i]);
+				constructorArgs[i] = manageMapIfNecessary((Map) constructorArgs[i]);
 			}
 		}
 		return Arrays.asList(constructorArgs);
@@ -601,6 +605,7 @@ private Object manageListIfNecessary(List list) {
 	 * This method overrides property setting in the scope of the {@code GroovyBeanDefinitionReader}
 	 * to set properties on the current bean definition.
 	 */
+	@Override
 	public void setProperty(String name, Object value) {
 		if (this.currentBeanDefinition != null) {
 			applyPropertyToBeanDefinition(name, value);
@@ -617,7 +622,7 @@ protected void applyPropertyToBeanDefinition(String name, Object value) {
 		else if (value instanceof Closure) {
 			GroovyBeanDefinitionWrapper current = this.currentBeanDefinition;
 			try {
-				Closure callable = (Closure) value;
+				Closure callable = (Closure) value;
 				Class parameterType = callable.getParameterTypes()[0];
 				if (Object.class == parameterType) {
 					this.currentBeanDefinition = new GroovyBeanDefinitionWrapper("");
@@ -647,6 +652,7 @@ else if (value instanceof Closure) {
 	 * properties from the {@code GroovyBeanDefinitionReader} itself
 	 * 
 	 */
+	@Override
 	public Object getProperty(String name) {
 		Binding binding = getBinding();
 		if (binding != null && binding.hasVariable(name)) {
@@ -690,8 +696,8 @@ else if (this.currentBeanDefinition != null) {
 	}
 
 	private GroovyDynamicElementReader createDynamicElementReader(String namespace) {
-		XmlReaderContext readerContext = this.groovyDslXmlBeanDefinitionReader.createReaderContext(new DescriptiveResource(
-			"Groovy"));
+		XmlReaderContext readerContext = this.groovyDslXmlBeanDefinitionReader.createReaderContext(
+				new DescriptiveResource("Groovy"));
 		BeanDefinitionParserDelegate delegate = new BeanDefinitionParserDelegate(readerContext);
 		boolean decorating = (this.currentBeanDefinition != null);
 		if (!decorating) {
@@ -749,10 +755,12 @@ public GroovyRuntimeBeanReference(String beanName, GroovyBeanDefinitionWrapper b
 			this.metaClass = InvokerHelper.getMetaClass(this);
 		}
 
+		@Override
 		public MetaClass getMetaClass() {
 			return this.metaClass;
 		}
 
+		@Override
 		public Object getProperty(String property) {
 			if (property.equals("beanName")) {
 				return getBeanName();
@@ -769,14 +777,17 @@ else if (this.beanDefinition != null) {
 			}
 		}
 
+		@Override
 		public Object invokeMethod(String name, Object args) {
 			return this.metaClass.invokeMethod(this, name, args);
 		}
 
+		@Override
 		public void setMetaClass(MetaClass metaClass) {
 			this.metaClass = metaClass;
 		}
 
+		@Override
 		public void setProperty(String property, Object newValue) {
 			if (!addDeferredProperty(property, newValue)) {
 				this.beanDefinition.getBeanDefinition().getPropertyValues().add(property, newValue);
@@ -785,7 +796,7 @@ public void setProperty(String property, Object newValue) {
 
 
 		/**
-		 * Wraps a bean definition property an ensures that any RuntimeBeanReference
+		 * Wraps a bean definition property and ensures that any RuntimeBeanReference
 		 * additions to it are deferred for resolution later.
 		 */
 		private class GroovyPropertyValue extends GroovyObjectSupport {
@@ -799,18 +810,21 @@ public GroovyPropertyValue(String propertyName, Object propertyValue) {
 				this.propertyValue = propertyValue;
 			}
 
+			@SuppressWarnings("unused")
 			public void leftShift(Object value) {
 				InvokerHelper.invokeMethod(this.propertyValue, "leftShift", value);
 				updateDeferredProperties(value);
 			}
 
+			@SuppressWarnings("unused")
 			public boolean add(Object value) {
 				boolean retVal = (Boolean) InvokerHelper.invokeMethod(this.propertyValue, "add", value);
 				updateDeferredProperties(value);
 				return retVal;
 			}
 
-			public boolean addAll(Collection values) {
+			@SuppressWarnings("unused")
+			public boolean addAll(Collection values) {
 				boolean retVal = (Boolean) InvokerHelper.invokeMethod(this.propertyValue, "addAll", values);
 				for (Object value : values) {
 					updateDeferredProperties(value);
diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionWrapper.java b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionWrapper.java
index 9afc6e9c3033..d1aeff7d0bcc 100644
--- a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionWrapper.java
+++ b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionWrapper.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2019 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -196,7 +196,7 @@ else if (Boolean.TRUE.equals(newValue)) {
 			// constructorArgs
 			else if (CONSTRUCTOR_ARGS.equals(property) && newValue instanceof List) {
 				ConstructorArgumentValues cav = new ConstructorArgumentValues();
-				List args = (List) newValue;
+				List args = (List) newValue;
 				for (Object arg : args) {
 					cav.addGenericArgumentValue(arg);
 				}
diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java
index 26c36be3a9ea..a46c12bdb000 100644
--- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java
+++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java
@@ -1131,28 +1131,28 @@ public boolean equals(Object other) {
 		}
 		AbstractBeanDefinition that = (AbstractBeanDefinition) other;
 		boolean rtn = ObjectUtils.nullSafeEquals(getBeanClassName(), that.getBeanClassName());
-		rtn = rtn &= ObjectUtils.nullSafeEquals(this.scope, that.scope);
-		rtn = rtn &= this.abstractFlag == that.abstractFlag;
-		rtn = rtn &= this.lazyInit == that.lazyInit;
-		rtn = rtn &= this.autowireMode == that.autowireMode;
-		rtn = rtn &= this.dependencyCheck == that.dependencyCheck;
-		rtn = rtn &= Arrays.equals(this.dependsOn, that.dependsOn);
-		rtn = rtn &= this.autowireCandidate == that.autowireCandidate;
-		rtn = rtn &= ObjectUtils.nullSafeEquals(this.qualifiers, that.qualifiers);
-		rtn = rtn &= this.primary == that.primary;
-		rtn = rtn &= this.nonPublicAccessAllowed == that.nonPublicAccessAllowed;
-		rtn = rtn &= this.lenientConstructorResolution == that.lenientConstructorResolution;
-		rtn = rtn &= ObjectUtils.nullSafeEquals(this.constructorArgumentValues, that.constructorArgumentValues);
-		rtn = rtn &= ObjectUtils.nullSafeEquals(this.propertyValues, that.propertyValues);
-		rtn = rtn &= ObjectUtils.nullSafeEquals(this.methodOverrides, that.methodOverrides);
-		rtn = rtn &= ObjectUtils.nullSafeEquals(this.factoryBeanName, that.factoryBeanName);
-		rtn = rtn &= ObjectUtils.nullSafeEquals(this.factoryMethodName, that.factoryMethodName);
-		rtn = rtn &= ObjectUtils.nullSafeEquals(this.initMethodName, that.initMethodName);
-		rtn = rtn &= this.enforceInitMethod == that.enforceInitMethod;
-		rtn = rtn &= ObjectUtils.nullSafeEquals(this.destroyMethodName, that.destroyMethodName);
-		rtn = rtn &= this.enforceDestroyMethod == that.enforceDestroyMethod;
-		rtn = rtn &= this.synthetic == that.synthetic;
-		rtn = rtn &= this.role == that.role;
+		rtn = rtn && ObjectUtils.nullSafeEquals(this.scope, that.scope);
+		rtn = rtn && this.abstractFlag == that.abstractFlag;
+		rtn = rtn && this.lazyInit == that.lazyInit;
+		rtn = rtn && this.autowireMode == that.autowireMode;
+		rtn = rtn && this.dependencyCheck == that.dependencyCheck;
+		rtn = rtn && Arrays.equals(this.dependsOn, that.dependsOn);
+		rtn = rtn && this.autowireCandidate == that.autowireCandidate;
+		rtn = rtn && ObjectUtils.nullSafeEquals(this.qualifiers, that.qualifiers);
+		rtn = rtn && this.primary == that.primary;
+		rtn = rtn && this.nonPublicAccessAllowed == that.nonPublicAccessAllowed;
+		rtn = rtn && this.lenientConstructorResolution == that.lenientConstructorResolution;
+		rtn = rtn && ObjectUtils.nullSafeEquals(this.constructorArgumentValues, that.constructorArgumentValues);
+		rtn = rtn && ObjectUtils.nullSafeEquals(this.propertyValues, that.propertyValues);
+		rtn = rtn && ObjectUtils.nullSafeEquals(this.methodOverrides, that.methodOverrides);
+		rtn = rtn && ObjectUtils.nullSafeEquals(this.factoryBeanName, that.factoryBeanName);
+		rtn = rtn && ObjectUtils.nullSafeEquals(this.factoryMethodName, that.factoryMethodName);
+		rtn = rtn && ObjectUtils.nullSafeEquals(this.initMethodName, that.initMethodName);
+		rtn = rtn && this.enforceInitMethod == that.enforceInitMethod;
+		rtn = rtn && ObjectUtils.nullSafeEquals(this.destroyMethodName, that.destroyMethodName);
+		rtn = rtn && this.enforceDestroyMethod == that.enforceDestroyMethod;
+		rtn = rtn && this.synthetic == that.synthetic;
+		rtn = rtn && this.role == that.role;
 		return rtn && super.equals(other);
 	}
 
diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java
index ad4064e6b8f9..ffc3d630deda 100644
--- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java
+++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java
@@ -616,6 +616,7 @@ protected void destroyBean(String beanName, @Nullable DisposableBean bean) {
 	 * should not have their own mutexes involved in singleton creation,
 	 * to avoid the potential for deadlocks in lazy-init situations.
 	 */
+	@Override
 	public final Object getSingletonMutex() {
 		return this.singletonObjects;
 	}
diff --git a/spring-context-support/spring-context-support.gradle b/spring-context-support/spring-context-support.gradle
index f4f961a66864..b1d8559cdcf7 100644
--- a/spring-context-support/spring-context-support.gradle
+++ b/spring-context-support/spring-context-support.gradle
@@ -16,7 +16,7 @@ dependencies {
 	optional("org.freemarker:freemarker:${freemarkerVersion}")
 	testCompile(project(":spring-context"))
 	testCompile("org.hsqldb:hsqldb:${hsqldbVersion}")
-	testCompile("org.hibernate:hibernate-validator:6.0.17.Final")
+	testCompile("org.hibernate:hibernate-validator:6.0.18.Final")
 	testCompile("javax.annotation:javax.annotation-api:1.3.2")
 	testRuntime("org.ehcache:jcache:1.0.1")
 	testRuntime("org.ehcache:ehcache:3.4.0")
diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.java
index dca7f6135f22..893e4a6b792b 100644
--- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.java
+++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.java
@@ -88,6 +88,7 @@ public JCacheOperationSource getCacheOperationSource() {
 		return this.cacheOperationSource;
 	}
 
+	@Override
 	public void afterPropertiesSet() {
 		getCacheOperationSource();
 
diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.java
index db86dd4e3689..d47fa28c0ea4 100644
--- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.java
+++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2019 the original author 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 Connection getConnection() throws SQLException {
 					public void shutdown() {
 						// Do nothing - a Spring-managed DataSource has its own lifecycle.
 					}
-					/* Quartz 2.2 initialize method */
+					@Override
 					public void initialize() {
 						// Do nothing - a Spring-managed DataSource has its own lifecycle.
 					}
@@ -138,7 +138,7 @@ public Connection getConnection() throws SQLException {
 					public void shutdown() {
 						// Do nothing - a Spring-managed DataSource has its own lifecycle.
 					}
-					/* Quartz 2.2 initialize method */
+					@Override
 					public void initialize() {
 						// Do nothing - a Spring-managed DataSource has its own lifecycle.
 					}
diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/ResourceLoaderClassLoadHelper.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/ResourceLoaderClassLoadHelper.java
index 63cbb800bfab..996a598460d2 100644
--- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/ResourceLoaderClassLoadHelper.java
+++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/ResourceLoaderClassLoadHelper.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2017 the original author or authors.
+ * Copyright 2002-2019 the original author 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,6 +82,7 @@ public Class loadClass(String name) throws ClassNotFoundException {
 	}
 
 	@SuppressWarnings("unchecked")
+	@Override
 	public  Class loadClass(String name, Class clazz) throws ClassNotFoundException {
 		return (Class) loadClass(name);
 	}
diff --git a/spring-context/src/main/java/org/springframework/context/support/GenericGroovyApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/GenericGroovyApplicationContext.java
index 79950d445769..8e1f74ba45d8 100644
--- a/spring-context/src/main/java/org/springframework/context/support/GenericGroovyApplicationContext.java
+++ b/spring-context/src/main/java/org/springframework/context/support/GenericGroovyApplicationContext.java
@@ -225,18 +225,22 @@ public void load(Class relativeClass, String... resourceNames) {
 
 	// Implementation of the GroovyObject interface
 
+	@Override
 	public void setMetaClass(MetaClass metaClass) {
 		this.metaClass = metaClass;
 	}
 
+	@Override
 	public MetaClass getMetaClass() {
 		return this.metaClass;
 	}
 
+	@Override
 	public Object invokeMethod(String name, Object args) {
 		return this.metaClass.invokeMethod(this, name, args);
 	}
 
+	@Override
 	public void setProperty(String property, Object newValue) {
 		if (newValue instanceof BeanDefinition) {
 			registerBeanDefinition(property, (BeanDefinition) newValue);
@@ -246,6 +250,7 @@ public void setProperty(String property, Object newValue) {
 		}
 	}
 
+	@Override
 	@Nullable
 	public Object getProperty(String property) {
 		if (containsBean(property)) {
diff --git a/spring-core/spring-core.gradle b/spring-core/spring-core.gradle
index bda90aad8276..aaf884b1bf29 100644
--- a/spring-core/spring-core.gradle
+++ b/spring-core/spring-core.gradle
@@ -83,7 +83,7 @@ dependencies {
 	testCompile("com.google.code.findbugs:jsr305:3.0.2")
 	testCompile("org.xmlunit:xmlunit-matchers:2.6.2")
 	testCompile("javax.xml.bind:jaxb-api:2.3.1")
-	testCompile("com.fasterxml.woodstox:woodstox-core:5.2.0") {
+	testCompile("com.fasterxml.woodstox:woodstox-core:5.3.0") {
 		exclude group: "stax", module: "stax-api"
 	}
 }
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 c1a21478faf3..7f1055d1a282 100644
--- a/spring-core/src/main/java/org/springframework/util/StringUtils.java
+++ b/spring-core/src/main/java/org/springframework/util/StringUtils.java
@@ -417,14 +417,14 @@ public static String replace(String inString, String oldPattern, @Nullable Strin
 		int pos = 0;  // our position in the old string
 		int patLen = oldPattern.length();
 		while (index >= 0) {
-			sb.append(inString.substring(pos, index));
+			sb.append(inString, pos, index);
 			sb.append(newPattern);
 			pos = index + patLen;
 			index = inString.indexOf(oldPattern, pos);
 		}
 
 		// append any characters to the right of a match
-		sb.append(inString.substring(pos));
+		sb.append(inString, pos, inString.length());
 		return sb.toString();
 	}
 
diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java
index 0ca70fac7419..ea719a1e2e56 100644
--- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java
+++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java
@@ -488,6 +488,20 @@ public void getMergedAnnotationWithImplicitAliasesInMetaAnnotationOnComposedAnno
 		assertTrue(isAnnotated(element, name));
 	}
 
+	@Test
+	public void getMergedAnnotationWithImplicitAliasesWithDefaultsInMetaAnnotationOnComposedAnnotation() {
+		Class element = ImplicitAliasesWithDefaultsClass.class;
+		String name = AliasesWithDefaults.class.getName();
+		AliasesWithDefaults annotation = getMergedAnnotation(element, AliasesWithDefaults.class);
+
+		assertNotNull("Should find @AliasesWithDefaults on " + element.getSimpleName(), annotation);
+		assertEquals("a1", "ImplicitAliasesWithDefaults", annotation.a1());
+		assertEquals("a2", "ImplicitAliasesWithDefaults", annotation.a2());
+
+		// Verify contracts between utility methods:
+		assertTrue(isAnnotated(element, name));
+	}
+
 	@Test
 	public void getMergedAnnotationAttributesWithInvalidConventionBasedComposedAnnotation() {
 		Class element = InvalidConventionBasedComposedContextConfigClass.class;
@@ -958,7 +972,6 @@ static class MetaCycleAnnotatedClass {
 		String[] xmlConfigFiles() default {};
 	}
 
-
 	@ContextConfig
 	@Retention(RetentionPolicy.RUNTIME)
 	@interface AliasedComposedContextConfig {
@@ -999,6 +1012,27 @@ static class MetaCycleAnnotatedClass {
 	@interface ComposedImplicitAliasesContextConfig {
 	}
 
+	@Retention(RetentionPolicy.RUNTIME)
+	@interface AliasesWithDefaults {
+
+		@AliasFor("a2")
+		String a1() default "AliasesWithDefaults";
+
+		@AliasFor("a1")
+		String a2() default "AliasesWithDefaults";
+	}
+
+	@Retention(RetentionPolicy.RUNTIME)
+	@AliasesWithDefaults
+	@interface ImplicitAliasesWithDefaults {
+
+		@AliasFor(annotation = AliasesWithDefaults.class, attribute = "a1")
+		String b1() default "ImplicitAliasesWithDefaults";
+
+		@AliasFor(annotation = AliasesWithDefaults.class, attribute = "a2")
+		String b2() default "ImplicitAliasesWithDefaults";
+	}
+
 	@ImplicitAliasesContextConfig
 	@Retention(RetentionPolicy.RUNTIME)
 	@interface TransitiveImplicitAliasesContextConfig {
@@ -1296,6 +1330,10 @@ static class ImplicitAliasesContextConfigClass2 {
 	static class ImplicitAliasesContextConfigClass3 {
 	}
 
+	@ImplicitAliasesWithDefaults
+	static class ImplicitAliasesWithDefaultsClass {
+	}
+
 	@TransitiveImplicitAliasesContextConfig(groovy = "test.groovy")
 	static class TransitiveImplicitAliasesContextConfigClass {
 	}
diff --git a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java
index eb1fdf6db700..98c688ab3421 100644
--- a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java
+++ b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java
@@ -20,6 +20,7 @@
 import java.lang.annotation.Annotation;
 import java.lang.annotation.Documented;
 import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
@@ -37,7 +38,7 @@
 import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
 import org.springframework.stereotype.Component;
 
-import static org.hamcrest.CoreMatchers.*;
+import static org.hamcrest.Matchers.*;
 import static org.junit.Assert.*;
 
 /**
@@ -70,7 +71,7 @@ public void asmAnnotationMetadata() throws Exception {
 	@Test
 	public void standardAnnotationMetadataForSubclass() {
 		AnnotationMetadata metadata = new StandardAnnotationMetadata(AnnotatedComponentSubClass.class, true);
-		doTestSubClassAnnotationInfo(metadata);
+		doTestSubClassAnnotationInfo(metadata, false);
 	}
 
 	@Test
@@ -78,10 +79,10 @@ public void asmAnnotationMetadataForSubclass() throws Exception {
 		MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory();
 		MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(AnnotatedComponentSubClass.class.getName());
 		AnnotationMetadata metadata = metadataReader.getAnnotationMetadata();
-		doTestSubClassAnnotationInfo(metadata);
+		doTestSubClassAnnotationInfo(metadata, true);
 	}
 
-	private void doTestSubClassAnnotationInfo(AnnotationMetadata metadata) {
+	private void doTestSubClassAnnotationInfo(AnnotationMetadata metadata, boolean asm) {
 		assertThat(metadata.getClassName(), is(AnnotatedComponentSubClass.class.getName()));
 		assertThat(metadata.isInterface(), is(false));
 		assertThat(metadata.isAnnotation(), is(false));
@@ -93,11 +94,26 @@ private void doTestSubClassAnnotationInfo(AnnotationMetadata metadata) {
 		assertThat(metadata.isAnnotated(Component.class.getName()), is(false));
 		assertThat(metadata.isAnnotated(Scope.class.getName()), is(false));
 		assertThat(metadata.isAnnotated(SpecialAttr.class.getName()), is(false));
+
+		if (asm) {
+			assertThat(metadata.isAnnotated(NamedComposedAnnotation.class.getName()), is(false));
+			assertThat(metadata.hasAnnotation(NamedComposedAnnotation.class.getName()), is(false));
+			assertThat(metadata.getAnnotationTypes(), is(emptyCollectionOf(String.class)));
+		}
+		else {
+			assertThat(metadata.isAnnotated(NamedComposedAnnotation.class.getName()), is(true));
+			assertThat(metadata.hasAnnotation(NamedComposedAnnotation.class.getName()), is(true));
+			assertThat(metadata.getAnnotationTypes(), containsInAnyOrder(NamedComposedAnnotation.class.getName()));
+		}
+
 		assertThat(metadata.hasAnnotation(Component.class.getName()), is(false));
 		assertThat(metadata.hasAnnotation(Scope.class.getName()), is(false));
 		assertThat(metadata.hasAnnotation(SpecialAttr.class.getName()), is(false));
-		assertThat(metadata.getAnnotationTypes().size(), is(0));
+		assertThat(metadata.hasMetaAnnotation(Component.class.getName()), is(false));
+		assertThat(metadata.hasMetaAnnotation(MetaAnnotation.class.getName()), is(false));
 		assertThat(metadata.getAnnotationAttributes(Component.class.getName()), nullValue());
+		assertThat(metadata.getAnnotationAttributes(MetaAnnotation.class.getName(), false), nullValue());
+		assertThat(metadata.getAnnotationAttributes(MetaAnnotation.class.getName(), true), nullValue());
 		assertThat(metadata.getAnnotatedMethods(DirectAnnotation.class.getName()).size(), equalTo(0));
 		assertThat(metadata.isAnnotated(IsAnnotatedAnnotation.class.getName()), equalTo(false));
 		assertThat(metadata.getAllAnnotationAttributes(DirectAnnotation.class.getName()), nullValue());
@@ -262,13 +278,18 @@ private void doTestAnnotationInfo(AnnotationMetadata metadata) {
 		assertThat(metadata.getInterfaceNames().length, is(1));
 		assertThat(metadata.getInterfaceNames()[0], is(Serializable.class.getName()));
 
+		assertThat(metadata.isAnnotated(NamedComposedAnnotation.class.getName()), is(true));
+		assertThat(metadata.isAnnotated(Component.class.getName()), is(true));
 		assertThat(metadata.hasAnnotation(Component.class.getName()), is(true));
 		assertThat(metadata.hasAnnotation(Scope.class.getName()), is(true));
 		assertThat(metadata.hasAnnotation(SpecialAttr.class.getName()), is(true));
-		assertThat(metadata.getAnnotationTypes().size(), is(6));
-		assertThat(metadata.getAnnotationTypes().contains(Component.class.getName()), is(true));
-		assertThat(metadata.getAnnotationTypes().contains(Scope.class.getName()), is(true));
-		assertThat(metadata.getAnnotationTypes().contains(SpecialAttr.class.getName()), is(true));
+		assertThat(metadata.hasAnnotation(NamedComposedAnnotation.class.getName()), is(true));
+		assertThat(metadata.getAnnotationTypes(),
+				containsInAnyOrder(Component.class.getName(), Scope.class.getName(),
+						SpecialAttr.class.getName(), DirectAnnotation.class.getName(),
+						MetaMetaAnnotation.class.getName(),
+						EnumSubclasses.class.getName(),
+						NamedComposedAnnotation.class.getName()));
 
 		AnnotationAttributes compAttrs = (AnnotationAttributes) metadata.getAnnotationAttributes(Component.class.getName());
 		assertThat(compAttrs.size(), is(1));
@@ -465,6 +486,7 @@ public enum SubclassEnum {
 	@DirectAnnotation(value = "direct", additional = "", additionalArray = {})
 	@MetaMetaAnnotation
 	@EnumSubclasses({SubclassEnum.FOO, SubclassEnum.BAR})
+	@NamedComposedAnnotation
 	private static class AnnotatedComponent implements Serializable {
 
 		@TestAutowired
@@ -545,6 +567,7 @@ public static class NamedAnnotationsClass {
 	@NamedAnnotation3(name = "name 3")
 	@Retention(RetentionPolicy.RUNTIME)
 	@Target(ElementType.TYPE)
+	@Inherited
 	public @interface NamedComposedAnnotation {
 	}
 
diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSet.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSet.java
index 8062eb0eaf85..220c8d174633 100644
--- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSet.java
+++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSet.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2019 the original author 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,6 +29,7 @@
 import java.util.Map;
 
 import org.springframework.jdbc.InvalidResultSetAccessException;
+import org.springframework.lang.Nullable;
 
 /**
  * The default implementation of Spring's {@link SqlRowSet} interface, wrapping a
@@ -160,6 +161,7 @@ public int findColumn(String columnLabel) throws InvalidResultSetAccessException
 	 * @see java.sql.ResultSet#getBigDecimal(int)
 	 */
 	@Override
+	@Nullable
 	public BigDecimal getBigDecimal(int columnIndex) throws InvalidResultSetAccessException {
 		try {
 			return this.resultSet.getBigDecimal(columnIndex);
@@ -173,6 +175,7 @@ public BigDecimal getBigDecimal(int columnIndex) throws InvalidResultSetAccessEx
 	 * @see java.sql.ResultSet#getBigDecimal(String)
 	 */
 	@Override
+	@Nullable
 	public BigDecimal getBigDecimal(String columnLabel) throws InvalidResultSetAccessException {
 		return getBigDecimal(findColumn(columnLabel));
 	}
@@ -223,6 +226,7 @@ public byte getByte(String columnLabel) throws InvalidResultSetAccessException {
 	 * @see java.sql.ResultSet#getDate(int)
 	 */
 	@Override
+	@Nullable
 	public Date getDate(int columnIndex) throws InvalidResultSetAccessException {
 		try {
 			return this.resultSet.getDate(columnIndex);
@@ -236,6 +240,7 @@ public Date getDate(int columnIndex) throws InvalidResultSetAccessException {
 	 * @see java.sql.ResultSet#getDate(String)
 	 */
 	@Override
+	@Nullable
 	public Date getDate(String columnLabel) throws InvalidResultSetAccessException {
 		return getDate(findColumn(columnLabel));
 	}
@@ -244,6 +249,7 @@ public Date getDate(String columnLabel) throws InvalidResultSetAccessException {
 	 * @see java.sql.ResultSet#getDate(int, Calendar)
 	 */
 	@Override
+	@Nullable
 	public Date getDate(int columnIndex, Calendar cal) throws InvalidResultSetAccessException {
 		try {
 			return this.resultSet.getDate(columnIndex, cal);
@@ -257,6 +263,7 @@ public Date getDate(int columnIndex, Calendar cal) throws InvalidResultSetAccess
 	 * @see java.sql.ResultSet#getDate(String, Calendar)
 	 */
 	@Override
+	@Nullable
 	public Date getDate(String columnLabel, Calendar cal) throws InvalidResultSetAccessException {
 		return getDate(findColumn(columnLabel), cal);
 	}
@@ -349,6 +356,7 @@ public long getLong(String columnLabel) throws InvalidResultSetAccessException {
 	 * @see java.sql.ResultSet#getNString(int)
 	 */
 	@Override
+	@Nullable
 	public String getNString(int columnIndex) throws InvalidResultSetAccessException {
 		try {
 			return this.resultSet.getNString(columnIndex);
@@ -362,6 +370,7 @@ public String getNString(int columnIndex) throws InvalidResultSetAccessException
 	 * @see java.sql.ResultSet#getNString(String)
 	 */
 	@Override
+	@Nullable
 	public String getNString(String columnLabel) throws InvalidResultSetAccessException {
 		return getNString(findColumn(columnLabel));
 	}
@@ -370,6 +379,7 @@ public String getNString(String columnLabel) throws InvalidResultSetAccessExcept
 	 * @see java.sql.ResultSet#getObject(int)
 	 */
 	@Override
+	@Nullable
 	public Object getObject(int columnIndex) throws InvalidResultSetAccessException {
 		try {
 			return this.resultSet.getObject(columnIndex);
@@ -383,6 +393,7 @@ public Object getObject(int columnIndex) throws InvalidResultSetAccessException
 	 * @see java.sql.ResultSet#getObject(String)
 	 */
 	@Override
+	@Nullable
 	public Object getObject(String columnLabel) throws InvalidResultSetAccessException {
 		return getObject(findColumn(columnLabel));
 	}
@@ -391,6 +402,7 @@ public Object getObject(String columnLabel) throws InvalidResultSetAccessExcepti
 	 * @see java.sql.ResultSet#getObject(int, Map)
 	 */
 	@Override
+	@Nullable
 	public Object getObject(int columnIndex, Map> map) throws InvalidResultSetAccessException {
 		try {
 			return this.resultSet.getObject(columnIndex, map);
@@ -404,6 +416,7 @@ public Object getObject(int columnIndex, Map> map) throws Inval
 	 * @see java.sql.ResultSet#getObject(String, Map)
 	 */
 	@Override
+	@Nullable
 	public Object getObject(String columnLabel, Map> map) throws InvalidResultSetAccessException {
 		return getObject(findColumn(columnLabel), map);
 	}
@@ -412,6 +425,7 @@ public Object getObject(String columnLabel, Map> map) throws In
 	 * @see java.sql.ResultSet#getObject(int, Class)
 	 */
 	@Override
+	@Nullable
 	public  T getObject(int columnIndex, Class type) throws InvalidResultSetAccessException {
 		try {
 			return this.resultSet.getObject(columnIndex, type);
@@ -425,6 +439,7 @@ public  T getObject(int columnIndex, Class type) throws InvalidResultSetAc
 	 * @see java.sql.ResultSet#getObject(String, Class)
 	 */
 	@Override
+	@Nullable
 	public  T getObject(String columnLabel, Class type) throws InvalidResultSetAccessException {
 		return getObject(findColumn(columnLabel), type);
 	}
@@ -454,6 +469,7 @@ public short getShort(String columnLabel) throws InvalidResultSetAccessException
 	 * @see java.sql.ResultSet#getString(int)
 	 */
 	@Override
+	@Nullable
 	public String getString(int columnIndex) throws InvalidResultSetAccessException {
 		try {
 			return this.resultSet.getString(columnIndex);
@@ -467,6 +483,7 @@ public String getString(int columnIndex) throws InvalidResultSetAccessException
 	 * @see java.sql.ResultSet#getString(String)
 	 */
 	@Override
+	@Nullable
 	public String getString(String columnLabel) throws InvalidResultSetAccessException {
 		return getString(findColumn(columnLabel));
 	}
@@ -475,6 +492,7 @@ public String getString(String columnLabel) throws InvalidResultSetAccessExcepti
 	 * @see java.sql.ResultSet#getTime(int)
 	 */
 	@Override
+	@Nullable
 	public Time getTime(int columnIndex) throws InvalidResultSetAccessException {
 		try {
 			return this.resultSet.getTime(columnIndex);
@@ -488,6 +506,7 @@ public Time getTime(int columnIndex) throws InvalidResultSetAccessException {
 	 * @see java.sql.ResultSet#getTime(String)
 	 */
 	@Override
+	@Nullable
 	public Time getTime(String columnLabel) throws InvalidResultSetAccessException {
 		return getTime(findColumn(columnLabel));
 	}
@@ -496,6 +515,7 @@ public Time getTime(String columnLabel) throws InvalidResultSetAccessException {
 	 * @see java.sql.ResultSet#getTime(int, Calendar)
 	 */
 	@Override
+	@Nullable
 	public Time getTime(int columnIndex, Calendar cal) throws InvalidResultSetAccessException {
 		try {
 			return this.resultSet.getTime(columnIndex, cal);
@@ -509,6 +529,7 @@ public Time getTime(int columnIndex, Calendar cal) throws InvalidResultSetAccess
 	 * @see java.sql.ResultSet#getTime(String, Calendar)
 	 */
 	@Override
+	@Nullable
 	public Time getTime(String columnLabel, Calendar cal) throws InvalidResultSetAccessException {
 		return getTime(findColumn(columnLabel), cal);
 	}
@@ -517,6 +538,7 @@ public Time getTime(String columnLabel, Calendar cal) throws InvalidResultSetAcc
 	 * @see java.sql.ResultSet#getTimestamp(int)
 	 */
 	@Override
+	@Nullable
 	public Timestamp getTimestamp(int columnIndex) throws InvalidResultSetAccessException {
 		try {
 			return this.resultSet.getTimestamp(columnIndex);
@@ -530,6 +552,7 @@ public Timestamp getTimestamp(int columnIndex) throws InvalidResultSetAccessExce
 	 * @see java.sql.ResultSet#getTimestamp(String)
 	 */
 	@Override
+	@Nullable
 	public Timestamp getTimestamp(String columnLabel) throws InvalidResultSetAccessException {
 		return getTimestamp(findColumn(columnLabel));
 	}
@@ -538,6 +561,7 @@ public Timestamp getTimestamp(String columnLabel) throws InvalidResultSetAccessE
 	 * @see java.sql.ResultSet#getTimestamp(int, Calendar)
 	 */
 	@Override
+	@Nullable
 	public Timestamp getTimestamp(int columnIndex, Calendar cal) throws InvalidResultSetAccessException {
 		try {
 			return this.resultSet.getTimestamp(columnIndex, cal);
@@ -551,6 +575,7 @@ public Timestamp getTimestamp(int columnIndex, Calendar cal) throws InvalidResul
 	 * @see java.sql.ResultSet#getTimestamp(String, Calendar)
 	 */
 	@Override
+	@Nullable
 	public Timestamp getTimestamp(String columnLabel, Calendar cal) throws InvalidResultSetAccessException {
 		return getTimestamp(findColumn(columnLabel), cal);
 	}
diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/SqlRowSet.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/SqlRowSet.java
index c7c3bef1f49f..f0257f7b0e78 100644
--- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/SqlRowSet.java
+++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/SqlRowSet.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2019 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@
 import java.util.Map;
 
 import org.springframework.jdbc.InvalidResultSetAccessException;
+import org.springframework.lang.Nullable;
 
 /**
  * Mirror interface for {@link javax.sql.RowSet}, representing a disconnected variant of
@@ -74,6 +75,7 @@ public interface SqlRowSet extends Serializable {
 	 * @return an BigDecimal object representing the column value
 	 * @see java.sql.ResultSet#getBigDecimal(int)
 	 */
+	@Nullable
 	BigDecimal getBigDecimal(int columnIndex) throws InvalidResultSetAccessException;
 
 	/**
@@ -82,6 +84,7 @@ public interface SqlRowSet extends Serializable {
 	 * @return an BigDecimal object representing the column value
 	 * @see java.sql.ResultSet#getBigDecimal(String)
 	 */
+	@Nullable
 	BigDecimal getBigDecimal(String columnLabel) throws InvalidResultSetAccessException;
 
 	/**
@@ -122,6 +125,7 @@ public interface SqlRowSet extends Serializable {
 	 * @return a Date object representing the column value
 	 * @see java.sql.ResultSet#getDate(int)
 	 */
+	@Nullable
 	Date getDate(int columnIndex) throws InvalidResultSetAccessException;
 
 	/**
@@ -130,6 +134,7 @@ public interface SqlRowSet extends Serializable {
 	 * @return a Date object representing the column value
 	 * @see java.sql.ResultSet#getDate(String)
 	 */
+	@Nullable
 	Date getDate(String columnLabel) throws InvalidResultSetAccessException;
 
 	/**
@@ -139,6 +144,7 @@ public interface SqlRowSet extends Serializable {
 	 * @return a Date object representing the column value
 	 * @see java.sql.ResultSet#getDate(int, Calendar)
 	 */
+	@Nullable
 	Date getDate(int columnIndex, Calendar cal) throws InvalidResultSetAccessException;
 
 	/**
@@ -148,6 +154,7 @@ public interface SqlRowSet extends Serializable {
 	 * @return a Date object representing the column value
 	 * @see java.sql.ResultSet#getDate(String, Calendar)
 	 */
+	@Nullable
 	Date getDate(String columnLabel, Calendar cal) throws InvalidResultSetAccessException;
 
 	/**
@@ -222,6 +229,7 @@ public interface SqlRowSet extends Serializable {
 	 * @since 4.1.3
 	 * @see java.sql.ResultSet#getNString(int)
 	 */
+	@Nullable
 	String getNString(int columnIndex) throws InvalidResultSetAccessException;
 
 	/**
@@ -232,6 +240,7 @@ public interface SqlRowSet extends Serializable {
 	 * @since 4.1.3
 	 * @see java.sql.ResultSet#getNString(String)
 	 */
+	@Nullable
 	String getNString(String columnLabel) throws InvalidResultSetAccessException;
 
 	/**
@@ -240,6 +249,7 @@ public interface SqlRowSet extends Serializable {
 	 * @return a Object representing the column value
 	 * @see java.sql.ResultSet#getObject(int)
 	 */
+	@Nullable
 	Object getObject(int columnIndex) throws InvalidResultSetAccessException;
 
 	/**
@@ -248,6 +258,7 @@ public interface SqlRowSet extends Serializable {
 	 * @return a Object representing the column value
 	 * @see java.sql.ResultSet#getObject(String)
 	 */
+	@Nullable
 	Object getObject(String columnLabel) throws InvalidResultSetAccessException;
 
 	/**
@@ -257,6 +268,7 @@ public interface SqlRowSet extends Serializable {
 	 * @return a Object representing the column value
 	 * @see java.sql.ResultSet#getObject(int, Map)
 	 */
+	@Nullable
 	Object getObject(int columnIndex,  Map> map) throws InvalidResultSetAccessException;
 
 	/**
@@ -266,6 +278,7 @@ public interface SqlRowSet extends Serializable {
 	 * @return a Object representing the column value
 	 * @see java.sql.ResultSet#getObject(String, Map)
 	 */
+	@Nullable
 	Object getObject(String columnLabel,  Map> map) throws InvalidResultSetAccessException;
 
 	/**
@@ -273,9 +286,10 @@ public interface SqlRowSet extends Serializable {
 	 * @param columnIndex the column index
 	 * @param type the Java type to convert the designated column to
 	 * @return a Object representing the column value
-	 * @see java.sql.ResultSet#getObject(int)
 	 * @since 4.1.3
+	 * @see java.sql.ResultSet#getObject(int, Class)
 	 */
+	@Nullable
 	 T getObject(int columnIndex, Class type) throws InvalidResultSetAccessException;
 
 	/**
@@ -283,9 +297,10 @@ public interface SqlRowSet extends Serializable {
 	 * @param columnLabel the column label
 	 * @param type the Java type to convert the designated column to
 	 * @return a Object representing the column value
-	 * @see java.sql.ResultSet#getObject(int)
 	 * @since 4.1.3
+	 * @see java.sql.ResultSet#getObject(String, Class)
 	 */
+	@Nullable
 	 T getObject(String columnLabel, Class type) throws InvalidResultSetAccessException;
 
 	/**
@@ -310,6 +325,7 @@ public interface SqlRowSet extends Serializable {
 	 * @return a String representing the column value
 	 * @see java.sql.ResultSet#getString(int)
 	 */
+	@Nullable
 	String getString(int columnIndex) throws InvalidResultSetAccessException;
 
 	/**
@@ -318,6 +334,7 @@ public interface SqlRowSet extends Serializable {
 	 * @return a String representing the column value
 	 * @see java.sql.ResultSet#getString(String)
 	 */
+	@Nullable
 	String getString(String columnLabel) throws InvalidResultSetAccessException;
 
 	/**
@@ -326,6 +343,7 @@ public interface SqlRowSet extends Serializable {
 	 * @return a Time object representing the column value
 	 * @see java.sql.ResultSet#getTime(int)
 	 */
+	@Nullable
 	Time getTime(int columnIndex) throws InvalidResultSetAccessException;
 
 	/**
@@ -334,6 +352,7 @@ public interface SqlRowSet extends Serializable {
 	 * @return a Time object representing the column value
 	 * @see java.sql.ResultSet#getTime(String)
 	 */
+	@Nullable
 	Time getTime(String columnLabel) throws InvalidResultSetAccessException;
 
 	/**
@@ -343,6 +362,7 @@ public interface SqlRowSet extends Serializable {
 	 * @return a Time object representing the column value
 	 * @see java.sql.ResultSet#getTime(int, Calendar)
 	 */
+	@Nullable
 	Time getTime(int columnIndex, Calendar cal) throws InvalidResultSetAccessException;
 
 	/**
@@ -352,6 +372,7 @@ public interface SqlRowSet extends Serializable {
 	 * @return a Time object representing the column value
 	 * @see java.sql.ResultSet#getTime(String, Calendar)
 	 */
+	@Nullable
 	Time getTime(String columnLabel, Calendar cal) throws InvalidResultSetAccessException;
 
 	/**
@@ -360,6 +381,7 @@ public interface SqlRowSet extends Serializable {
 	 * @return a Timestamp object representing the column value
 	 * @see java.sql.ResultSet#getTimestamp(int)
 	 */
+	@Nullable
 	Timestamp getTimestamp(int columnIndex) throws InvalidResultSetAccessException;
 
 	/**
@@ -368,6 +390,7 @@ public interface SqlRowSet extends Serializable {
 	 * @return a Timestamp object representing the column value
 	 * @see java.sql.ResultSet#getTimestamp(String)
 	 */
+	@Nullable
 	Timestamp getTimestamp(String columnLabel) throws InvalidResultSetAccessException;
 
 	/**
@@ -377,6 +400,7 @@ public interface SqlRowSet extends Serializable {
 	 * @return a Timestamp object representing the column value
 	 * @see java.sql.ResultSet#getTimestamp(int, Calendar)
 	 */
+	@Nullable
 	Timestamp getTimestamp(int columnIndex, Calendar cal) throws InvalidResultSetAccessException;
 
 	/**
@@ -386,6 +410,7 @@ public interface SqlRowSet extends Serializable {
 	 * @return a Timestamp object representing the column value
 	 * @see java.sql.ResultSet#getTimestamp(String, Calendar)
 	 */
+	@Nullable
 	Timestamp getTimestamp(String columnLabel, Calendar cal) throws InvalidResultSetAccessException;
 
 
diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/CachedMessageProducer.java b/spring-jms/src/main/java/org/springframework/jms/connection/CachedMessageProducer.java
index e7960c2e5e4c..daae35af0aa9 100644
--- a/spring-jms/src/main/java/org/springframework/jms/connection/CachedMessageProducer.java
+++ b/spring-jms/src/main/java/org/springframework/jms/connection/CachedMessageProducer.java
@@ -89,6 +89,7 @@ public boolean getDisableMessageTimestamp() throws JMSException {
 		return this.target.getDisableMessageTimestamp();
 	}
 
+	@Override
 	public void setDeliveryDelay(long deliveryDelay) throws JMSException {
 		if (this.originalDeliveryDelay == null) {
 			this.originalDeliveryDelay = this.target.getDeliveryDelay();
@@ -96,6 +97,7 @@ public void setDeliveryDelay(long deliveryDelay) throws JMSException {
 		this.target.setDeliveryDelay(deliveryDelay);
 	}
 
+	@Override
 	public long getDeliveryDelay() throws JMSException {
 		return this.target.getDeliveryDelay();
 	}
diff --git a/spring-test/spring-test.gradle b/spring-test/spring-test.gradle
index c5345278c39d..621ab4dec425 100644
--- a/spring-test/spring-test.gradle
+++ b/spring-test/spring-test.gradle
@@ -61,7 +61,7 @@ dependencies {
 	testCompile("javax.interceptor:javax.interceptor-api:1.2.2")
 	testCompile("javax.mail:javax.mail-api:1.6.2")
 	testCompile("org.hibernate:hibernate-core:5.3.12.Final")
-	testCompile("org.hibernate:hibernate-validator:6.0.17.Final")
+	testCompile("org.hibernate:hibernate-validator:6.0.18.Final")
 	// Enable use of the JUnit Platform Runner
 	testCompile("org.junit.platform:junit-platform-runner")
 	testCompile("org.junit.jupiter:junit-jupiter-params")
diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java
index f314b4455daf..82006d37b255 100644
--- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java
+++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2019 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -142,6 +142,12 @@ public WebTestClient.Builder exchangeStrategies(ExchangeStrategies strategies) {
 		return this;
 	}
 
+	@Override
+	public WebTestClient.Builder exchangeStrategies(Consumer configurer) {
+		this.webClientBuilder.exchangeStrategies(configurer);
+		return this;
+	}
+
 	@Override
 	public WebTestClient.Builder responseTimeout(Duration timeout) {
 		this.responseTimeout = timeout;
diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java
index cc2e8ab96fc8..e8b109cbc014 100644
--- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java
+++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java
@@ -77,6 +77,7 @@
  * and Spring Kotlin extensions to perform integration tests on an embedded WebFlux server.
  *
  * @author Rossen Stoyanchev
+ * @author Brian Clozel
  * @since 5.0
  * @see StatusAssertions
  * @see HeaderAssertions
@@ -436,11 +437,25 @@ interface Builder {
 
 		/**
 		 * Configure the {@link ExchangeStrategies} to use.
-		 * 

By default {@link ExchangeStrategies#withDefaults()} is used. + *

Note that in a scenario where the builder is configured by + * multiple parties, it is preferable to use + * {@link #exchangeStrategies(Consumer)} in order to customize the same + * {@code ExchangeStrategies}. This method here sets the strategies that + * everyone else then can customize. + *

By default this is {@link ExchangeStrategies#withDefaults()}. * @param strategies the strategies to use */ Builder exchangeStrategies(ExchangeStrategies strategies); + /** + * Customize the strategies configured via + * {@link #exchangeStrategies(ExchangeStrategies)}. This method is + * designed for use in scenarios where multiple parties wish to update + * the {@code ExchangeStrategies}. + * @since 5.1.12 + */ + Builder exchangeStrategies(Consumer configurer); + /** * Max amount of time to wait for responses. *

By default 5 seconds. @@ -877,7 +892,7 @@ interface BodyContentSpec { * @since 5.1 * @see #xpath(String, Map, Object...) */ - default XpathAssertions xpath(String expression, Object... args){ + default XpathAssertions xpath(String expression, Object... args) { return xpath(expression, null, args); } @@ -891,7 +906,7 @@ default XpathAssertions xpath(String expression, Object... args){ * @param args arguments to parameterize the expression * @since 5.1 */ - XpathAssertions xpath(String expression, @Nullable Map namespaces, Object... args); + XpathAssertions xpath(String expression, @Nullable Map namespaces, Object... args); /** * Assert the response body content with the given {@link Consumer}. diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/ControllerAdviceIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/ControllerAdviceIntegrationTests.java new file mode 100644 index 000000000000..982e5e136429 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/ControllerAdviceIntegrationTests.java @@ -0,0 +1,135 @@ +/* + * Copyright 2002-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.web.servlet.samples.spr; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Controller; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.annotation.RequestScope; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import static org.junit.Assert.assertEquals; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; + +/** + * Integration tests for {@link ControllerAdvice @ControllerAdvice}. + * + *

Introduced in conjunction with + * gh-24017. + * + * @author Sam Brannen + * @since 5.1.12 + */ +@RunWith(SpringRunner.class) +@WebAppConfiguration +public class ControllerAdviceIntegrationTests { + + @Autowired + WebApplicationContext wac; + + MockMvc mockMvc; + + @Before + public void setUpMockMvc() { + this.mockMvc = webAppContextSetup(wac).build(); + } + + @Test + public void controllerAdviceIsAppliedOnlyOnce() throws Exception { + assertEquals(0, SingletonControllerAdvice.counter.get()); + assertEquals(0, RequestScopedControllerAdvice.counter.get()); + + this.mockMvc.perform(get("/test"))// + .andExpect(status().isOk())// + .andExpect(forwardedUrl("singleton:1;request-scoped:1")); + + assertEquals(1, SingletonControllerAdvice.counter.get()); + assertEquals(1, RequestScopedControllerAdvice.counter.get()); + } + + @Configuration + @EnableWebMvc + static class Config { + + @Bean + TestController testController() { + return new TestController(); + } + + @Bean + SingletonControllerAdvice singletonControllerAdvice() { + return new SingletonControllerAdvice(); + } + + @Bean + @RequestScope + RequestScopedControllerAdvice requestScopedControllerAdvice() { + return new RequestScopedControllerAdvice(); + } + } + + @ControllerAdvice + static class SingletonControllerAdvice { + + static final AtomicInteger counter = new AtomicInteger(); + + @ModelAttribute + void initModel(Model model) { + model.addAttribute("singleton", counter.incrementAndGet()); + } + } + + @ControllerAdvice + static class RequestScopedControllerAdvice { + + static final AtomicInteger counter = new AtomicInteger(); + + @ModelAttribute + void initModel(Model model) { + model.addAttribute("request-scoped", counter.incrementAndGet()); + } + } + + @Controller + static class TestController { + + @GetMapping("/test") + String get(Model model) { + return "singleton:" + model.asMap().get("singleton") + ";request-scoped:" + + model.asMap().get("request-scoped"); + } + } + +} diff --git a/spring-web/spring-web.gradle b/spring-web/spring-web.gradle index 390f697127d9..d735f4a333e2 100644 --- a/spring-web/spring-web.gradle +++ b/spring-web/spring-web.gradle @@ -47,7 +47,7 @@ dependencies { } optional("commons-fileupload:commons-fileupload:1.4") optional("org.synchronoss.cloud:nio-multipart-parser:1.1.0") - optional("com.fasterxml.woodstox:woodstox-core:5.2.0") { // woodstox before aalto + optional("com.fasterxml.woodstox:woodstox-core:5.3.0") { // woodstox before aalto exclude group: "stax", module: "stax-api" } optional("com.fasterxml:aalto-xml:1.1.1") diff --git a/spring-web/src/main/java/org/springframework/http/codec/ClientCodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/ClientCodecConfigurer.java index db31e97218a4..e41ec7348117 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ClientCodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/ClientCodecConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,6 +63,12 @@ public interface ClientCodecConfigurer extends CodecConfigurer { @Override ClientDefaultCodecs defaultCodecs(); + /** + * {@inheritDoc}. + */ + @Override + ClientCodecConfigurer clone(); + /** * Static factory method for a {@code ClientCodecConfigurer}. diff --git a/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java index 3d4c625b5d56..518d3fac7e12 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java @@ -17,9 +17,11 @@ package org.springframework.http.codec; import java.util.List; +import java.util.function.Consumer; import org.springframework.core.codec.Decoder; import org.springframework.core.codec.Encoder; +import org.springframework.lang.Nullable; /** * Defines a common interface for configuring either client or server HTTP @@ -87,6 +89,15 @@ public interface CodecConfigurer { */ List> getWriters(); + /** + * Create a copy of this {@link CodecConfigurer}. The returned clone has its + * own lists of default and custom codecs and generally can be configured + * independently. Keep in mind however that codec instances (if any are + * configured) are themselves not cloned. + * @since 5.1.12 + */ + CodecConfigurer clone(); + /** * Customize or replace the HTTP message readers and writers registered by @@ -204,6 +215,38 @@ interface CustomCodecs { * @param writer the writer to add */ void writer(HttpMessageWriter writer); + + /** + * Register a callback for the {@link DefaultCodecConfig configuration} + * applied to default codecs. This allows custom codecs to follow general + * guidelines applied to default ones, such as logging details and limiting + * the amount of buffered data. + * @param codecsConfigConsumer the default codecs configuration callback + * @since 5.1.12 + */ + void withDefaultCodecConfig(Consumer codecsConfigConsumer); + } + + + /** + * Common options applied to default codecs and passed in a callback to custom codecs + * so they get a chance to align their behavior on the default ones. + * @since 5.1.12 + */ + interface DefaultCodecConfig { + + /** + * Get the configured limit on the number of bytes that can be buffered whenever + * the input stream needs to be aggregated. + */ + @Nullable + Integer maxInMemorySize(); + + /** + * Whether to log form data at DEBUG level, and headers at TRACE level. + * Both may contain sensitive information. + */ + boolean isEnableLoggingRequestDetails(); } } diff --git a/spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java index 59a209ac59a7..ba8501b3b297 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java @@ -62,6 +62,12 @@ public interface ServerCodecConfigurer extends CodecConfigurer { @Override ServerDefaultCodecs defaultCodecs(); + /** + * {@inheritDoc}. + */ + @Override + ServerCodecConfigurer clone(); + /** * Static factory method for a {@code ServerCodecConfigurer}. diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java index 606f0a34aa19..9b4b28427622 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java @@ -120,13 +120,18 @@ private Flux endOfInput() { private List parseTokenBufferFlux() throws IOException { List result = new ArrayList<>(); - while (true) { + // SPR-16151: Smile data format uses null to separate documents + boolean previousNull = false; + while (!this.parser.isClosed()) { JsonToken token = this.parser.nextToken(); - // SPR-16151: Smile data format uses null to separate documents if (token == JsonToken.NOT_AVAILABLE || - (token == null && (token = this.parser.nextToken()) == null)) { + token == null && previousNull) { break; } + else if (token == null ) { // !previousNull + previousNull = true; + continue; + } updateDepth(token); if (!this.tokenizeArrayElements) { processTokenNormal(token, result); diff --git a/spring-web/src/main/java/org/springframework/http/codec/support/BaseCodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/support/BaseCodecConfigurer.java index e86ac954f9f6..a63e0be88827 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/support/BaseCodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/support/BaseCodecConfigurer.java @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; import org.springframework.core.ResolvableType; import org.springframework.core.codec.Decoder; @@ -34,13 +35,16 @@ * client and server specific variants. * * @author Rossen Stoyanchev + * @author Brian Clozel * @since 5.0 */ -class BaseCodecConfigurer implements CodecConfigurer { +abstract class BaseCodecConfigurer implements CodecConfigurer { - private final BaseDefaultCodecs defaultCodecs; + protected boolean customCodecsInitialized; - private final DefaultCustomCodecs customCodecs = new DefaultCustomCodecs(); + protected final BaseDefaultCodecs defaultCodecs; + + protected final DefaultCustomCodecs customCodecs; /** @@ -50,8 +54,25 @@ class BaseCodecConfigurer implements CodecConfigurer { BaseCodecConfigurer(BaseDefaultCodecs defaultCodecs) { Assert.notNull(defaultCodecs, "'defaultCodecs' is required"); this.defaultCodecs = defaultCodecs; + this.customCodecs = new DefaultCustomCodecs(); + } + + /** + * Create a deep copy of the given {@link BaseCodecConfigurer}. + * @since 5.1.12 + */ + protected BaseCodecConfigurer(BaseCodecConfigurer other) { + this.defaultCodecs = other.cloneDefaultCodecs(); + this.customCodecs = new DefaultCustomCodecs(other.customCodecs); } + /** + * Sub-classes should override this to create deep copy of + * {@link BaseDefaultCodecs} which can can be client or server specific. + * @since 5.1.12 + */ + protected abstract BaseDefaultCodecs cloneDefaultCodecs(); + @Override public DefaultCodecs defaultCodecs() { @@ -70,6 +91,7 @@ public CustomCodecs customCodecs() { @Override public List> getReaders() { + initializeCustomCodecs(); List> result = new ArrayList<>(); result.addAll(this.defaultCodecs.getTypedReaders()); @@ -87,6 +109,7 @@ public List> getWriters() { return getWritersInternal(false); } + /** * Internal method that returns the configured writers. * @param forMultipart whether to returns writers for general use ("false"), @@ -94,6 +117,7 @@ public List> getWriters() { * same except for the multipart writer itself. */ protected List> getWritersInternal(boolean forMultipart) { + initializeCustomCodecs(); List> result = new ArrayList<>(); result.addAll(this.defaultCodecs.getTypedWriters(forMultipart)); @@ -106,11 +130,21 @@ protected List> getWritersInternal(boolean forMultipart) { return result; } + @Override + public abstract CodecConfigurer clone(); + + private void initializeCustomCodecs() { + if(!this.customCodecsInitialized) { + this.customCodecs.configConsumers.forEach(consumer -> consumer.accept(this.defaultCodecs)); + this.customCodecsInitialized = true; + } + } + /** * Default implementation of {@code CustomCodecs}. */ - private static final class DefaultCustomCodecs implements CustomCodecs { + protected static final class DefaultCustomCodecs implements CustomCodecs { private final List> typedReaders = new ArrayList<>(); @@ -120,6 +154,21 @@ private static final class DefaultCustomCodecs implements CustomCodecs { private final List> objectWriters = new ArrayList<>(); + private final List> configConsumers = new ArrayList<>(); + + DefaultCustomCodecs() { + } + + /** + * Create a deep copy of the given {@link DefaultCustomCodecs}. + * @since 5.1.12 + */ + DefaultCustomCodecs(DefaultCustomCodecs other) { + other.typedReaders.addAll(this.typedReaders); + other.typedWriters.addAll(this.typedWriters); + other.objectReaders.addAll(this.objectReaders); + other.objectWriters.addAll(this.objectWriters); + } @Override public void decoder(Decoder decoder) { @@ -143,6 +192,10 @@ public void writer(HttpMessageWriter writer) { (canWriteObject ? this.objectWriters : this.typedWriters).add(writer); } + @Override + public void withDefaultCodecConfig(Consumer codecsConfigConsumer) { + this.configConsumers.add(codecsConfigConsumer); + } // Package private accessors... diff --git a/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java b/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java index f3034ad9354c..75ec9191dcb5 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java +++ b/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java @@ -59,7 +59,7 @@ * @author Rossen Stoyanchev * @author Sebastien Deleuze */ -class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs { +class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigurer.DefaultCodecConfig { static final boolean jackson2Present; @@ -105,6 +105,24 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs { private boolean registerDefaults = true; + BaseDefaultCodecs() { + } + + /** + * Create a deep copy of the given {@link BaseDefaultCodecs}. + */ + protected BaseDefaultCodecs(BaseDefaultCodecs other) { + this.jackson2JsonDecoder = other.jackson2JsonDecoder; + this.jackson2JsonEncoder = other.jackson2JsonEncoder; + this.protobufDecoder = other.protobufDecoder; + this.protobufEncoder = other.protobufEncoder; + this.jaxb2Decoder = other.jaxb2Decoder; + this.jaxb2Encoder = other.jaxb2Encoder; + this.maxInMemorySize = other.maxInMemorySize; + this.enableLoggingRequestDetails = other.enableLoggingRequestDetails; + this.registerDefaults = other.registerDefaults; + } + @Override public void jackson2JsonDecoder(Decoder decoder) { this.jackson2JsonDecoder = decoder; @@ -140,8 +158,9 @@ public void maxInMemorySize(int byteCount) { this.maxInMemorySize = byteCount; } + @Override @Nullable - protected Integer maxInMemorySize() { + public Integer maxInMemorySize() { return this.maxInMemorySize; } @@ -150,7 +169,8 @@ public void enableLoggingRequestDetails(boolean enable) { this.enableLoggingRequestDetails = enable; } - protected boolean isEnableLoggingRequestDetails() { + @Override + public boolean isEnableLoggingRequestDetails() { return this.enableLoggingRequestDetails; } diff --git a/spring-web/src/main/java/org/springframework/http/codec/support/ClientDefaultCodecsImpl.java b/spring-web/src/main/java/org/springframework/http/codec/support/ClientDefaultCodecsImpl.java index 9f578b7320ab..e764cb969612 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/support/ClientDefaultCodecsImpl.java +++ b/spring-web/src/main/java/org/springframework/http/codec/support/ClientDefaultCodecsImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 the original author 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,6 +49,17 @@ class ClientDefaultCodecsImpl extends BaseDefaultCodecs implements ClientCodecCo private Supplier>> partWritersSupplier; + ClientDefaultCodecsImpl() { + } + + ClientDefaultCodecsImpl(ClientDefaultCodecsImpl other) { + super(other); + this.multipartCodecs = new DefaultMultipartCodecs(other.multipartCodecs); + this.sseDecoder = other.sseDecoder; + this.partWritersSupplier = other.partWritersSupplier; + } + + /** * Set a supplier for part writers to use when * {@link #multipartCodecs()} are not explicitly configured. @@ -73,6 +84,14 @@ public void serverSentEventDecoder(Decoder decoder) { this.sseDecoder = decoder; } + @Override + public ClientDefaultCodecsImpl clone() { + ClientDefaultCodecsImpl codecs = new ClientDefaultCodecsImpl(); + codecs.multipartCodecs = this.multipartCodecs; + codecs.sseDecoder = this.sseDecoder; + codecs.partWritersSupplier = this.partWritersSupplier; + return codecs; + } @Override protected void extendObjectReaders(List> objectReaders) { @@ -116,6 +135,17 @@ private static class DefaultMultipartCodecs implements ClientCodecConfigurer.Mul private final List> writers = new ArrayList<>(); + + DefaultMultipartCodecs() { + } + + DefaultMultipartCodecs(@Nullable DefaultMultipartCodecs other) { + if (other != null) { + this.writers.addAll(other.writers); + } + } + + @Override public ClientCodecConfigurer.MultipartCodecs encoder(Encoder encoder) { writer(new EncoderHttpMessageWriter<>(encoder)); diff --git a/spring-web/src/main/java/org/springframework/http/codec/support/DefaultClientCodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/support/DefaultClientCodecConfigurer.java index 9875ded1b98d..737282eecd5e 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/support/DefaultClientCodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/support/DefaultClientCodecConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 the original author 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,14 +26,30 @@ */ public class DefaultClientCodecConfigurer extends BaseCodecConfigurer implements ClientCodecConfigurer { + public DefaultClientCodecConfigurer() { super(new ClientDefaultCodecsImpl()); ((ClientDefaultCodecsImpl) defaultCodecs()).setPartWritersSupplier(() -> getWritersInternal(true)); } + private DefaultClientCodecConfigurer(DefaultClientCodecConfigurer other) { + super(other); + } + + @Override public ClientDefaultCodecs defaultCodecs() { return (ClientDefaultCodecs) super.defaultCodecs(); } + @Override + public DefaultClientCodecConfigurer clone() { + return new DefaultClientCodecConfigurer(this); + } + + @Override + protected BaseDefaultCodecs cloneDefaultCodecs() { + return new ClientDefaultCodecsImpl((ClientDefaultCodecsImpl) defaultCodecs()); + } + } diff --git a/spring-web/src/main/java/org/springframework/http/codec/support/DefaultServerCodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/support/DefaultServerCodecConfigurer.java index 2623d5a7f7b2..661d45d66693 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/support/DefaultServerCodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/support/DefaultServerCodecConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 the original author 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,13 +26,28 @@ */ public class DefaultServerCodecConfigurer extends BaseCodecConfigurer implements ServerCodecConfigurer { + public DefaultServerCodecConfigurer() { super(new ServerDefaultCodecsImpl()); } + private DefaultServerCodecConfigurer(BaseCodecConfigurer other) { + super(other); + } + + @Override public ServerDefaultCodecs defaultCodecs() { return (ServerDefaultCodecs) super.defaultCodecs(); } + @Override + public DefaultServerCodecConfigurer clone() { + return new DefaultServerCodecConfigurer(this); + } + + @Override + protected BaseDefaultCodecs cloneDefaultCodecs() { + return new ServerDefaultCodecsImpl((ServerDefaultCodecsImpl) defaultCodecs()); + } } diff --git a/spring-web/src/main/java/org/springframework/http/codec/support/ServerDefaultCodecsImpl.java b/spring-web/src/main/java/org/springframework/http/codec/support/ServerDefaultCodecsImpl.java index 37e924cd7e91..1d997c3777b1 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/support/ServerDefaultCodecsImpl.java +++ b/spring-web/src/main/java/org/springframework/http/codec/support/ServerDefaultCodecsImpl.java @@ -46,6 +46,16 @@ class ServerDefaultCodecsImpl extends BaseDefaultCodecs implements ServerCodecCo private Encoder sseEncoder; + ServerDefaultCodecsImpl() { + } + + ServerDefaultCodecsImpl(ServerDefaultCodecsImpl other) { + super(other); + this.multipartReader = other.multipartReader; + this.sseEncoder = other.sseEncoder; + } + + @Override public void multipartReader(HttpMessageReader reader) { this.multipartReader = reader; diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java index 4f05d5e1e091..3f37a9558068 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java @@ -62,8 +62,17 @@ public abstract class AbstractListenerWriteProcessor implements Processor void onNext(AbstractListenerWriteProcessor processor, T data) { } @Override public void onComplete(AbstractListenerWriteProcessor processor) { - processor.changeStateToComplete(this); + processor.readyToCompleteAfterLastWrite = true; + processor.changeStateToReceived(this); } }, @@ -352,7 +362,10 @@ public void onComplete(AbstractListenerWriteProcessor processor) { @SuppressWarnings("deprecation") @Override public void onWritePossible(AbstractListenerWriteProcessor processor) { - if (processor.changeState(this, WRITING)) { + if (processor.readyToCompleteAfterLastWrite) { + processor.changeStateToComplete(RECEIVED); + } + else if (processor.changeState(this, WRITING)) { T data = processor.currentData; Assert.state(data != null, "No data"); try { @@ -360,7 +373,8 @@ public void onWritePossible(AbstractListenerWriteProcessor processor) { if (processor.changeState(WRITING, REQUESTED)) { processor.currentData = null; if (processor.subscriberCompleted) { - processor.changeStateToComplete(REQUESTED); + processor.readyToCompleteAfterLastWrite = true; + processor.changeStateToReceived(REQUESTED); } else { processor.writingPaused(); diff --git a/spring-web/src/main/java/org/springframework/web/context/support/GroovyWebApplicationContext.java b/spring-web/src/main/java/org/springframework/web/context/support/GroovyWebApplicationContext.java index ca344393beaa..25610fd6a3d1 100644 --- a/spring-web/src/main/java/org/springframework/web/context/support/GroovyWebApplicationContext.java +++ b/spring-web/src/main/java/org/springframework/web/context/support/GroovyWebApplicationContext.java @@ -154,22 +154,27 @@ protected String[] getDefaultConfigLocations() { // Implementation of the GroovyObject interface + @Override public void setMetaClass(MetaClass metaClass) { this.metaClass = metaClass; } + @Override public MetaClass getMetaClass() { return this.metaClass; } + @Override public Object invokeMethod(String name, Object args) { return this.metaClass.invokeMethod(this, name, args); } + @Override public void setProperty(String property, Object newValue) { this.metaClass.setProperty(this, property, newValue); } + @Override @Nullable public Object getProperty(String property) { if (containsBean(property)) { diff --git a/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java b/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java index af12b15d5be0..ddf67af093ba 100644 --- a/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java +++ b/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 the original author 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 java.util.List; import java.util.stream.Collectors; +import org.springframework.aop.scope.ScopedProxyUtils; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.context.ApplicationContext; @@ -42,6 +43,7 @@ * @author Rossen Stoyanchev * @author Brian Clozel * @author Juergen Hoeller + * @author Sam Brannen * @since 3.2 */ public class ControllerAdviceBean implements Ordered { @@ -187,6 +189,7 @@ public String toString() { */ public static List findAnnotatedBeans(ApplicationContext context) { return Arrays.stream(BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, Object.class)) + .filter(name -> !ScopedProxyUtils.isScopedTarget(name)) .filter(name -> context.findAnnotationOnBean(name, ControllerAdvice.class) != null) .map(name -> new ControllerAdviceBean(name, context)) .collect(Collectors.toList()); diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java index 5c08550c07e2..aa25442cb00e 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java @@ -210,8 +210,8 @@ public void testLimit() { .expectNext(expected) .verifyComplete(); - StepVerifier.create(decode(source, false, maxInMemorySize - 1)) - .expectError(DataBufferLimitException.class); + StepVerifier.create(decode(source, false, maxInMemorySize - 2)) + .verifyError(DataBufferLimitException.class); } @Test diff --git a/spring-web/src/test/java/org/springframework/http/codec/support/ClientCodecConfigurerTests.java b/spring-web/src/test/java/org/springframework/http/codec/support/ClientCodecConfigurerTests.java index afa5f4cec37b..1b98450036e5 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/support/ClientCodecConfigurerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/support/ClientCodecConfigurerTests.java @@ -22,6 +22,7 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; import org.junit.Test; import reactor.core.publisher.Flux; @@ -59,7 +60,11 @@ import org.springframework.http.codec.xml.Jaxb2XmlEncoder; import org.springframework.util.MimeTypeUtils; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; import static org.springframework.core.ResolvableType.forClass; /** @@ -122,6 +127,47 @@ public void jackson2EncoderOverride() { .filter(e -> e == decoder).orElse(null)); } + @Test + public void cloneConfigurer() { + ClientCodecConfigurer clone = this.configurer.clone(); + + Jackson2JsonDecoder jackson2Decoder = new Jackson2JsonDecoder(); + clone.defaultCodecs().serverSentEventDecoder(jackson2Decoder); + clone.defaultCodecs().multipartCodecs().encoder(new Jackson2SmileEncoder()); + clone.defaultCodecs().multipartCodecs().writer(new ResourceHttpMessageWriter()); + + // Clone has the customizations + + Decoder sseDecoder = clone.getReaders().stream() + .filter(reader -> reader instanceof ServerSentEventHttpMessageReader) + .map(reader -> ((ServerSentEventHttpMessageReader) reader).getDecoder()) + .findFirst() + .get(); + + List> multipartWriters = clone.getWriters().stream() + .filter(writer -> writer instanceof MultipartHttpMessageWriter) + .flatMap(writer -> ((MultipartHttpMessageWriter) writer).getPartWriters().stream()) + .collect(Collectors.toList()); + + assertSame(jackson2Decoder, sseDecoder); + assertEquals(2, multipartWriters.size()); + + // Original does not have the customizations + + sseDecoder = this.configurer.getReaders().stream() + .filter(reader -> reader instanceof ServerSentEventHttpMessageReader) + .map(reader -> ((ServerSentEventHttpMessageReader) reader).getDecoder()) + .findFirst() + .get(); + + multipartWriters = this.configurer.getWriters().stream() + .filter(writer -> writer instanceof MultipartHttpMessageWriter) + .flatMap(writer -> ((MultipartHttpMessageWriter) writer).getPartWriters().stream()) + .collect(Collectors.toList()); + + assertNotSame(jackson2Decoder, sseDecoder); + assertEquals(10, multipartWriters.size()); + } private Decoder getNextDecoder(List> readers) { HttpMessageReader reader = readers.get(this.index.getAndIncrement()); diff --git a/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java b/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java index 48e20a9074da..02f41655f46e 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java @@ -16,8 +16,11 @@ package org.springframework.http.codec.support; +import java.util.Arrays; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; import com.google.protobuf.ExtensionRegistry; import org.junit.Test; @@ -42,6 +45,8 @@ import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.HttpMessageWriter; import org.springframework.http.codec.ResourceHttpMessageWriter; +import org.springframework.http.codec.ServerSentEventHttpMessageReader; +import org.springframework.http.codec.ServerSentEventHttpMessageWriter; import org.springframework.http.codec.json.Jackson2JsonDecoder; import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.codec.json.Jackson2SmileDecoder; @@ -267,6 +272,83 @@ public void encoderDecoderOverrides() { assertEncoderInstance(jaxb2Encoder); } + @Test + public void cloneCustomCodecs() { + this.configurer.registerDefaults(false); + CodecConfigurer clone = this.configurer.clone(); + + clone.customCodecs().encoder(new Jackson2JsonEncoder()); + clone.customCodecs().decoder(new Jackson2JsonDecoder()); + clone.customCodecs().reader(new ServerSentEventHttpMessageReader()); + clone.customCodecs().writer(new ServerSentEventHttpMessageWriter()); + + assertEquals(0, this.configurer.getReaders().size()); + assertEquals(0, this.configurer.getWriters().size()); + assertEquals(2, clone.getReaders().size()); + assertEquals(2, clone.getWriters().size()); + } + + @Test + public void cloneDefaultCodecs() { + CodecConfigurer clone = this.configurer.clone(); + + Jackson2JsonDecoder jacksonDecoder = new Jackson2JsonDecoder(); + Jackson2JsonEncoder jacksonEncoder = new Jackson2JsonEncoder(); + Jaxb2XmlDecoder jaxb2Decoder = new Jaxb2XmlDecoder(); + Jaxb2XmlEncoder jaxb2Encoder = new Jaxb2XmlEncoder(); + ProtobufDecoder protoDecoder = new ProtobufDecoder(); + ProtobufEncoder protoEncoder = new ProtobufEncoder(); + + clone.defaultCodecs().jackson2JsonDecoder(jacksonDecoder); + clone.defaultCodecs().jackson2JsonEncoder(jacksonEncoder); + clone.defaultCodecs().jaxb2Decoder(jaxb2Decoder); + clone.defaultCodecs().jaxb2Encoder(jaxb2Encoder); + clone.defaultCodecs().protobufDecoder(protoDecoder); + clone.defaultCodecs().protobufEncoder(protoEncoder); + + // Clone has the customized the customizations + + List> decoders = clone.getReaders().stream() + .filter(reader -> reader instanceof DecoderHttpMessageReader) + .map(reader -> ((DecoderHttpMessageReader) reader).getDecoder()) + .collect(Collectors.toList()); + + List> encoders = clone.getWriters().stream() + .filter(writer -> writer instanceof EncoderHttpMessageWriter) + .map(reader -> ((EncoderHttpMessageWriter) reader).getEncoder()) + .collect(Collectors.toList()); + + assertTrue(decoders.containsAll(Arrays.asList(jacksonDecoder, jaxb2Decoder, protoDecoder))); + assertTrue(encoders.containsAll(Arrays.asList(jacksonEncoder, jaxb2Encoder, protoEncoder))); + + // Original does not have the customizations + + decoders = this.configurer.getReaders().stream() + .filter(reader -> reader instanceof DecoderHttpMessageReader) + .map(reader -> ((DecoderHttpMessageReader) reader).getDecoder()) + .collect(Collectors.toList()); + + encoders = this.configurer.getWriters().stream() + .filter(writer -> writer instanceof EncoderHttpMessageWriter) + .map(reader -> ((EncoderHttpMessageWriter) reader).getEncoder()) + .collect(Collectors.toList()); + + assertFalse(decoders.containsAll(Arrays.asList(jacksonDecoder, jaxb2Decoder, protoDecoder))); + assertFalse(encoders.containsAll(Arrays.asList(jacksonEncoder, jaxb2Encoder, protoEncoder))); + } + + @Test + public void withDefaultCodecConfig() { + AtomicBoolean callbackCalled = new AtomicBoolean(false); + this.configurer.defaultCodecs().enableLoggingRequestDetails(true); + this.configurer.customCodecs().withDefaultCodecConfig(config -> { + assertTrue(config.isEnableLoggingRequestDetails()); + callbackCalled.compareAndSet(false, true); + }); + this.configurer.getReaders(); + assertTrue(callbackCalled.get()); + } + private Decoder getNextDecoder(List> readers) { HttpMessageReader reader = readers.get(this.index.getAndIncrement()); assertEquals(DecoderHttpMessageReader.class, reader.getClass()); @@ -313,10 +395,21 @@ private void assertEncoderInstance(Encoder encoder) { private static class TestCodecConfigurer extends BaseCodecConfigurer { TestCodecConfigurer() { - super(new TestDefaultCodecs()); + super(new BaseDefaultCodecs()); + } + + TestCodecConfigurer(TestCodecConfigurer other) { + super(other); + } + + @Override + protected BaseDefaultCodecs cloneDefaultCodecs() { + return new BaseDefaultCodecs((BaseDefaultCodecs) defaultCodecs()); } - private static class TestDefaultCodecs extends BaseDefaultCodecs { + @Override + public CodecConfigurer clone() { + return new TestCodecConfigurer(this); } } diff --git a/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java b/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java index b36cdd0ca7d6..6373ff6b515e 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java @@ -60,7 +60,11 @@ import org.springframework.http.codec.xml.Jaxb2XmlEncoder; import org.springframework.util.MimeTypeUtils; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; import static org.springframework.core.ResolvableType.forClass; /** @@ -149,6 +153,50 @@ public void maxInMemorySize() { assertEquals(size, ((StringDecoder) getNextDecoder(readers)).getMaxInMemorySize()); } + @Test + public void cloneConfigurer() { + ServerCodecConfigurer clone = this.configurer.clone(); + + MultipartHttpMessageReader reader = new MultipartHttpMessageReader(new SynchronossPartHttpMessageReader()); + Jackson2JsonEncoder encoder = new Jackson2JsonEncoder(); + clone.defaultCodecs().multipartReader(reader); + clone.defaultCodecs().serverSentEventEncoder(encoder); + + // Clone has the customizations + + HttpMessageReader actualReader = clone.getReaders().stream() + .filter(r -> r instanceof MultipartHttpMessageReader) + .findFirst() + .get(); + + Encoder actualEncoder = clone.getWriters().stream() + .filter(writer -> writer instanceof ServerSentEventHttpMessageWriter) + .map(writer -> ((ServerSentEventHttpMessageWriter) writer).getEncoder()) + .findFirst() + .get(); + + + assertSame(reader, actualReader); + assertSame(encoder, actualEncoder); + + // Original does not have the customizations + + actualReader = this.configurer.getReaders().stream() + .filter(r -> r instanceof MultipartHttpMessageReader) + .findFirst() + .get(); + + actualEncoder = this.configurer.getWriters().stream() + .filter(writer -> writer instanceof ServerSentEventHttpMessageWriter) + .map(writer -> ((ServerSentEventHttpMessageWriter) writer).getEncoder()) + .findFirst() + .get(); + + + assertNotSame(reader, actualReader); + assertNotSame(encoder, actualEncoder); + } + private Decoder getNextDecoder(List> readers) { HttpMessageReader reader = nextReader(readers); assertEquals(DecoderHttpMessageReader.class, reader.getClass()); diff --git a/spring-webflux/spring-webflux.gradle b/spring-webflux/spring-webflux.gradle index 88483ef35174..46575ca753ff 100644 --- a/spring-webflux/spring-webflux.gradle +++ b/spring-webflux/spring-webflux.gradle @@ -42,7 +42,7 @@ dependencies { optional("com.google.protobuf:protobuf-java-util:3.6.1") testCompile("javax.xml.bind:jaxb-api:2.3.1") testCompile("com.fasterxml:aalto-xml:1.1.1") - testCompile("org.hibernate:hibernate-validator:6.0.17.Final") + testCompile("org.hibernate:hibernate-validator:6.0.18.Final") testCompile "io.reactivex.rxjava2:rxjava:${rxjava2Version}" testCompile("io.projectreactor:reactor-test") testCompile("io.undertow:undertow-core:${undertowVersion}") diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultExchangeStrategiesBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultExchangeStrategiesBuilder.java index aa1523d9ace5..e5703203fc5d 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultExchangeStrategiesBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultExchangeStrategiesBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 the original author 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,13 +42,18 @@ final class DefaultExchangeStrategiesBuilder implements ExchangeStrategies.Build } - private final ClientCodecConfigurer codecConfigurer = ClientCodecConfigurer.create(); + private final ClientCodecConfigurer codecConfigurer; public DefaultExchangeStrategiesBuilder() { + this.codecConfigurer = ClientCodecConfigurer.create(); this.codecConfigurer.registerDefaults(false); } + private DefaultExchangeStrategiesBuilder(DefaultExchangeStrategies other) { + this.codecConfigurer = other.codecConfigurer.clone(); + } + public void defaultConfiguration() { this.codecConfigurer.registerDefaults(true); @@ -62,21 +67,23 @@ public ExchangeStrategies.Builder codecs(Consumer consume @Override public ExchangeStrategies build() { - return new DefaultExchangeStrategies( - this.codecConfigurer.getReaders(), this.codecConfigurer.getWriters()); + return new DefaultExchangeStrategies(this.codecConfigurer); } private static class DefaultExchangeStrategies implements ExchangeStrategies { + private final ClientCodecConfigurer codecConfigurer; + private final List> readers; private final List> writers; - public DefaultExchangeStrategies(List> readers, List> writers) { - this.readers = unmodifiableCopy(readers); - this.writers = unmodifiableCopy(writers); + public DefaultExchangeStrategies(ClientCodecConfigurer codecConfigurer) { + this.codecConfigurer = codecConfigurer; + this.readers = unmodifiableCopy(this.codecConfigurer.getReaders()); + this.writers = unmodifiableCopy(this.codecConfigurer.getWriters()); } private static List unmodifiableCopy(List list) { @@ -93,6 +100,11 @@ public List> messageReaders() { public List> messageWriters() { return this.writers; } + + @Override + public Builder mutate() { + return new DefaultExchangeStrategiesBuilder(this); + } } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java index c8796e91f782..44c3164b701b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java @@ -25,9 +25,11 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.http.client.reactive.JettyClientHttpConnector; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -38,10 +40,22 @@ * Default implementation of {@link WebClient.Builder}. * * @author Rossen Stoyanchev + * @author Brian Clozel * @since 5.0 */ final class DefaultWebClientBuilder implements WebClient.Builder { + private static final boolean reactorClientPresent; + + private static final boolean jettyClientPresent; + + static { + ClassLoader loader = DefaultWebClientBuilder.class.getClassLoader(); + reactorClientPresent = ClassUtils.isPresent("reactor.netty.http.client.HttpClient", loader); + jettyClientPresent = ClassUtils.isPresent("org.eclipse.jetty.client.HttpClient", loader); + } + + @Nullable private String baseUrl; @@ -66,14 +80,17 @@ final class DefaultWebClientBuilder implements WebClient.Builder { @Nullable private ClientHttpConnector connector; - private ExchangeStrategies exchangeStrategies; + @Nullable + private ExchangeStrategies strategies; + + @Nullable + private List> strategiesConfigurers; @Nullable private ExchangeFunction exchangeFunction; public DefaultWebClientBuilder() { - this.exchangeStrategies = ExchangeStrategies.withDefaults(); } public DefaultWebClientBuilder(DefaultWebClientBuilder other) { @@ -95,7 +112,7 @@ public DefaultWebClientBuilder(DefaultWebClientBuilder other) { this.defaultRequest = other.defaultRequest; this.filters = other.filters != null ? new ArrayList<>(other.filters) : null; this.connector = other.connector; - this.exchangeStrategies = other.exchangeStrategies; + this.strategies = other.strategies; this.exchangeFunction = other.exchangeFunction; } @@ -191,8 +208,16 @@ public WebClient.Builder clientConnector(ClientHttpConnector connector) { @Override public WebClient.Builder exchangeStrategies(ExchangeStrategies strategies) { - Assert.notNull(strategies, "ExchangeStrategies must not be null"); - this.exchangeStrategies = strategies; + this.strategies = strategies; + return this; + } + + @Override + public WebClient.Builder exchangeStrategies(Consumer configurer) { + if (this.strategiesConfigurers == null) { + this.strategiesConfigurers = new ArrayList<>(4); + } + this.strategiesConfigurers.add(configurer); return this; } @@ -215,7 +240,9 @@ public WebClient.Builder clone() { @Override public WebClient build() { - ExchangeFunction exchange = initExchangeFunction(); + ExchangeFunction exchange = (this.exchangeFunction == null ? + ExchangeFunctions.create(getOrInitConnector(), initExchangeStrategies()) : + this.exchangeFunction); ExchangeFunction filteredExchange = (this.filters != null ? this.filters.stream() .reduce(ExchangeFilterFunction::andThen) .map(filter -> filter.apply(exchange)) @@ -226,16 +253,29 @@ public WebClient build() { this.defaultRequest, new DefaultWebClientBuilder(this)); } - private ExchangeFunction initExchangeFunction() { - if (this.exchangeFunction != null) { - return this.exchangeFunction; + private ClientHttpConnector getOrInitConnector() { + if (this.connector != null) { + return this.connector; } - else if (this.connector != null) { - return ExchangeFunctions.create(this.connector, this.exchangeStrategies); + else if (reactorClientPresent) { + return new ReactorClientHttpConnector(); } - else { - return ExchangeFunctions.create(new ReactorClientHttpConnector(), this.exchangeStrategies); + else if (jettyClientPresent) { + return new JettyClientHttpConnector(); } + throw new IllegalStateException("No suitable default ClientHttpConnector found"); + } + + private ExchangeStrategies initExchangeStrategies() { + if (CollectionUtils.isEmpty(this.strategiesConfigurers)) { + return this.strategies != null ? this.strategies : ExchangeStrategies.withDefaults(); + } + + ExchangeStrategies.Builder builder = + this.strategies != null ? this.strategies.mutate() : ExchangeStrategies.builder(); + + this.strategiesConfigurers.forEach(configurer -> configurer.accept(builder)); + return builder.build(); } private UriBuilderFactory initUriBuilderFactory() { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeStrategies.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeStrategies.java index 804fbd9a42fd..acf32d0959ae 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeStrategies.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeStrategies.java @@ -47,6 +47,15 @@ public interface ExchangeStrategies { */ List> messageWriters(); + /** + * Return a builder to create a new {@link ExchangeStrategies} instance + * replicated from the current instance. + * @since 5.1.12 + */ + default Builder mutate() { + throw new UnsupportedOperationException(); + } + // Static builder methods diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java index 8dc2a17c0127..f60a822597cc 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java @@ -64,6 +64,7 @@ * * @author Rossen Stoyanchev * @author Arjen Poutsma + * @author Brian Clozel * @since 5.0 */ public interface WebClient { @@ -289,11 +290,25 @@ interface Builder { /** * Configure the {@link ExchangeStrategies} to use. - *

By default this is obtained from {@link ExchangeStrategies#withDefaults()}. + *

Note that in a scenario where the builder is configured by + * multiple parties, it is preferable to use + * {@link #exchangeStrategies(Consumer)} in order to customize the same + * {@code ExchangeStrategies}. This method here sets the strategies that + * everyone else then can customize. + *

By default this is {@link ExchangeStrategies#withDefaults()}. * @param strategies the strategies to use */ Builder exchangeStrategies(ExchangeStrategies strategies); + /** + * Customize the strategies configured via + * {@link #exchangeStrategies(ExchangeStrategies)}. This method is + * designed for use in scenarios where multiple parties wish to update + * the {@code ExchangeStrategies}. + * @since 5.1.12 + */ + Builder exchangeStrategies(Consumer configurer); + /** * Provide an {@link ExchangeFunction} pre-configured with * {@link ClientHttpConnector} and {@link ExchangeStrategies}. diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/ExchangeStrategiesTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/ExchangeStrategiesTests.java index b08662c8fb9c..b25bfe9dd4b7 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/ExchangeStrategiesTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/ExchangeStrategiesTests.java @@ -39,4 +39,16 @@ public void withDefaults() { assertFalse(strategies.messageWriters().isEmpty()); } + @Test + @SuppressWarnings("deprecation") + public void mutate() { + ExchangeStrategies strategies = ExchangeStrategies.empty().build(); + assertTrue(strategies.messageReaders().isEmpty()); + assertTrue(strategies.messageWriters().isEmpty()); + + ExchangeStrategies mutated = strategies.mutate().codecs(codecs -> codecs.registerDefaults(true)).build(); + assertFalse(mutated.messageReaders().isEmpty()); + assertFalse(mutated.messageWriters().isEmpty()); + } + } diff --git a/spring-webmvc/spring-webmvc.gradle b/spring-webmvc/spring-webmvc.gradle index 1c24771a654a..6da9b01d6e1a 100644 --- a/spring-webmvc/spring-webmvc.gradle +++ b/spring-webmvc/spring-webmvc.gradle @@ -66,7 +66,7 @@ dependencies { exclude group: "xerces", module: "xercesImpl" } testCompile("org.xmlunit:xmlunit-matchers:2.6.2") - testCompile("org.hibernate:hibernate-validator:6.0.17.Final") + testCompile("org.hibernate:hibernate-validator:6.0.18.Final") testCompile("io.projectreactor:reactor-core") testCompile("io.reactivex:rxjava:${rxjavaVersion}") testCompile("io.reactivex:rxjava-reactive-streams:${rxjavaAdapterVersion}") diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/groovy/GroovyMarkupConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/groovy/GroovyMarkupConfigurer.java index 741ca35b14af..8c80a96ad7a0 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/groovy/GroovyMarkupConfigurer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/groovy/GroovyMarkupConfigurer.java @@ -121,6 +121,7 @@ public void setTemplateEngine(MarkupTemplateEngine templateEngine) { this.templateEngine = templateEngine; } + @Override public MarkupTemplateEngine getTemplateEngine() { Assert.state(this.templateEngine != null, "No MarkupTemplateEngine set"); return this.templateEngine; diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/annotation/RequestScopedControllerAdviceIntegrationTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/annotation/RequestScopedControllerAdviceIntegrationTests.java new file mode 100644 index 000000000000..062ed71dde46 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/annotation/RequestScopedControllerAdviceIntegrationTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.mvc.annotation; + +import java.util.List; + +import org.junit.Test; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.mock.web.test.MockServletContext; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.context.annotation.RequestScope; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.method.ControllerAdviceBean; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import static org.junit.Assert.assertEquals; + +/** + * Integration tests for request-scoped {@link ControllerAdvice @ControllerAdvice} beans. + * + * @author Sam Brannen + * @since 5.1.12 + */ +public class RequestScopedControllerAdviceIntegrationTests { + + @Test // gh-23985 + public void loadContextWithRequestScopedControllerAdvice() { + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.setServletContext(new MockServletContext()); + context.register(Config.class); + context.refresh(); + + List adviceBeans = ControllerAdviceBean.findAnnotatedBeans(context); + assertEquals(1, adviceBeans.size()); + + ControllerAdviceBean adviceBean = adviceBeans.get(0); + assertEquals(RequestScopedControllerAdvice.class, adviceBean.getBeanType()); + assertEquals(42, adviceBean.getOrder()); + + context.close(); + } + + + @Configuration + @EnableWebMvc + static class Config { + + @Bean + @RequestScope + RequestScopedControllerAdvice requestScopedControllerAdvice() { + return new RequestScopedControllerAdvice(); + } + } + + @ControllerAdvice + @Order(42) + static class RequestScopedControllerAdvice implements Ordered { + + @Override + public int getOrder() { + return 99; + } + } + +} diff --git a/src/docs/asciidoc/web/webflux-webclient.adoc b/src/docs/asciidoc/web/webflux-webclient.adoc index 20980df9dd85..0bc6fdce2981 100644 --- a/src/docs/asciidoc/web/webflux-webclient.adoc +++ b/src/docs/asciidoc/web/webflux-webclient.adoc @@ -42,17 +42,14 @@ The following example configures < { - // ... - }) - .build(); - WebClient client = WebClient.builder() - .exchangeStrategies(strategies) + .exchangeStrategies(builder -> { + return builder.codecs(codecConfigurer -> { + //... + }); + }) .build(); ---- -==== Once built, a `WebClient` instance is immutable. However, you can clone it and build a modified copy without affecting the original instance, as the following example shows: @@ -73,6 +70,31 @@ modified copy without affecting the original instance, as the following example ---- ==== +[[webflux-client-builder-maxinmemorysize]] +=== MaxInMemorySize + +Spring WebFlux configures <> for buffering +data in-memory in codec to avoid application memory issues. By the default this is +configured to 256KB and if that's not enough for your use case, you'll see the following: + +---- +org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer +---- + +You can configure this limit on all default codecs with the following code sample: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + WebClient webClient = WebClient.builder() + .exchangeStrategies(builder -> + builder.codecs(codecs -> + codecs.defaultCodecs().maxInMemorySize(2 * 1024 * 1024) + ) + ) + .build(); +---- + [[webflux-client-builder-reactor]] diff --git a/src/docs/asciidoc/web/webflux.adoc b/src/docs/asciidoc/web/webflux.adoc index 5789f295273c..a43d12b03095 100644 --- a/src/docs/asciidoc/web/webflux.adoc +++ b/src/docs/asciidoc/web/webflux.adoc @@ -776,7 +776,8 @@ To configure buffer sizes, you can check if a given `Decoder` or `HttpMessageRea exposes a `maxInMemorySize` property and if so the Javadoc will have details about default values. In WebFlux, the `ServerCodecConfigurer` provides a <> from where to set all codecs, through the -`maxInMemorySize` property for default codecs. +`maxInMemorySize` property for default codecs. On the client side, the limit can be changed +in <>. For <> the `maxInMemorySize` property limits the size of non-file parts. For file parts it determines the threshold at which the part @@ -882,16 +883,44 @@ The following example shows how to do so for client-side requests: [subs="verbatim,quotes"] ---- Consumer consumer = configurer -> - configurer.defaultCodecs().enableLoggingRequestDetails(true); + configurer.defaultCodecs().enableLoggingRequestDetails(true); WebClient webClient = WebClient.builder() - .exchangeStrategies(ExchangeStrategies.builder().codecs(consumer).build()) - .build(); + .exchangeStrategies(strategies -> strategies.codecs(consumer)) + .build(); ---- ==== +[[webflux-codecs-custom]] +==== Custom codecs + +Applications can register custom codecs for supporting additional media types, +or specific behaviors that are not supported by the default codecs. + +Some configuration options expressed by developers are enforced on default codecs. +Custom codecs might want to get a chance to align with those preferences, +like <> +or <>. +The following example shows how to do so for client-side requests: + +==== +[source,java,indent=0] +[subs="verbatim,quotes"] +---- + Consumer consumer = configurer -> { + CustomDecoder customDecoder = new CustomDecoder(); + configurer.customCodecs().decoder(customDecoder); + configurer.customCodecs().withDefaultCodecConfig(config -> + customDecoder.maxInMemorySize(config.maxInMemorySize()) + ); + } + WebClient webClient = WebClient.builder() + .exchangeStrategies(strategies -> strategies.codecs(consumer)) + .build(); +---- +==== [[webflux-dispatcher-handler]] == `DispatcherHandler`